Vega strike Python Modules doc  0.5.1
Documentation of the " Modules " folder of Vega strike
 All Data Structures Namespaces Files Functions Variables
imaplib.py
Go to the documentation of this file.
1 """IMAP4 client.
2 
3 Based on RFC 2060.
4 
5 Public class: IMAP4
6 Public variable: Debug
7 Public functions: Internaldate2tuple
8  Int2AP
9  ParseFlags
10  Time2Internaldate
11 """
12 
13 # Author: Piers Lauder <piers@cs.su.oz.au> December 1997.
14 #
15 # Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
16 # String method conversion by ESR, February 2001.
17 # GET/SETACL contributed by Anthony Baxter <anthony@interlink.com.au> April 2001.
18 
19 __version__ = "2.49"
20 
21 import binascii, re, socket, time, random, sys
22 
23 __all__ = ["IMAP4", "Internaldate2tuple",
24  "Int2AP", "ParseFlags", "Time2Internaldate"]
25 
26 # Globals
27 
28 CRLF = '\r\n'
29 Debug = 0
30 IMAP4_PORT = 143
31 AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first
32 
33 # Commands
34 
35 Commands = {
36  # name valid states
37  'APPEND': ('AUTH', 'SELECTED'),
38  'AUTHENTICATE': ('NONAUTH',),
39  'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
40  'CHECK': ('SELECTED',),
41  'CLOSE': ('SELECTED',),
42  'COPY': ('SELECTED',),
43  'CREATE': ('AUTH', 'SELECTED'),
44  'DELETE': ('AUTH', 'SELECTED'),
45  'EXAMINE': ('AUTH', 'SELECTED'),
46  'EXPUNGE': ('SELECTED',),
47  'FETCH': ('SELECTED',),
48  'GETACL': ('AUTH', 'SELECTED'),
49  'LIST': ('AUTH', 'SELECTED'),
50  'LOGIN': ('NONAUTH',),
51  'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
52  'LSUB': ('AUTH', 'SELECTED'),
53  'NAMESPACE': ('AUTH', 'SELECTED'),
54  'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
55  'PARTIAL': ('SELECTED',),
56  'RENAME': ('AUTH', 'SELECTED'),
57  'SEARCH': ('SELECTED',),
58  'SELECT': ('AUTH', 'SELECTED'),
59  'SETACL': ('AUTH', 'SELECTED'),
60  'SORT': ('SELECTED',),
61  'STATUS': ('AUTH', 'SELECTED'),
62  'STORE': ('SELECTED',),
63  'SUBSCRIBE': ('AUTH', 'SELECTED'),
64  'UID': ('SELECTED',),
65  'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
66  }
67 
68 # Patterns to match server responses
69 
70 Continuation = re.compile(r'\+( (?P<data>.*))?')
71 Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
72 InternalDate = re.compile(r'.*INTERNALDATE "'
73  r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
74  r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
75  r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
76  r'"')
77 Literal = re.compile(r'.*{(?P<size>\d+)}$')
78 Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
79 Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
80 Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
81 
82 
83 
84 class IMAP4:
85 
86  """IMAP4 client class.
87 
88  Instantiate with: IMAP4([host[, port]])
89 
90  host - host's name (default: localhost);
91  port - port number (default: standard IMAP4 port).
92 
93  All IMAP4rev1 commands are supported by methods of the same
94  name (in lower-case).
95 
96  All arguments to commands are converted to strings, except for
97  AUTHENTICATE, and the last argument to APPEND which is passed as
98  an IMAP4 literal. If necessary (the string contains any
99  non-printing characters or white-space and isn't enclosed with
100  either parentheses or double quotes) each string is quoted.
101  However, the 'password' argument to the LOGIN command is always
102  quoted. If you want to avoid having an argument string quoted
103  (eg: the 'flags' argument to STORE) then enclose the string in
104  parentheses (eg: "(\Deleted)").
105 
106  Each command returns a tuple: (type, [data, ...]) where 'type'
107  is usually 'OK' or 'NO', and 'data' is either the text from the
108  tagged response, or untagged results from command.
109 
110  Errors raise the exception class <instance>.error("<reason>").
111  IMAP4 server errors raise <instance>.abort("<reason>"),
112  which is a sub-class of 'error'. Mailbox status changes
113  from READ-WRITE to READ-ONLY raise the exception class
114  <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
115 
116  "error" exceptions imply a program error.
117  "abort" exceptions imply the connection should be reset, and
118  the command re-tried.
119  "readonly" exceptions imply the command should be re-tried.
120 
121  Note: to use this module, you must read the RFCs pertaining
122  to the IMAP4 protocol, as the semantics of the arguments to
123  each IMAP4 command are left to the invoker, not to mention
124  the results.
125  """
126 
127  class error(Exception): pass # Logical errors - debug required
128  class abort(error): pass # Service errors - close and retry
129  class readonly(abort): pass # Mailbox status changed to READ-ONLY
130 
131  mustquote = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]")
132 
133  def __init__(self, host = '', port = IMAP4_PORT):
134  self.host = host
135  self.port = port
136  self.debug = Debug
137  self.state = 'LOGOUT'
138  self.literal = None # A literal argument to a command
139  self.tagged_commands = {} # Tagged commands awaiting response
140  self.untagged_responses = {} # {typ: [data, ...], ...}
141  self.continuation_response = '' # Last continuation response
142  self.is_readonly = None # READ-ONLY desired state
143  self.tagnum = 0
144 
145  # Open socket to server.
146 
147  self.open(host, port)
148 
149  # Create unique tag for this session,
150  # and compile tagged response matcher.
151 
152  self.tagpre = Int2AP(random.randint(0, 31999))
153  self.tagre = re.compile(r'(?P<tag>'
154  + self.tagpre
155  + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
156 
157  # Get server welcome message,
158  # request and store CAPABILITY response.
159 
160  if __debug__:
161  if self.debug >= 1:
162  _mesg('imaplib version %s' % __version__)
163  _mesg('new IMAP4 connection, tag=%s' % self.tagpre)
164 
165  self.welcome = self._get_response()
166  if self.untagged_responses.has_key('PREAUTH'):
167  self.state = 'AUTH'
168  elif self.untagged_responses.has_key('OK'):
169  self.state = 'NONAUTH'
170  else:
171  raise self.error(self.welcome)
172 
173  cap = 'CAPABILITY'
174  self._simple_command(cap)
175  if not self.untagged_responses.has_key(cap):
176  raise self.error('no CAPABILITY response from server')
177  self.capabilities = tuple(self.untagged_responses[cap][-1].upper().split())
178 
179  if __debug__:
180  if self.debug >= 3:
181  _mesg('CAPABILITIES: %s' % `self.capabilities`)
182 
183  for version in AllowedVersions:
184  if not version in self.capabilities:
185  continue
186  self.PROTOCOL_VERSION = version
187  return
188 
189  raise self.error('server not IMAP4 compliant')
190 
191 
192  def __getattr__(self, attr):
193  # Allow UPPERCASE variants of IMAP4 command methods.
194  if Commands.has_key(attr):
195  return getattr(self, attr.lower())
196  raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
197 
198 
199 
200  # Overridable methods
201 
202 
203  def open(self, host, port):
204  """Setup connection to remote server on "host:port".
205  This connection will be used by the routines:
206  read, readline, send, shutdown.
207  """
208  self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
209  self.sock.connect((self.host, self.port))
210  self.file = self.sock.makefile('rb')
211 
212 
213  def read(self, size):
214  """Read 'size' bytes from remote."""
215  return self.file.read(size)
216 
217 
218  def readline(self):
219  """Read line from remote."""
220  return self.file.readline()
221 
222 
223  def send(self, data):
224  """Send data to remote."""
225  self.sock.sendall(data)
226 
227  def shutdown(self):
228  """Close I/O established in "open"."""
229  self.file.close()
230  self.sock.close()
231 
232 
233  def socket(self):
234  """Return socket instance used to connect to IMAP4 server.
235 
236  socket = <instance>.socket()
237  """
238  return self.sock
239 
240 
241 
242  # Utility methods
243 
244 
245  def recent(self):
246  """Return most recent 'RECENT' responses if any exist,
247  else prompt server for an update using the 'NOOP' command.
248 
249  (typ, [data]) = <instance>.recent()
250 
251  'data' is None if no new messages,
252  else list of RECENT responses, most recent last.
253  """
254  name = 'RECENT'
255  typ, dat = self._untagged_response('OK', [None], name)
256  if dat[-1]:
257  return typ, dat
258  typ, dat = self.noop() # Prod server for response
259  return self._untagged_response(typ, dat, name)
260 
261 
262  def response(self, code):
263  """Return data for response 'code' if received, or None.
264 
265  Old value for response 'code' is cleared.
266 
267  (code, [data]) = <instance>.response(code)
268  """
269  return self._untagged_response(code, [None], code.upper())
270 
271 
272 
273  # IMAP4 commands
274 
275 
276  def append(self, mailbox, flags, date_time, message):
277  """Append message to named mailbox.
278 
279  (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
280 
281  All args except `message' can be None.
282  """
283  name = 'APPEND'
284  if not mailbox:
285  mailbox = 'INBOX'
286  if flags:
287  if (flags[0],flags[-1]) != ('(',')'):
288  flags = '(%s)' % flags
289  else:
290  flags = None
291  if date_time:
292  date_time = Time2Internaldate(date_time)
293  else:
294  date_time = None
295  self.literal = message
296  return self._simple_command(name, mailbox, flags, date_time)
297 
298 
299  def authenticate(self, mechanism, authobject):
300  """Authenticate command - requires response processing.
301 
302  'mechanism' specifies which authentication mechanism is to
303  be used - it must appear in <instance>.capabilities in the
304  form AUTH=<mechanism>.
305 
306  'authobject' must be a callable object:
307 
308  data = authobject(response)
309 
310  It will be called to process server continuation responses.
311  It should return data that will be encoded and sent to server.
312  It should return None if the client abort response '*' should
313  be sent instead.
314  """
315  mech = mechanism.upper()
316  cap = 'AUTH=%s' % mech
317  if not cap in self.capabilities:
318  raise self.error("Server doesn't allow %s authentication." % mech)
319  self.literal = _Authenticator(authobject).process
320  typ, dat = self._simple_command('AUTHENTICATE', mech)
321  if typ != 'OK':
322  raise self.error(dat[-1])
323  self.state = 'AUTH'
324  return typ, dat
325 
326 
327  def check(self):
328  """Checkpoint mailbox on server.
329 
330  (typ, [data]) = <instance>.check()
331  """
332  return self._simple_command('CHECK')
333 
334 
335  def close(self):
336  """Close currently selected mailbox.
337 
338  Deleted messages are removed from writable mailbox.
339  This is the recommended command before 'LOGOUT'.
340 
341  (typ, [data]) = <instance>.close()
342  """
343  try:
344  typ, dat = self._simple_command('CLOSE')
345  finally:
346  self.state = 'AUTH'
347  return typ, dat
348 
349 
350  def copy(self, message_set, new_mailbox):
351  """Copy 'message_set' messages onto end of 'new_mailbox'.
352 
353  (typ, [data]) = <instance>.copy(message_set, new_mailbox)
354  """
355  return self._simple_command('COPY', message_set, new_mailbox)
356 
357 
358  def create(self, mailbox):
359  """Create new mailbox.
360 
361  (typ, [data]) = <instance>.create(mailbox)
362  """
363  return self._simple_command('CREATE', mailbox)
364 
365 
366  def delete(self, mailbox):
367  """Delete old mailbox.
368 
369  (typ, [data]) = <instance>.delete(mailbox)
370  """
371  return self._simple_command('DELETE', mailbox)
372 
373 
374  def expunge(self):
375  """Permanently remove deleted items from selected mailbox.
376 
377  Generates 'EXPUNGE' response for each deleted message.
378 
379  (typ, [data]) = <instance>.expunge()
380 
381  'data' is list of 'EXPUNGE'd message numbers in order received.
382  """
383  name = 'EXPUNGE'
384  typ, dat = self._simple_command(name)
385  return self._untagged_response(typ, dat, name)
386 
387 
388  def fetch(self, message_set, message_parts):
389  """Fetch (parts of) messages.
390 
391  (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
392 
393  'message_parts' should be a string of selected parts
394  enclosed in parentheses, eg: "(UID BODY[TEXT])".
395 
396  'data' are tuples of message part envelope and data.
397  """
398  name = 'FETCH'
399  typ, dat = self._simple_command(name, message_set, message_parts)
400  return self._untagged_response(typ, dat, name)
401 
402 
403  def getacl(self, mailbox):
404  """Get the ACLs for a mailbox.
405 
406  (typ, [data]) = <instance>.getacl(mailbox)
407  """
408  typ, dat = self._simple_command('GETACL', mailbox)
409  return self._untagged_response(typ, dat, 'ACL')
410 
411 
412  def list(self, directory='""', pattern='*'):
413  """List mailbox names in directory matching pattern.
414 
415  (typ, [data]) = <instance>.list(directory='""', pattern='*')
416 
417  'data' is list of LIST responses.
418  """
419  name = 'LIST'
420  typ, dat = self._simple_command(name, directory, pattern)
421  return self._untagged_response(typ, dat, name)
422 
423 
424  def login(self, user, password):
425  """Identify client using plaintext password.
426 
427  (typ, [data]) = <instance>.login(user, password)
428 
429  NB: 'password' will be quoted.
430  """
431  #if not 'AUTH=LOGIN' in self.capabilities:
432  # raise self.error("Server doesn't allow LOGIN authentication." % mech)
433  typ, dat = self._simple_command('LOGIN', user, self._quote(password))
434  if typ != 'OK':
435  raise self.error(dat[-1])
436  self.state = 'AUTH'
437  return typ, dat
438 
439 
440  def logout(self):
441  """Shutdown connection to server.
442 
443  (typ, [data]) = <instance>.logout()
444 
445  Returns server 'BYE' response.
446  """
447  self.state = 'LOGOUT'
448  try: typ, dat = self._simple_command('LOGOUT')
449  except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
450  self.shutdown()
451  if self.untagged_responses.has_key('BYE'):
452  return 'BYE', self.untagged_responses['BYE']
453  return typ, dat
454 
455 
456  def lsub(self, directory='""', pattern='*'):
457  """List 'subscribed' mailbox names in directory matching pattern.
458 
459  (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
460 
461  'data' are tuples of message part envelope and data.
462  """
463  name = 'LSUB'
464  typ, dat = self._simple_command(name, directory, pattern)
465  return self._untagged_response(typ, dat, name)
466 
467 
468  def namespace(self):
469  """ Returns IMAP namespaces ala rfc2342
470 
471  (typ, [data, ...]) = <instance>.namespace()
472  """
473  name = 'NAMESPACE'
474  typ, dat = self._simple_command(name)
475  return self._untagged_response(typ, dat, name)
476 
477 
478  def noop(self):
479  """Send NOOP command.
480 
481  (typ, data) = <instance>.noop()
482  """
483  if __debug__:
484  if self.debug >= 3:
485  _dump_ur(self.untagged_responses)
486  return self._simple_command('NOOP')
487 
488 
489  def partial(self, message_num, message_part, start, length):
490  """Fetch truncated part of a message.
491 
492  (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
493 
494  'data' is tuple of message part envelope and data.
495  """
496  name = 'PARTIAL'
497  typ, dat = self._simple_command(name, message_num, message_part, start, length)
498  return self._untagged_response(typ, dat, 'FETCH')
499 
500 
501  def rename(self, oldmailbox, newmailbox):
502  """Rename old mailbox name to new.
503 
504  (typ, data) = <instance>.rename(oldmailbox, newmailbox)
505  """
506  return self._simple_command('RENAME', oldmailbox, newmailbox)
507 
508 
509  def search(self, charset, *criteria):
510  """Search mailbox for matching messages.
511 
512  (typ, [data]) = <instance>.search(charset, criterium, ...)
513 
514  'data' is space separated list of matching message numbers.
515  """
516  name = 'SEARCH'
517  if charset:
518  typ, dat = apply(self._simple_command, (name, 'CHARSET', charset) + criteria)
519  else:
520  typ, dat = apply(self._simple_command, (name,) + criteria)
521  return self._untagged_response(typ, dat, name)
522 
523 
524  def select(self, mailbox='INBOX', readonly=None):
525  """Select a mailbox.
526 
527  Flush all untagged responses.
528 
529  (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
530 
531  'data' is count of messages in mailbox ('EXISTS' response).
532  """
533  # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
534  self.untagged_responses = {} # Flush old responses.
535  self.is_readonly = readonly
536  if readonly:
537  name = 'EXAMINE'
538  else:
539  name = 'SELECT'
540  typ, dat = self._simple_command(name, mailbox)
541  if typ != 'OK':
542  self.state = 'AUTH' # Might have been 'SELECTED'
543  return typ, dat
544  self.state = 'SELECTED'
545  if self.untagged_responses.has_key('READ-ONLY') \
546  and not readonly:
547  if __debug__:
548  if self.debug >= 1:
549  _dump_ur(self.untagged_responses)
550  raise self.readonly('%s is not writable' % mailbox)
551  return typ, self.untagged_responses.get('EXISTS', [None])
552 
553 
554  def setacl(self, mailbox, who, what):
555  """Set a mailbox acl.
556 
557  (typ, [data]) = <instance>.create(mailbox, who, what)
558  """
559  return self._simple_command('SETACL', mailbox, who, what)
560 
561 
562  def sort(self, sort_criteria, charset, *search_criteria):
563  """IMAP4rev1 extension SORT command.
564 
565  (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
566  """
567  name = 'SORT'
568  #if not name in self.capabilities: # Let the server decide!
569  # raise self.error('unimplemented extension command: %s' % name)
570  if (sort_criteria[0],sort_criteria[-1]) != ('(',')'):
571  sort_criteria = '(%s)' % sort_criteria
572  typ, dat = apply(self._simple_command, (name, sort_criteria, charset) + search_criteria)
573  return self._untagged_response(typ, dat, name)
574 
575 
576  def status(self, mailbox, names):
577  """Request named status conditions for mailbox.
578 
579  (typ, [data]) = <instance>.status(mailbox, names)
580  """
581  name = 'STATUS'
582  #if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide!
583  # raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
584  typ, dat = self._simple_command(name, mailbox, names)
585  return self._untagged_response(typ, dat, name)
586 
587 
588  def store(self, message_set, command, flags):
589  """Alters flag dispositions for messages in mailbox.
590 
591  (typ, [data]) = <instance>.store(message_set, command, flags)
592  """
593  if (flags[0],flags[-1]) != ('(',')'):
594  flags = '(%s)' % flags # Avoid quoting the flags
595  typ, dat = self._simple_command('STORE', message_set, command, flags)
596  return self._untagged_response(typ, dat, 'FETCH')
597 
598 
599  def subscribe(self, mailbox):
600  """Subscribe to new mailbox.
601 
602  (typ, [data]) = <instance>.subscribe(mailbox)
603  """
604  return self._simple_command('SUBSCRIBE', mailbox)
605 
606 
607  def uid(self, command, *args):
608  """Execute "command arg ..." with messages identified by UID,
609  rather than message number.
610 
611  (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
612 
613  Returns response appropriate to 'command'.
614  """
615  command = command.upper()
616  if not Commands.has_key(command):
617  raise self.error("Unknown IMAP4 UID command: %s" % command)
618  if self.state not in Commands[command]:
619  raise self.error('command %s illegal in state %s'
620  % (command, self.state))
621  name = 'UID'
622  typ, dat = apply(self._simple_command, (name, command) + args)
623  if command in ('SEARCH', 'SORT'):
624  name = command
625  else:
626  name = 'FETCH'
627  return self._untagged_response(typ, dat, name)
628 
629 
630  def unsubscribe(self, mailbox):
631  """Unsubscribe from old mailbox.
632 
633  (typ, [data]) = <instance>.unsubscribe(mailbox)
634  """
635  return self._simple_command('UNSUBSCRIBE', mailbox)
636 
637 
638  def xatom(self, name, *args):
639  """Allow simple extension commands
640  notified by server in CAPABILITY response.
641 
642  Assumes command is legal in current state.
643 
644  (typ, [data]) = <instance>.xatom(name, arg, ...)
645 
646  Returns response appropriate to extension command `name'.
647  """
648  name = name.upper()
649  #if not name in self.capabilities: # Let the server decide!
650  # raise self.error('unknown extension command: %s' % name)
651  if not Commands.has_key(name):
652  Commands[name] = (self.state,)
653  return apply(self._simple_command, (name,) + args)
654 
655 
656 
657  # Private methods
658 
659 
660  def _append_untagged(self, typ, dat):
661 
662  if dat is None: dat = ''
663  ur = self.untagged_responses
664  if __debug__:
665  if self.debug >= 5:
666  _mesg('untagged_responses[%s] %s += ["%s"]' %
667  (typ, len(ur.get(typ,'')), dat))
668  if ur.has_key(typ):
669  ur[typ].append(dat)
670  else:
671  ur[typ] = [dat]
672 
673 
674  def _check_bye(self):
675  bye = self.untagged_responses.get('BYE')
676  if bye:
677  raise self.abort(bye[-1])
678 
679 
680  def _command(self, name, *args):
681 
682  if self.state not in Commands[name]:
683  self.literal = None
684  raise self.error(
685  'command %s illegal in state %s' % (name, self.state))
686 
687  for typ in ('OK', 'NO', 'BAD'):
688  if self.untagged_responses.has_key(typ):
689  del self.untagged_responses[typ]
690 
691  if self.untagged_responses.has_key('READ-ONLY') \
692  and not self.is_readonly:
693  raise self.readonly('mailbox status changed to READ-ONLY')
694 
695  tag = self._new_tag()
696  data = '%s %s' % (tag, name)
697  for arg in args:
698  if arg is None: continue
699  data = '%s %s' % (data, self._checkquote(arg))
700 
701  literal = self.literal
702  if literal is not None:
703  self.literal = None
704  if type(literal) is type(self._command):
705  literator = literal
706  else:
707  literator = None
708  data = '%s {%s}' % (data, len(literal))
709 
710  if __debug__:
711  if self.debug >= 4:
712  _mesg('> %s' % data)
713  else:
714  _log('> %s' % data)
715 
716  try:
717  self.send('%s%s' % (data, CRLF))
718  except (socket.error, OSError), val:
719  raise self.abort('socket error: %s' % val)
720 
721  if literal is None:
722  return tag
723 
724  while 1:
725  # Wait for continuation response
726 
727  while self._get_response():
728  if self.tagged_commands[tag]: # BAD/NO?
729  return tag
730 
731  # Send literal
732 
733  if literator:
734  literal = literator(self.continuation_response)
735 
736  if __debug__:
737  if self.debug >= 4:
738  _mesg('write literal size %s' % len(literal))
739 
740  try:
741  self.send(literal)
742  self.send(CRLF)
743  except (socket.error, OSError), val:
744  raise self.abort('socket error: %s' % val)
745 
746  if not literator:
747  break
748 
749  return tag
750 
751 
752  def _command_complete(self, name, tag):
753  self._check_bye()
754  try:
755  typ, data = self._get_tagged_response(tag)
756  except self.abort, val:
757  raise self.abort('command: %s => %s' % (name, val))
758  except self.error, val:
759  raise self.error('command: %s => %s' % (name, val))
760  self._check_bye()
761  if typ == 'BAD':
762  raise self.error('%s command error: %s %s' % (name, typ, data))
763  return typ, data
764 
765 
766  def _get_response(self):
767 
768  # Read response and store.
769  #
770  # Returns None for continuation responses,
771  # otherwise first response line received.
772 
773  resp = self._get_line()
774 
775  # Command completion response?
776 
777  if self._match(self.tagre, resp):
778  tag = self.mo.group('tag')
779  if not self.tagged_commands.has_key(tag):
780  raise self.abort('unexpected tagged response: %s' % resp)
781 
782  typ = self.mo.group('type')
783  dat = self.mo.group('data')
784  self.tagged_commands[tag] = (typ, [dat])
785  else:
786  dat2 = None
787 
788  # '*' (untagged) responses?
789 
790  if not self._match(Untagged_response, resp):
791  if self._match(Untagged_status, resp):
792  dat2 = self.mo.group('data2')
793 
794  if self.mo is None:
795  # Only other possibility is '+' (continuation) response...
796 
797  if self._match(Continuation, resp):
798  self.continuation_response = self.mo.group('data')
799  return None # NB: indicates continuation
800 
801  raise self.abort("unexpected response: '%s'" % resp)
802 
803  typ = self.mo.group('type')
804  dat = self.mo.group('data')
805  if dat is None: dat = '' # Null untagged response
806  if dat2: dat = dat + ' ' + dat2
807 
808  # Is there a literal to come?
809 
810  while self._match(Literal, dat):
811 
812  # Read literal direct from connection.
813 
814  size = int(self.mo.group('size'))
815  if __debug__:
816  if self.debug >= 4:
817  _mesg('read literal size %s' % size)
818  data = self.read(size)
819 
820  # Store response with literal as tuple
821 
822  self._append_untagged(typ, (dat, data))
823 
824  # Read trailer - possibly containing another literal
825 
826  dat = self._get_line()
827 
828  self._append_untagged(typ, dat)
829 
830  # Bracketed response information?
831 
832  if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
833  self._append_untagged(self.mo.group('type'), self.mo.group('data'))
834 
835  if __debug__:
836  if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
837  _mesg('%s response: %s' % (typ, dat))
838 
839  return resp
840 
841 
842  def _get_tagged_response(self, tag):
843 
844  while 1:
845  result = self.tagged_commands[tag]
846  if result is not None:
847  del self.tagged_commands[tag]
848  return result
849 
850  # Some have reported "unexpected response" exceptions.
851  # Note that ignoring them here causes loops.
852  # Instead, send me details of the unexpected response and
853  # I'll update the code in `_get_response()'.
854 
855  try:
856  self._get_response()
857  except self.abort, val:
858  if __debug__:
859  if self.debug >= 1:
860  print_log()
861  raise
862 
863 
864  def _get_line(self):
865 
866  line = self.readline()
867  if not line:
868  raise self.abort('socket error: EOF')
869 
870  # Protocol mandates all lines terminated by CRLF
871 
872  line = line[:-2]
873  if __debug__:
874  if self.debug >= 4:
875  _mesg('< %s' % line)
876  else:
877  _log('< %s' % line)
878  return line
879 
880 
881  def _match(self, cre, s):
882 
883  # Run compiled regular expression match method on 's'.
884  # Save result, return success.
885 
886  self.mo = cre.match(s)
887  if __debug__:
888  if self.mo is not None and self.debug >= 5:
889  _mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`))
890  return self.mo is not None
891 
892 
893  def _new_tag(self):
894 
895  tag = '%s%s' % (self.tagpre, self.tagnum)
896  self.tagnum = self.tagnum + 1
897  self.tagged_commands[tag] = None
898  return tag
899 
900 
901  def _checkquote(self, arg):
902 
903  # Must quote command args if non-alphanumeric chars present,
904  # and not already quoted.
905 
906  if type(arg) is not type(''):
907  return arg
908  if (arg[0],arg[-1]) in (('(',')'),('"','"')):
909  return arg
910  if self.mustquote.search(arg) is None:
911  return arg
912  return self._quote(arg)
913 
914 
915  def _quote(self, arg):
916 
917  arg = arg.replace('\\', '\\\\')
918  arg = arg.replace('"', '\\"')
919 
920  return '"%s"' % arg
921 
922 
923  def _simple_command(self, name, *args):
924 
925  return self._command_complete(name, apply(self._command, (name,) + args))
926 
927 
928  def _untagged_response(self, typ, dat, name):
929 
930  if typ == 'NO':
931  return typ, dat
932  if not self.untagged_responses.has_key(name):
933  return typ, [None]
934  data = self.untagged_responses[name]
935  if __debug__:
936  if self.debug >= 5:
937  _mesg('untagged_responses[%s] => %s' % (name, data))
938  del self.untagged_responses[name]
939  return typ, data
940 
941 
942 
943 class _Authenticator:
944 
945  """Private class to provide en/decoding
946  for base64-based authentication conversation.
947  """
948 
949  def __init__(self, mechinst):
950  self.mech = mechinst # Callable object to provide/process data
951 
952  def process(self, data):
953  ret = self.mech(self.decode(data))
954  if ret is None:
955  return '*' # Abort conversation
956  return self.encode(ret)
957 
958  def encode(self, inp):
959  #
960  # Invoke binascii.b2a_base64 iteratively with
961  # short even length buffers, strip the trailing
962  # line feed from the result and append. "Even"
963  # means a number that factors to both 6 and 8,
964  # so when it gets to the end of the 8-bit input
965  # there's no partial 6-bit output.
966  #
967  oup = ''
968  while inp:
969  if len(inp) > 48:
970  t = inp[:48]
971  inp = inp[48:]
972  else:
973  t = inp
974  inp = ''
975  e = binascii.b2a_base64(t)
976  if e:
977  oup = oup + e[:-1]
978  return oup
979 
980  def decode(self, inp):
981  if not inp:
982  return ''
983  return binascii.a2b_base64(inp)
984 
985 
986 
987 Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
988  'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
989 
991  """Convert IMAP4 INTERNALDATE to UT.
992 
993  Returns Python time module tuple.
994  """
995 
996  mo = InternalDate.match(resp)
997  if not mo:
998  return None
999 
1000  mon = Mon2num[mo.group('mon')]
1001  zonen = mo.group('zonen')
1002 
1003  day = int(mo.group('day'))
1004  year = int(mo.group('year'))
1005  hour = int(mo.group('hour'))
1006  min = int(mo.group('min'))
1007  sec = int(mo.group('sec'))
1008  zoneh = int(mo.group('zoneh'))
1009  zonem = int(mo.group('zonem'))
1010 
1011  # INTERNALDATE timezone must be subtracted to get UT
1012 
1013  zone = (zoneh*60 + zonem)*60
1014  if zonen == '-':
1015  zone = -zone
1016 
1017  tt = (year, mon, day, hour, min, sec, -1, -1, -1)
1018 
1019  utc = time.mktime(tt)
1020 
1021  # Following is necessary because the time module has no 'mkgmtime'.
1022  # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
1023 
1024  lt = time.localtime(utc)
1025  if time.daylight and lt[-1]:
1026  zone = zone + time.altzone
1027  else:
1028  zone = zone + time.timezone
1029 
1030  return time.localtime(utc - zone)
1031 
1032 
1033 
1034 def Int2AP(num):
1035 
1036  """Convert integer to A-P string representation."""
1037 
1038  val = ''; AP = 'ABCDEFGHIJKLMNOP'
1039  num = int(abs(num))
1040  while num:
1041  num, mod = divmod(num, 16)
1042  val = AP[mod] + val
1043  return val
1044 
1045 
1046 
1047 def ParseFlags(resp):
1048 
1049  """Convert IMAP4 flags response to python tuple."""
1050 
1051  mo = Flags.match(resp)
1052  if not mo:
1053  return ()
1054 
1055  return tuple(mo.group('flags').split())
1056 
1057 
1058 def Time2Internaldate(date_time):
1059 
1060  """Convert 'date_time' to IMAP4 INTERNALDATE representation.
1061 
1062  Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
1063  """
1064 
1065  if isinstance(date_time, (int, float)):
1066  tt = time.localtime(date_time)
1067  elif isinstance(date_time, (tuple, time.struct_time)):
1068  tt = date_time
1069  elif isinstance(date_time, str):
1070  return date_time # Assume in correct format
1071  else:
1072  raise ValueError("date_time not of a known type")
1073 
1074  dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
1075  if dt[0] == '0':
1076  dt = ' ' + dt[1:]
1077  if time.daylight and tt[-1]:
1078  zone = -time.altzone
1079  else:
1080  zone = -time.timezone
1081  return '"' + dt + " %+03d%02d" % divmod(zone/60, 60) + '"'
1082 
1083 
1084 
1085 if __debug__:
1086 
1087  def _mesg(s, secs=None):
1088  if secs is None:
1089  secs = time.time()
1090  tm = time.strftime('%M:%S', time.localtime(secs))
1091  sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
1092  sys.stderr.flush()
1093 
1094  def _dump_ur(dict):
1095  # Dump untagged responses (in `dict').
1096  l = dict.items()
1097  if not l: return
1098  t = '\n\t\t'
1099  l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l)
1100  _mesg('untagged responses dump:%s%s' % (t, t.join(l)))
1101 
1102  _cmd_log = [] # Last `_cmd_log_len' interactions
1103  _cmd_log_len = 10
1104 
1105  def _log(line):
1106  # Keep log of last `_cmd_log_len' interactions for debugging.
1107  if len(_cmd_log) == _cmd_log_len:
1108  del _cmd_log[0]
1109  _cmd_log.append((time.time(), line))
1110 
1111  def print_log():
1112  _mesg('last %d IMAP4 interactions:' % len(_cmd_log))
1113  for secs,line in _cmd_log:
1114  _mesg(line, secs)
1115 
1116 
1117 
1118 if __name__ == '__main__':
1119 
1120  import getopt, getpass
1121 
1122  try:
1123  optlist, args = getopt.getopt(sys.argv[1:], 'd:')
1124  except getopt.error, val:
1125  pass
1126 
1127  for opt,val in optlist:
1128  if opt == '-d':
1129  Debug = int(val)
1130 
1131  if not args: args = ('',)
1132 
1133  host = args[0]
1134 
1136  PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost"))
1137 
1138  test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER, 'lf':CRLF}
1139  test_seq1 = (
1140  ('login', (USER, PASSWD)),
1141  ('create', ('/tmp/xxx 1',)),
1142  ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1143  ('CREATE', ('/tmp/yyz 2',)),
1144  ('append', ('/tmp/yyz 2', None, None, test_mesg)),
1145  ('list', ('/tmp', 'yy*')),
1146  ('select', ('/tmp/yyz 2',)),
1147  ('search', (None, 'SUBJECT', 'test')),
1148  ('partial', ('1', 'RFC822', 1, 1024)),
1149  ('store', ('1', 'FLAGS', '(\Deleted)')),
1150  ('namespace', ()),
1151  ('expunge', ()),
1152  ('recent', ()),
1153  ('close', ()),
1154  )
1155 
1156  test_seq2 = (
1157  ('select', ()),
1158  ('response',('UIDVALIDITY',)),
1159  ('uid', ('SEARCH', 'ALL')),
1160  ('response', ('EXISTS',)),
1161  ('append', (None, None, None, test_mesg)),
1162  ('recent', ()),
1163  ('logout', ()),
1164  )
1165 
1166  def run(cmd, args):
1167  _mesg('%s %s' % (cmd, args))
1168  typ, dat = apply(getattr(M, cmd), args)
1169  _mesg('%s => %s %s' % (cmd, typ, dat))
1170  return dat
1171 
1172  try:
1173  M = IMAP4(host)
1174  _mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
1175  _mesg('CAPABILITIES = %s' % `M.capabilities`)
1176 
1177  for cmd,args in test_seq1:
1178  run(cmd, args)
1179 
1180  for ml in run('list', ('/tmp/', 'yy%')):
1181  mo = re.match(r'.*"([^"]+)"$', ml)
1182  if mo: path = mo.group(1)
1183  else: path = ml.split()[-1]
1184  run('delete', (path,))
1185 
1186  for cmd,args in test_seq2:
1187  dat = run(cmd, args)
1188 
1189  if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
1190  continue
1191 
1192  uid = dat[-1].split()
1193  if not uid: continue
1194  run('uid', ('FETCH', '%s' % uid[-1],
1195  '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1196 
1197  print '\nAll tests OK.'
1198 
1199  except:
1200  print '\nTests failed.'
1201 
1202  if not Debug:
1203  print '''
1204 If you would like to see debugging output,
1205 try: %s -d5
1206 ''' % sys.argv[0]
1207 
1208  raise