The 0.5 release happened on 2/15, not on 2/14. :-)
[python/dscho.git] / Lib / imaplib.py
blob027557132331c5b9f34d084235ce4665eeea8bd0
2 """IMAP4 client.
4 Based on RFC 2060.
6 Author: Piers Lauder <piers@cs.su.oz.au> December 1997.
8 Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
10 Public class: IMAP4
11 Public variable: Debug
12 Public functions: Internaldate2tuple
13 Int2AP
14 ParseFlags
15 Time2Internaldate
16 """
18 __version__ = "2.16"
20 import binascii, re, socket, string, time, random, sys
22 # Globals
24 CRLF = '\r\n'
25 Debug = 0
26 IMAP4_PORT = 143
27 AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first
29 # Commands
31 Commands = {
32 # name valid states
33 'APPEND': ('AUTH', 'SELECTED'),
34 'AUTHENTICATE': ('NONAUTH',),
35 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
36 'CHECK': ('SELECTED',),
37 'CLOSE': ('SELECTED',),
38 'COPY': ('SELECTED',),
39 'CREATE': ('AUTH', 'SELECTED'),
40 'DELETE': ('AUTH', 'SELECTED'),
41 'EXAMINE': ('AUTH', 'SELECTED'),
42 'EXPUNGE': ('SELECTED',),
43 'FETCH': ('SELECTED',),
44 'LIST': ('AUTH', 'SELECTED'),
45 'LOGIN': ('NONAUTH',),
46 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
47 'LSUB': ('AUTH', 'SELECTED'),
48 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
49 'PARTIAL': ('SELECTED',),
50 'RENAME': ('AUTH', 'SELECTED'),
51 'SEARCH': ('SELECTED',),
52 'SELECT': ('AUTH', 'SELECTED'),
53 'STATUS': ('AUTH', 'SELECTED'),
54 'STORE': ('SELECTED',),
55 'SUBSCRIBE': ('AUTH', 'SELECTED'),
56 'UID': ('SELECTED',),
57 'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
60 # Patterns to match server responses
62 Continuation = re.compile(r'\+( (?P<data>.*))?')
63 Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
64 InternalDate = re.compile(r'.*INTERNALDATE "'
65 r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
66 r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
67 r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
68 r'"')
69 Literal = re.compile(r'(?P<data>.*) {(?P<size>\d+)}$')
70 Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
71 Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
72 Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
76 class IMAP4:
78 """IMAP4 client class.
80 Instantiate with: IMAP4([host[, port]])
82 host - host's name (default: localhost);
83 port - port number (default: standard IMAP4 port).
85 All IMAP4rev1 commands are supported by methods of the same
86 name (in lower-case).
88 All arguments to commands are converted to strings, except for
89 AUTHENTICATE, and the last argument to APPEND which is passed as
90 an IMAP4 literal. If necessary (the string contains
91 white-space and isn't enclosed with either parentheses or
92 double quotes) each string is quoted. However, the 'password'
93 argument to the LOGIN command is always quoted.
95 Each command returns a tuple: (type, [data, ...]) where 'type'
96 is usually 'OK' or 'NO', and 'data' is either the text from the
97 tagged response, or untagged results from command.
99 Errors raise the exception class <instance>.error("<reason>").
100 IMAP4 server errors raise <instance>.abort("<reason>"),
101 which is a sub-class of 'error'. Mailbox status changes
102 from READ-WRITE to READ-ONLY raise the exception class
103 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
105 "error" exceptions imply a program error.
106 "abort" exceptions imply the connection should be reset, and
107 the command re-tried.
108 "readonly" exceptions imply the command should be re-tried.
110 Note: to use this module, you must read the RFCs pertaining
111 to the IMAP4 protocol, as the semantics of the arguments to
112 each IMAP4 command are left to the invoker, not to mention
113 the results.
116 class error(Exception): pass # Logical errors - debug required
117 class abort(error): pass # Service errors - close and retry
118 class readonly(abort): pass # Mailbox status changed to READ-ONLY
120 mustquote = re.compile(r'\W') # Match any non-alphanumeric character
122 def __init__(self, host = '', port = IMAP4_PORT):
123 self.host = host
124 self.port = port
125 self.debug = Debug
126 self.state = 'LOGOUT'
127 self.literal = None # A literal argument to a command
128 self.tagged_commands = {} # Tagged commands awaiting response
129 self.untagged_responses = {} # {typ: [data, ...], ...}
130 self.continuation_response = '' # Last continuation response
131 self.tagnum = 0
133 # Open socket to server.
135 self.open(host, port)
137 # Create unique tag for this session,
138 # and compile tagged response matcher.
140 self.tagpre = Int2AP(random.randint(0, 31999))
141 self.tagre = re.compile(r'(?P<tag>'
142 + self.tagpre
143 + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
145 # Get server welcome message,
146 # request and store CAPABILITY response.
148 if __debug__:
149 if self.debug >= 1:
150 _mesg('new IMAP4 connection, tag=%s' % self.tagpre)
152 self.welcome = self._get_response()
153 if self.untagged_responses.has_key('PREAUTH'):
154 self.state = 'AUTH'
155 elif self.untagged_responses.has_key('OK'):
156 self.state = 'NONAUTH'
157 else:
158 raise self.error(self.welcome)
160 cap = 'CAPABILITY'
161 self._simple_command(cap)
162 if not self.untagged_responses.has_key(cap):
163 raise self.error('no CAPABILITY response from server')
164 self.capabilities = tuple(string.split(string.upper(self.untagged_responses[cap][-1])))
166 if __debug__:
167 if self.debug >= 3:
168 _mesg('CAPABILITIES: %s' % `self.capabilities`)
170 for version in AllowedVersions:
171 if not version in self.capabilities:
172 continue
173 self.PROTOCOL_VERSION = version
174 return
176 raise self.error('server not IMAP4 compliant')
179 def __getattr__(self, attr):
180 # Allow UPPERCASE variants of IMAP4 command methods.
181 if Commands.has_key(attr):
182 return eval("self.%s" % string.lower(attr))
183 raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
187 # Public methods
190 def open(self, host, port):
191 """Setup 'self.sock' and 'self.file'."""
192 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
193 self.sock.connect(self.host, self.port)
194 self.file = self.sock.makefile('r')
197 def recent(self):
198 """Return most recent 'RECENT' responses if any exist,
199 else prompt server for an update using the 'NOOP' command.
201 (typ, [data]) = <instance>.recent()
203 'data' is None if no new messages,
204 else list of RECENT responses, most recent last.
206 name = 'RECENT'
207 typ, dat = self._untagged_response('OK', [None], name)
208 if dat[-1]:
209 return typ, dat
210 typ, dat = self.noop() # Prod server for response
211 return self._untagged_response(typ, dat, name)
214 def response(self, code):
215 """Return data for response 'code' if received, or None.
217 Old value for response 'code' is cleared.
219 (code, [data]) = <instance>.response(code)
221 return self._untagged_response(code, [None], string.upper(code))
224 def socket(self):
225 """Return socket instance used to connect to IMAP4 server.
227 socket = <instance>.socket()
229 return self.sock
233 # IMAP4 commands
236 def append(self, mailbox, flags, date_time, message):
237 """Append message to named mailbox.
239 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
241 All args except `message' can be None.
243 name = 'APPEND'
244 if not mailbox:
245 mailbox = 'INBOX'
246 if flags:
247 if (flags[0],flags[-1]) != ('(',')'):
248 flags = '(%s)' % flags
249 else:
250 flags = None
251 if date_time:
252 date_time = Time2Internaldate(date_time)
253 else:
254 date_time = None
255 self.literal = message
256 return self._simple_command(name, mailbox, flags, date_time)
259 def authenticate(self, mechanism, authobject):
260 """Authenticate command - requires response processing.
262 'mechanism' specifies which authentication mechanism is to
263 be used - it must appear in <instance>.capabilities in the
264 form AUTH=<mechanism>.
266 'authobject' must be a callable object:
268 data = authobject(response)
270 It will be called to process server continuation responses.
271 It should return data that will be encoded and sent to server.
272 It should return None if the client abort response '*' should
273 be sent instead.
275 mech = string.upper(mechanism)
276 cap = 'AUTH=%s' % mech
277 if not cap in self.capabilities:
278 raise self.error("Server doesn't allow %s authentication." % mech)
279 self.literal = _Authenticator(authobject).process
280 typ, dat = self._simple_command('AUTHENTICATE', mech)
281 if typ != 'OK':
282 raise self.error(dat[-1])
283 self.state = 'AUTH'
284 return typ, dat
287 def check(self):
288 """Checkpoint mailbox on server.
290 (typ, [data]) = <instance>.check()
292 return self._simple_command('CHECK')
295 def close(self):
296 """Close currently selected mailbox.
298 Deleted messages are removed from writable mailbox.
299 This is the recommended command before 'LOGOUT'.
301 (typ, [data]) = <instance>.close()
303 try:
304 typ, dat = self._simple_command('CLOSE')
305 finally:
306 self.state = 'AUTH'
307 return typ, dat
310 def copy(self, message_set, new_mailbox):
311 """Copy 'message_set' messages onto end of 'new_mailbox'.
313 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
315 return self._simple_command('COPY', message_set, new_mailbox)
318 def create(self, mailbox):
319 """Create new mailbox.
321 (typ, [data]) = <instance>.create(mailbox)
323 return self._simple_command('CREATE', mailbox)
326 def delete(self, mailbox):
327 """Delete old mailbox.
329 (typ, [data]) = <instance>.delete(mailbox)
331 return self._simple_command('DELETE', mailbox)
334 def expunge(self):
335 """Permanently remove deleted items from selected mailbox.
337 Generates 'EXPUNGE' response for each deleted message.
339 (typ, [data]) = <instance>.expunge()
341 'data' is list of 'EXPUNGE'd message numbers in order received.
343 name = 'EXPUNGE'
344 typ, dat = self._simple_command(name)
345 return self._untagged_response(typ, dat, name)
348 def fetch(self, message_set, message_parts):
349 """Fetch (parts of) messages.
351 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
353 'data' are tuples of message part envelope and data.
355 name = 'FETCH'
356 typ, dat = self._simple_command(name, message_set, message_parts)
357 return self._untagged_response(typ, dat, name)
360 def list(self, directory='""', pattern='*'):
361 """List mailbox names in directory matching pattern.
363 (typ, [data]) = <instance>.list(directory='""', pattern='*')
365 'data' is list of LIST responses.
367 name = 'LIST'
368 typ, dat = self._simple_command(name, directory, pattern)
369 return self._untagged_response(typ, dat, name)
372 def login(self, user, password):
373 """Identify client using plaintext password.
375 (typ, [data]) = <instance>.login(user, password)
377 NB: 'password' will be quoted.
379 #if not 'AUTH=LOGIN' in self.capabilities:
380 # raise self.error("Server doesn't allow LOGIN authentication." % mech)
381 typ, dat = self._simple_command('LOGIN', user, self._quote(password))
382 if typ != 'OK':
383 raise self.error(dat[-1])
384 self.state = 'AUTH'
385 return typ, dat
388 def logout(self):
389 """Shutdown connection to server.
391 (typ, [data]) = <instance>.logout()
393 Returns server 'BYE' response.
395 self.state = 'LOGOUT'
396 try: typ, dat = self._simple_command('LOGOUT')
397 except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
398 self.file.close()
399 self.sock.close()
400 if self.untagged_responses.has_key('BYE'):
401 return 'BYE', self.untagged_responses['BYE']
402 return typ, dat
405 def lsub(self, directory='""', pattern='*'):
406 """List 'subscribed' mailbox names in directory matching pattern.
408 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
410 'data' are tuples of message part envelope and data.
412 name = 'LSUB'
413 typ, dat = self._simple_command(name, directory, pattern)
414 return self._untagged_response(typ, dat, name)
417 def noop(self):
418 """Send NOOP command.
420 (typ, data) = <instance>.noop()
422 if __debug__:
423 if self.debug >= 3:
424 _dump_ur(self.untagged_responses)
425 return self._simple_command('NOOP')
428 def partial(self, message_num, message_part, start, length):
429 """Fetch truncated part of a message.
431 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
433 'data' is tuple of message part envelope and data.
435 name = 'PARTIAL'
436 typ, dat = self._simple_command(name, message_num, message_part, start, length)
437 return self._untagged_response(typ, dat, 'FETCH')
440 def rename(self, oldmailbox, newmailbox):
441 """Rename old mailbox name to new.
443 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
445 return self._simple_command('RENAME', oldmailbox, newmailbox)
448 def search(self, charset, criteria):
449 """Search mailbox for matching messages.
451 (typ, [data]) = <instance>.search(charset, criteria)
453 'data' is space separated list of matching message numbers.
455 name = 'SEARCH'
456 if charset:
457 charset = 'CHARSET ' + charset
458 typ, dat = self._simple_command(name, charset, criteria)
459 return self._untagged_response(typ, dat, name)
462 def select(self, mailbox='INBOX', readonly=None):
463 """Select a mailbox.
465 Flush all untagged responses.
467 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
469 'data' is count of messages in mailbox ('EXISTS' response).
471 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
472 self.untagged_responses = {} # Flush old responses.
473 if readonly:
474 name = 'EXAMINE'
475 else:
476 name = 'SELECT'
477 typ, dat = self._simple_command(name, mailbox)
478 if typ != 'OK':
479 self.state = 'AUTH' # Might have been 'SELECTED'
480 return typ, dat
481 self.state = 'SELECTED'
482 if not self.untagged_responses.has_key('READ-WRITE') \
483 and not readonly:
484 if __debug__:
485 if self.debug >= 1:
486 _dump_ur(self.untagged_responses)
487 raise self.readonly('%s is not writable' % mailbox)
488 return typ, self.untagged_responses.get('EXISTS', [None])
491 def status(self, mailbox, names):
492 """Request named status conditions for mailbox.
494 (typ, [data]) = <instance>.status(mailbox, names)
496 name = 'STATUS'
497 if self.PROTOCOL_VERSION == 'IMAP4':
498 raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
499 typ, dat = self._simple_command(name, mailbox, names)
500 return self._untagged_response(typ, dat, name)
503 def store(self, message_set, command, flag_list):
504 """Alters flag dispositions for messages in mailbox.
506 (typ, [data]) = <instance>.store(message_set, command, flag_list)
508 typ, dat = self._simple_command('STORE', message_set, command, flag_list)
509 return self._untagged_response(typ, dat, 'FETCH')
512 def subscribe(self, mailbox):
513 """Subscribe to new mailbox.
515 (typ, [data]) = <instance>.subscribe(mailbox)
517 return self._simple_command('SUBSCRIBE', mailbox)
520 def uid(self, command, *args):
521 """Execute "command arg ..." with messages identified by UID,
522 rather than message number.
524 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
526 Returns response appropriate to 'command'.
528 command = string.upper(command)
529 if not Commands.has_key(command):
530 raise self.error("Unknown IMAP4 UID command: %s" % command)
531 if self.state not in Commands[command]:
532 raise self.error('command %s illegal in state %s'
533 % (command, self.state))
534 name = 'UID'
535 typ, dat = apply(self._simple_command, (name, command) + args)
536 if command == 'SEARCH':
537 name = 'SEARCH'
538 else:
539 name = 'FETCH'
540 return self._untagged_response(typ, dat, name)
543 def unsubscribe(self, mailbox):
544 """Unsubscribe from old mailbox.
546 (typ, [data]) = <instance>.unsubscribe(mailbox)
548 return self._simple_command('UNSUBSCRIBE', mailbox)
551 def xatom(self, name, *args):
552 """Allow simple extension commands
553 notified by server in CAPABILITY response.
555 (typ, [data]) = <instance>.xatom(name, arg, ...)
557 if name[0] != 'X' or not name in self.capabilities:
558 raise self.error('unknown extension command: %s' % name)
559 return apply(self._simple_command, (name,) + args)
563 # Private methods
566 def _append_untagged(self, typ, dat):
568 if dat is None: dat = ''
569 ur = self.untagged_responses
570 if __debug__:
571 if self.debug >= 5:
572 _mesg('untagged_responses[%s] %s += ["%s"]' %
573 (typ, len(ur.get(typ,'')), dat))
574 if ur.has_key(typ):
575 ur[typ].append(dat)
576 else:
577 ur[typ] = [dat]
580 def _check_bye(self):
581 bye = self.untagged_responses.get('BYE')
582 if bye:
583 raise self.abort(bye[-1])
586 def _command(self, name, *args):
588 if self.state not in Commands[name]:
589 self.literal = None
590 raise self.error(
591 'command %s illegal in state %s' % (name, self.state))
593 for typ in ('OK', 'NO', 'BAD'):
594 if self.untagged_responses.has_key(typ):
595 del self.untagged_responses[typ]
597 if self.untagged_responses.has_key('READ-WRITE') \
598 and self.untagged_responses.has_key('READ-ONLY'):
599 del self.untagged_responses['READ-WRITE']
600 raise self.readonly('mailbox status changed to READ-ONLY')
602 tag = self._new_tag()
603 data = '%s %s' % (tag, name)
604 for arg in args:
605 if arg is None: continue
606 data = '%s %s' % (data, self._checkquote(arg))
608 literal = self.literal
609 if literal is not None:
610 self.literal = None
611 if type(literal) is type(self._command):
612 literator = literal
613 else:
614 literator = None
615 data = '%s {%s}' % (data, len(literal))
617 if __debug__:
618 if self.debug >= 4:
619 _mesg('> %s' % data)
620 else:
621 _log('> %s' % data)
623 try:
624 self.sock.send('%s%s' % (data, CRLF))
625 except socket.error, val:
626 raise self.abort('socket error: %s' % val)
628 if literal is None:
629 return tag
631 while 1:
632 # Wait for continuation response
634 while self._get_response():
635 if self.tagged_commands[tag]: # BAD/NO?
636 return tag
638 # Send literal
640 if literator:
641 literal = literator(self.continuation_response)
643 if __debug__:
644 if self.debug >= 4:
645 _mesg('write literal size %s' % len(literal))
647 try:
648 self.sock.send(literal)
649 self.sock.send(CRLF)
650 except socket.error, val:
651 raise self.abort('socket error: %s' % val)
653 if not literator:
654 break
656 return tag
659 def _command_complete(self, name, tag):
660 self._check_bye()
661 try:
662 typ, data = self._get_tagged_response(tag)
663 except self.abort, val:
664 raise self.abort('command: %s => %s' % (name, val))
665 except self.error, val:
666 raise self.error('command: %s => %s' % (name, val))
667 self._check_bye()
668 if typ == 'BAD':
669 raise self.error('%s command error: %s %s' % (name, typ, data))
670 return typ, data
673 def _get_response(self):
675 # Read response and store.
677 # Returns None for continuation responses,
678 # otherwise first response line received.
680 resp = self._get_line()
682 # Command completion response?
684 if self._match(self.tagre, resp):
685 tag = self.mo.group('tag')
686 if not self.tagged_commands.has_key(tag):
687 raise self.abort('unexpected tagged response: %s' % resp)
689 typ = self.mo.group('type')
690 dat = self.mo.group('data')
691 self.tagged_commands[tag] = (typ, [dat])
692 else:
693 dat2 = None
695 # '*' (untagged) responses?
697 if not self._match(Untagged_response, resp):
698 if self._match(Untagged_status, resp):
699 dat2 = self.mo.group('data2')
701 if self.mo is None:
702 # Only other possibility is '+' (continuation) rsponse...
704 if self._match(Continuation, resp):
705 self.continuation_response = self.mo.group('data')
706 return None # NB: indicates continuation
708 raise self.abort("unexpected response: '%s'" % resp)
710 typ = self.mo.group('type')
711 dat = self.mo.group('data')
712 if dat is None: dat = '' # Null untagged response
713 if dat2: dat = dat + ' ' + dat2
715 # Is there a literal to come?
717 while self._match(Literal, dat):
719 # Read literal direct from connection.
721 size = string.atoi(self.mo.group('size'))
722 if __debug__:
723 if self.debug >= 4:
724 _mesg('read literal size %s' % size)
725 data = self.file.read(size)
727 # Store response with literal as tuple
729 self._append_untagged(typ, (dat, data))
731 # Read trailer - possibly containing another literal
733 dat = self._get_line()
735 self._append_untagged(typ, dat)
737 # Bracketed response information?
739 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
740 self._append_untagged(self.mo.group('type'), self.mo.group('data'))
742 if __debug__:
743 if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
744 _mesg('%s response: %s' % (typ, dat))
746 return resp
749 def _get_tagged_response(self, tag):
751 while 1:
752 result = self.tagged_commands[tag]
753 if result is not None:
754 del self.tagged_commands[tag]
755 return result
756 self._get_response()
759 def _get_line(self):
761 line = self.file.readline()
762 if not line:
763 raise self.abort('socket error: EOF')
765 # Protocol mandates all lines terminated by CRLF
767 line = line[:-2]
768 if __debug__:
769 if self.debug >= 4:
770 _mesg('< %s' % line)
771 else:
772 _log('< %s' % line)
773 return line
776 def _match(self, cre, s):
778 # Run compiled regular expression match method on 's'.
779 # Save result, return success.
781 self.mo = cre.match(s)
782 if __debug__:
783 if self.mo is not None and self.debug >= 5:
784 _mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`))
785 return self.mo is not None
788 def _new_tag(self):
790 tag = '%s%s' % (self.tagpre, self.tagnum)
791 self.tagnum = self.tagnum + 1
792 self.tagged_commands[tag] = None
793 return tag
796 def _checkquote(self, arg):
798 # Must quote command args if non-alphanumeric chars present,
799 # and not already quoted.
801 if type(arg) is not type(''):
802 return arg
803 if (arg[0],arg[-1]) in (('(',')'),('"','"')):
804 return arg
805 if self.mustquote.search(arg) is None:
806 return arg
807 return self._quote(arg)
810 def _quote(self, arg):
812 arg = string.replace(arg, '\\', '\\\\')
813 arg = string.replace(arg, '"', '\\"')
815 return '"%s"' % arg
818 def _simple_command(self, name, *args):
820 return self._command_complete(name, apply(self._command, (name,) + args))
823 def _untagged_response(self, typ, dat, name):
825 if typ == 'NO':
826 return typ, dat
827 if not self.untagged_responses.has_key(name):
828 return typ, [None]
829 data = self.untagged_responses[name]
830 if __debug__:
831 if self.debug >= 5:
832 _mesg('untagged_responses[%s] => %s' % (name, data))
833 del self.untagged_responses[name]
834 return typ, data
838 class _Authenticator:
840 """Private class to provide en/decoding
841 for base64-based authentication conversation.
844 def __init__(self, mechinst):
845 self.mech = mechinst # Callable object to provide/process data
847 def process(self, data):
848 ret = self.mech(self.decode(data))
849 if ret is None:
850 return '*' # Abort conversation
851 return self.encode(ret)
853 def encode(self, inp):
855 # Invoke binascii.b2a_base64 iteratively with
856 # short even length buffers, strip the trailing
857 # line feed from the result and append. "Even"
858 # means a number that factors to both 6 and 8,
859 # so when it gets to the end of the 8-bit input
860 # there's no partial 6-bit output.
862 oup = ''
863 while inp:
864 if len(inp) > 48:
865 t = inp[:48]
866 inp = inp[48:]
867 else:
868 t = inp
869 inp = ''
870 e = binascii.b2a_base64(t)
871 if e:
872 oup = oup + e[:-1]
873 return oup
875 def decode(self, inp):
876 if not inp:
877 return ''
878 return binascii.a2b_base64(inp)
882 Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
883 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
885 def Internaldate2tuple(resp):
887 """Convert IMAP4 INTERNALDATE to UT.
889 Returns Python time module tuple.
892 mo = InternalDate.match(resp)
893 if not mo:
894 return None
896 mon = Mon2num[mo.group('mon')]
897 zonen = mo.group('zonen')
899 for name in ('day', 'year', 'hour', 'min', 'sec', 'zoneh', 'zonem'):
900 exec "%s = string.atoi(mo.group('%s'))" % (name, name)
902 # INTERNALDATE timezone must be subtracted to get UT
904 zone = (zoneh*60 + zonem)*60
905 if zonen == '-':
906 zone = -zone
908 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
910 utc = time.mktime(tt)
912 # Following is necessary because the time module has no 'mkgmtime'.
913 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
915 lt = time.localtime(utc)
916 if time.daylight and lt[-1]:
917 zone = zone + time.altzone
918 else:
919 zone = zone + time.timezone
921 return time.localtime(utc - zone)
925 def Int2AP(num):
927 """Convert integer to A-P string representation."""
929 val = ''; AP = 'ABCDEFGHIJKLMNOP'
930 num = int(abs(num))
931 while num:
932 num, mod = divmod(num, 16)
933 val = AP[mod] + val
934 return val
938 def ParseFlags(resp):
940 """Convert IMAP4 flags response to python tuple."""
942 mo = Flags.match(resp)
943 if not mo:
944 return ()
946 return tuple(string.split(mo.group('flags')))
949 def Time2Internaldate(date_time):
951 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
953 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
956 dttype = type(date_time)
957 if dttype is type(1) or dttype is type(1.1):
958 tt = time.localtime(date_time)
959 elif dttype is type(()):
960 tt = date_time
961 elif dttype is type(""):
962 return date_time # Assume in correct format
963 else: raise ValueError
965 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
966 if dt[0] == '0':
967 dt = ' ' + dt[1:]
968 if time.daylight and tt[-1]:
969 zone = -time.altzone
970 else:
971 zone = -time.timezone
972 return '"' + dt + " %+02d%02d" % divmod(zone/60, 60) + '"'
976 if __debug__:
978 def _mesg(s, secs=None):
979 if secs is None:
980 secs = time.time()
981 tm = time.strftime('%M:%S', time.localtime(secs))
982 sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
983 sys.stderr.flush()
985 def _dump_ur(dict):
986 # Dump untagged responses (in `dict').
987 l = dict.items()
988 if not l: return
989 t = '\n\t\t'
990 j = string.join
991 l = map(lambda x,j=j:'%s: "%s"' % (x[0], x[1][0] and j(x[1], '" "') or ''), l)
992 _mesg('untagged responses dump:%s%s' % (t, j(l, t)))
994 _cmd_log = [] # Last `_cmd_log_len' interactions
995 _cmd_log_len = 10
997 def _log(line):
998 # Keep log of last `_cmd_log_len' interactions for debugging.
999 if len(_cmd_log) == _cmd_log_len:
1000 del _cmd_log[0]
1001 _cmd_log.append((time.time(), line))
1003 def print_log():
1004 _mesg('last %d IMAP4 interactions:' % len(_cmd_log))
1005 for secs,line in _cmd_log:
1006 _mesg(line, secs)
1010 if __name__ == '__main__':
1012 import getpass, sys
1014 host = ''
1015 if sys.argv[1:]: host = sys.argv[1]
1017 USER = getpass.getuser()
1018 PASSWD = getpass.getpass("IMAP password for %s: " % (host or "localhost"))
1020 test_seq1 = (
1021 ('login', (USER, PASSWD)),
1022 ('create', ('/tmp/xxx 1',)),
1023 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1024 ('CREATE', ('/tmp/yyz 2',)),
1025 ('append', ('/tmp/yyz 2', None, None, 'From: anon@x.y.z\n\ndata...')),
1026 ('list', ('/tmp', 'yy*')),
1027 ('select', ('/tmp/yyz 2',)),
1028 ('search', (None, '(TO zork)')),
1029 ('partial', ('1', 'RFC822', 1, 1024)),
1030 ('store', ('1', 'FLAGS', '(\Deleted)')),
1031 ('expunge', ()),
1032 ('recent', ()),
1033 ('close', ()),
1036 test_seq2 = (
1037 ('select', ()),
1038 ('response',('UIDVALIDITY',)),
1039 ('uid', ('SEARCH', 'ALL')),
1040 ('response', ('EXISTS',)),
1041 ('append', (None, None, None, 'From: anon@x.y.z\n\ndata...')),
1042 ('recent', ()),
1043 ('logout', ()),
1046 def run(cmd, args):
1047 _mesg('%s %s' % (cmd, args))
1048 typ, dat = apply(eval('M.%s' % cmd), args)
1049 _mesg('%s => %s %s' % (cmd, typ, dat))
1050 return dat
1052 Debug = 5
1053 M = IMAP4(host)
1054 _mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
1056 for cmd,args in test_seq1:
1057 run(cmd, args)
1059 for ml in run('list', ('/tmp/', 'yy%')):
1060 mo = re.match(r'.*"([^"]+)"$', ml)
1061 if mo: path = mo.group(1)
1062 else: path = string.split(ml)[-1]
1063 run('delete', (path,))
1065 for cmd,args in test_seq2:
1066 dat = run(cmd, args)
1068 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
1069 continue
1071 uid = string.split(dat[-1])
1072 if not uid: continue
1073 run('uid', ('FETCH', '%s' % uid[-1],
1074 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))