This commit was manufactured by cvs2svn to create tag 'r22a4-fork'.
[python/dscho.git] / Lib / imaplib.py
blobc82b455ee816ceb3e9b7dce9ffacf47e065f8164
1 """IMAP4 client.
3 Based on RFC 2060.
5 Public class: IMAP4
6 Public variable: Debug
7 Public functions: Internaldate2tuple
8 Int2AP
9 ParseFlags
10 Time2Internaldate
11 """
13 # Author: Piers Lauder <piers@cs.su.oz.au> December 1997.
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.
19 __version__ = "2.47"
21 import binascii, re, socket, time, random, sys
23 __all__ = ["IMAP4", "Internaldate2tuple",
24 "Int2AP", "ParseFlags", "Time2Internaldate"]
26 # Globals
28 CRLF = '\r\n'
29 Debug = 0
30 IMAP4_PORT = 143
31 AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first
33 # Commands
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'),
68 # Patterns to match server responses
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>.*))?')
84 class IMAP4:
86 """IMAP4 client class.
88 Instantiate with: IMAP4([host[, port]])
90 host - host's name (default: localhost);
91 port - port number (default: standard IMAP4 port).
93 All IMAP4rev1 commands are supported by methods of the same
94 name (in lower-case).
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)").
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.
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'.
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.
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.
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
131 mustquote = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]")
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
145 # Open socket to server.
147 self.open(host, port)
149 # Create unique tag for this session,
150 # and compile tagged response matcher.
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>.*)')
157 # Get server welcome message,
158 # request and store CAPABILITY response.
160 if __debug__:
161 if self.debug >= 1:
162 _mesg('imaplib version %s' % __version__)
163 _mesg('new IMAP4 connection, tag=%s' % self.tagpre)
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)
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())
179 if __debug__:
180 if self.debug >= 3:
181 _mesg('CAPABILITIES: %s' % `self.capabilities`)
183 for version in AllowedVersions:
184 if not version in self.capabilities:
185 continue
186 self.PROTOCOL_VERSION = version
187 return
189 raise self.error('server not IMAP4 compliant')
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)
200 # Overridable methods
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.
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('r')
213 def read(self, size):
214 """Read 'size' bytes from remote."""
215 return self.file.read(size)
218 def readline(self):
219 """Read line from remote."""
220 return self.file.readline()
223 def send(self, data):
224 """Send data to remote."""
225 self.sock.send(data)
228 def shutdown(self):
229 """Close I/O established in "open"."""
230 self.file.close()
231 self.sock.close()
234 def socket(self):
235 """Return socket instance used to connect to IMAP4 server.
237 socket = <instance>.socket()
239 return self.sock
243 # Utility methods
246 def recent(self):
247 """Return most recent 'RECENT' responses if any exist,
248 else prompt server for an update using the 'NOOP' command.
250 (typ, [data]) = <instance>.recent()
252 'data' is None if no new messages,
253 else list of RECENT responses, most recent last.
255 name = 'RECENT'
256 typ, dat = self._untagged_response('OK', [None], name)
257 if dat[-1]:
258 return typ, dat
259 typ, dat = self.noop() # Prod server for response
260 return self._untagged_response(typ, dat, name)
263 def response(self, code):
264 """Return data for response 'code' if received, or None.
266 Old value for response 'code' is cleared.
268 (code, [data]) = <instance>.response(code)
270 return self._untagged_response(code, [None], code.upper())
274 # IMAP4 commands
277 def append(self, mailbox, flags, date_time, message):
278 """Append message to named mailbox.
280 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
282 All args except `message' can be None.
284 name = 'APPEND'
285 if not mailbox:
286 mailbox = 'INBOX'
287 if flags:
288 if (flags[0],flags[-1]) != ('(',')'):
289 flags = '(%s)' % flags
290 else:
291 flags = None
292 if date_time:
293 date_time = Time2Internaldate(date_time)
294 else:
295 date_time = None
296 self.literal = message
297 return self._simple_command(name, mailbox, flags, date_time)
300 def authenticate(self, mechanism, authobject):
301 """Authenticate command - requires response processing.
303 'mechanism' specifies which authentication mechanism is to
304 be used - it must appear in <instance>.capabilities in the
305 form AUTH=<mechanism>.
307 'authobject' must be a callable object:
309 data = authobject(response)
311 It will be called to process server continuation responses.
312 It should return data that will be encoded and sent to server.
313 It should return None if the client abort response '*' should
314 be sent instead.
316 mech = mechanism.upper()
317 cap = 'AUTH=%s' % mech
318 if not cap in self.capabilities:
319 raise self.error("Server doesn't allow %s authentication." % mech)
320 self.literal = _Authenticator(authobject).process
321 typ, dat = self._simple_command('AUTHENTICATE', mech)
322 if typ != 'OK':
323 raise self.error(dat[-1])
324 self.state = 'AUTH'
325 return typ, dat
328 def check(self):
329 """Checkpoint mailbox on server.
331 (typ, [data]) = <instance>.check()
333 return self._simple_command('CHECK')
336 def close(self):
337 """Close currently selected mailbox.
339 Deleted messages are removed from writable mailbox.
340 This is the recommended command before 'LOGOUT'.
342 (typ, [data]) = <instance>.close()
344 try:
345 typ, dat = self._simple_command('CLOSE')
346 finally:
347 self.state = 'AUTH'
348 return typ, dat
351 def copy(self, message_set, new_mailbox):
352 """Copy 'message_set' messages onto end of 'new_mailbox'.
354 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
356 return self._simple_command('COPY', message_set, new_mailbox)
359 def create(self, mailbox):
360 """Create new mailbox.
362 (typ, [data]) = <instance>.create(mailbox)
364 return self._simple_command('CREATE', mailbox)
367 def delete(self, mailbox):
368 """Delete old mailbox.
370 (typ, [data]) = <instance>.delete(mailbox)
372 return self._simple_command('DELETE', mailbox)
375 def expunge(self):
376 """Permanently remove deleted items from selected mailbox.
378 Generates 'EXPUNGE' response for each deleted message.
380 (typ, [data]) = <instance>.expunge()
382 'data' is list of 'EXPUNGE'd message numbers in order received.
384 name = 'EXPUNGE'
385 typ, dat = self._simple_command(name)
386 return self._untagged_response(typ, dat, name)
389 def fetch(self, message_set, message_parts):
390 """Fetch (parts of) messages.
392 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
394 'message_parts' should be a string of selected parts
395 enclosed in parentheses, eg: "(UID BODY[TEXT])".
397 'data' are tuples of message part envelope and data.
399 name = 'FETCH'
400 typ, dat = self._simple_command(name, message_set, message_parts)
401 return self._untagged_response(typ, dat, name)
404 def getacl(self, mailbox):
405 """Get the ACLs for a mailbox.
407 (typ, [data]) = <instance>.getacl(mailbox)
409 typ, dat = self._simple_command('GETACL', mailbox)
410 return self._untagged_response(typ, dat, 'ACL')
413 def list(self, directory='""', pattern='*'):
414 """List mailbox names in directory matching pattern.
416 (typ, [data]) = <instance>.list(directory='""', pattern='*')
418 'data' is list of LIST responses.
420 name = 'LIST'
421 typ, dat = self._simple_command(name, directory, pattern)
422 return self._untagged_response(typ, dat, name)
425 def login(self, user, password):
426 """Identify client using plaintext password.
428 (typ, [data]) = <instance>.login(user, password)
430 NB: 'password' will be quoted.
432 #if not 'AUTH=LOGIN' in self.capabilities:
433 # raise self.error("Server doesn't allow LOGIN authentication." % mech)
434 typ, dat = self._simple_command('LOGIN', user, self._quote(password))
435 if typ != 'OK':
436 raise self.error(dat[-1])
437 self.state = 'AUTH'
438 return typ, dat
441 def logout(self):
442 """Shutdown connection to server.
444 (typ, [data]) = <instance>.logout()
446 Returns server 'BYE' response.
448 self.state = 'LOGOUT'
449 try: typ, dat = self._simple_command('LOGOUT')
450 except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
451 self.shutdown()
452 if self.untagged_responses.has_key('BYE'):
453 return 'BYE', self.untagged_responses['BYE']
454 return typ, dat
457 def lsub(self, directory='""', pattern='*'):
458 """List 'subscribed' mailbox names in directory matching pattern.
460 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
462 'data' are tuples of message part envelope and data.
464 name = 'LSUB'
465 typ, dat = self._simple_command(name, directory, pattern)
466 return self._untagged_response(typ, dat, name)
469 def namespace(self):
470 """ Returns IMAP namespaces ala rfc2342
472 (typ, [data, ...]) = <instance>.namespace()
474 name = 'NAMESPACE'
475 typ, dat = self._simple_command(name)
476 return self._untagged_response(typ, dat, name)
479 def noop(self):
480 """Send NOOP command.
482 (typ, data) = <instance>.noop()
484 if __debug__:
485 if self.debug >= 3:
486 _dump_ur(self.untagged_responses)
487 return self._simple_command('NOOP')
490 def partial(self, message_num, message_part, start, length):
491 """Fetch truncated part of a message.
493 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
495 'data' is tuple of message part envelope and data.
497 name = 'PARTIAL'
498 typ, dat = self._simple_command(name, message_num, message_part, start, length)
499 return self._untagged_response(typ, dat, 'FETCH')
502 def rename(self, oldmailbox, newmailbox):
503 """Rename old mailbox name to new.
505 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
507 return self._simple_command('RENAME', oldmailbox, newmailbox)
510 def search(self, charset, *criteria):
511 """Search mailbox for matching messages.
513 (typ, [data]) = <instance>.search(charset, criterium, ...)
515 'data' is space separated list of matching message numbers.
517 name = 'SEARCH'
518 if charset:
519 typ, dat = apply(self._simple_command, (name, 'CHARSET', charset) + criteria)
520 else:
521 typ, dat = apply(self._simple_command, (name,) + criteria)
522 return self._untagged_response(typ, dat, name)
525 def select(self, mailbox='INBOX', readonly=None):
526 """Select a mailbox.
528 Flush all untagged responses.
530 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
532 'data' is count of messages in mailbox ('EXISTS' response).
534 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
535 self.untagged_responses = {} # Flush old responses.
536 self.is_readonly = readonly
537 if readonly:
538 name = 'EXAMINE'
539 else:
540 name = 'SELECT'
541 typ, dat = self._simple_command(name, mailbox)
542 if typ != 'OK':
543 self.state = 'AUTH' # Might have been 'SELECTED'
544 return typ, dat
545 self.state = 'SELECTED'
546 if self.untagged_responses.has_key('READ-ONLY') \
547 and not readonly:
548 if __debug__:
549 if self.debug >= 1:
550 _dump_ur(self.untagged_responses)
551 raise self.readonly('%s is not writable' % mailbox)
552 return typ, self.untagged_responses.get('EXISTS', [None])
555 def setacl(self, mailbox, who, what):
556 """Set a mailbox acl.
558 (typ, [data]) = <instance>.create(mailbox, who, what)
560 return self._simple_command('SETACL', mailbox, who, what)
563 def sort(self, sort_criteria, charset, *search_criteria):
564 """IMAP4rev1 extension SORT command.
566 (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
568 name = 'SORT'
569 #if not name in self.capabilities: # Let the server decide!
570 # raise self.error('unimplemented extension command: %s' % name)
571 if (sort_criteria[0],sort_criteria[-1]) != ('(',')'):
572 sort_criteria = '(%s)' % sort_criteria
573 typ, dat = apply(self._simple_command, (name, sort_criteria, charset) + search_criteria)
574 return self._untagged_response(typ, dat, name)
577 def status(self, mailbox, names):
578 """Request named status conditions for mailbox.
580 (typ, [data]) = <instance>.status(mailbox, names)
582 name = 'STATUS'
583 #if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide!
584 # raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
585 typ, dat = self._simple_command(name, mailbox, names)
586 return self._untagged_response(typ, dat, name)
589 def store(self, message_set, command, flags):
590 """Alters flag dispositions for messages in mailbox.
592 (typ, [data]) = <instance>.store(message_set, command, flags)
594 if (flags[0],flags[-1]) != ('(',')'):
595 flags = '(%s)' % flags # Avoid quoting the flags
596 typ, dat = self._simple_command('STORE', message_set, command, flags)
597 return self._untagged_response(typ, dat, 'FETCH')
600 def subscribe(self, mailbox):
601 """Subscribe to new mailbox.
603 (typ, [data]) = <instance>.subscribe(mailbox)
605 return self._simple_command('SUBSCRIBE', mailbox)
608 def uid(self, command, *args):
609 """Execute "command arg ..." with messages identified by UID,
610 rather than message number.
612 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
614 Returns response appropriate to 'command'.
616 command = command.upper()
617 if not Commands.has_key(command):
618 raise self.error("Unknown IMAP4 UID command: %s" % command)
619 if self.state not in Commands[command]:
620 raise self.error('command %s illegal in state %s'
621 % (command, self.state))
622 name = 'UID'
623 typ, dat = apply(self._simple_command, (name, command) + args)
624 if command in ('SEARCH', 'SORT'):
625 name = command
626 else:
627 name = 'FETCH'
628 return self._untagged_response(typ, dat, name)
631 def unsubscribe(self, mailbox):
632 """Unsubscribe from old mailbox.
634 (typ, [data]) = <instance>.unsubscribe(mailbox)
636 return self._simple_command('UNSUBSCRIBE', mailbox)
639 def xatom(self, name, *args):
640 """Allow simple extension commands
641 notified by server in CAPABILITY response.
643 Assumes command is legal in current state.
645 (typ, [data]) = <instance>.xatom(name, arg, ...)
647 Returns response appropriate to extension command `name'.
649 name = name.upper()
650 #if not name in self.capabilities: # Let the server decide!
651 # raise self.error('unknown extension command: %s' % name)
652 if not Commands.has_key(name):
653 Commands[name] = (self.state,)
654 return apply(self._simple_command, (name,) + args)
658 # Private methods
661 def _append_untagged(self, typ, dat):
663 if dat is None: dat = ''
664 ur = self.untagged_responses
665 if __debug__:
666 if self.debug >= 5:
667 _mesg('untagged_responses[%s] %s += ["%s"]' %
668 (typ, len(ur.get(typ,'')), dat))
669 if ur.has_key(typ):
670 ur[typ].append(dat)
671 else:
672 ur[typ] = [dat]
675 def _check_bye(self):
676 bye = self.untagged_responses.get('BYE')
677 if bye:
678 raise self.abort(bye[-1])
681 def _command(self, name, *args):
683 if self.state not in Commands[name]:
684 self.literal = None
685 raise self.error(
686 'command %s illegal in state %s' % (name, self.state))
688 for typ in ('OK', 'NO', 'BAD'):
689 if self.untagged_responses.has_key(typ):
690 del self.untagged_responses[typ]
692 if self.untagged_responses.has_key('READ-ONLY') \
693 and not self.is_readonly:
694 raise self.readonly('mailbox status changed to READ-ONLY')
696 tag = self._new_tag()
697 data = '%s %s' % (tag, name)
698 for arg in args:
699 if arg is None: continue
700 data = '%s %s' % (data, self._checkquote(arg))
702 literal = self.literal
703 if literal is not None:
704 self.literal = None
705 if type(literal) is type(self._command):
706 literator = literal
707 else:
708 literator = None
709 data = '%s {%s}' % (data, len(literal))
711 if __debug__:
712 if self.debug >= 4:
713 _mesg('> %s' % data)
714 else:
715 _log('> %s' % data)
717 try:
718 self.send('%s%s' % (data, CRLF))
719 except (socket.error, OSError), val:
720 raise self.abort('socket error: %s' % val)
722 if literal is None:
723 return tag
725 while 1:
726 # Wait for continuation response
728 while self._get_response():
729 if self.tagged_commands[tag]: # BAD/NO?
730 return tag
732 # Send literal
734 if literator:
735 literal = literator(self.continuation_response)
737 if __debug__:
738 if self.debug >= 4:
739 _mesg('write literal size %s' % len(literal))
741 try:
742 self.send(literal)
743 self.send(CRLF)
744 except (socket.error, OSError), val:
745 raise self.abort('socket error: %s' % val)
747 if not literator:
748 break
750 return tag
753 def _command_complete(self, name, tag):
754 self._check_bye()
755 try:
756 typ, data = self._get_tagged_response(tag)
757 except self.abort, val:
758 raise self.abort('command: %s => %s' % (name, val))
759 except self.error, val:
760 raise self.error('command: %s => %s' % (name, val))
761 self._check_bye()
762 if typ == 'BAD':
763 raise self.error('%s command error: %s %s' % (name, typ, data))
764 return typ, data
767 def _get_response(self):
769 # Read response and store.
771 # Returns None for continuation responses,
772 # otherwise first response line received.
774 resp = self._get_line()
776 # Command completion response?
778 if self._match(self.tagre, resp):
779 tag = self.mo.group('tag')
780 if not self.tagged_commands.has_key(tag):
781 raise self.abort('unexpected tagged response: %s' % resp)
783 typ = self.mo.group('type')
784 dat = self.mo.group('data')
785 self.tagged_commands[tag] = (typ, [dat])
786 else:
787 dat2 = None
789 # '*' (untagged) responses?
791 if not self._match(Untagged_response, resp):
792 if self._match(Untagged_status, resp):
793 dat2 = self.mo.group('data2')
795 if self.mo is None:
796 # Only other possibility is '+' (continuation) response...
798 if self._match(Continuation, resp):
799 self.continuation_response = self.mo.group('data')
800 return None # NB: indicates continuation
802 raise self.abort("unexpected response: '%s'" % resp)
804 typ = self.mo.group('type')
805 dat = self.mo.group('data')
806 if dat is None: dat = '' # Null untagged response
807 if dat2: dat = dat + ' ' + dat2
809 # Is there a literal to come?
811 while self._match(Literal, dat):
813 # Read literal direct from connection.
815 size = int(self.mo.group('size'))
816 if __debug__:
817 if self.debug >= 4:
818 _mesg('read literal size %s' % size)
819 data = self.read(size)
821 # Store response with literal as tuple
823 self._append_untagged(typ, (dat, data))
825 # Read trailer - possibly containing another literal
827 dat = self._get_line()
829 self._append_untagged(typ, dat)
831 # Bracketed response information?
833 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
834 self._append_untagged(self.mo.group('type'), self.mo.group('data'))
836 if __debug__:
837 if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
838 _mesg('%s response: %s' % (typ, dat))
840 return resp
843 def _get_tagged_response(self, tag):
845 while 1:
846 result = self.tagged_commands[tag]
847 if result is not None:
848 del self.tagged_commands[tag]
849 return result
851 # Some have reported "unexpected response" exceptions.
852 # Note that ignoring them here causes loops.
853 # Instead, send me details of the unexpected response and
854 # I'll update the code in `_get_response()'.
856 try:
857 self._get_response()
858 except self.abort, val:
859 if __debug__:
860 if self.debug >= 1:
861 print_log()
862 raise
865 def _get_line(self):
867 line = self.readline()
868 if not line:
869 raise self.abort('socket error: EOF')
871 # Protocol mandates all lines terminated by CRLF
873 line = line[:-2]
874 if __debug__:
875 if self.debug >= 4:
876 _mesg('< %s' % line)
877 else:
878 _log('< %s' % line)
879 return line
882 def _match(self, cre, s):
884 # Run compiled regular expression match method on 's'.
885 # Save result, return success.
887 self.mo = cre.match(s)
888 if __debug__:
889 if self.mo is not None and self.debug >= 5:
890 _mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`))
891 return self.mo is not None
894 def _new_tag(self):
896 tag = '%s%s' % (self.tagpre, self.tagnum)
897 self.tagnum = self.tagnum + 1
898 self.tagged_commands[tag] = None
899 return tag
902 def _checkquote(self, arg):
904 # Must quote command args if non-alphanumeric chars present,
905 # and not already quoted.
907 if type(arg) is not type(''):
908 return arg
909 if (arg[0],arg[-1]) in (('(',')'),('"','"')):
910 return arg
911 if self.mustquote.search(arg) is None:
912 return arg
913 return self._quote(arg)
916 def _quote(self, arg):
918 arg = arg.replace('\\', '\\\\')
919 arg = arg.replace('"', '\\"')
921 return '"%s"' % arg
924 def _simple_command(self, name, *args):
926 return self._command_complete(name, apply(self._command, (name,) + args))
929 def _untagged_response(self, typ, dat, name):
931 if typ == 'NO':
932 return typ, dat
933 if not self.untagged_responses.has_key(name):
934 return typ, [None]
935 data = self.untagged_responses[name]
936 if __debug__:
937 if self.debug >= 5:
938 _mesg('untagged_responses[%s] => %s' % (name, data))
939 del self.untagged_responses[name]
940 return typ, data
944 class _Authenticator:
946 """Private class to provide en/decoding
947 for base64-based authentication conversation.
950 def __init__(self, mechinst):
951 self.mech = mechinst # Callable object to provide/process data
953 def process(self, data):
954 ret = self.mech(self.decode(data))
955 if ret is None:
956 return '*' # Abort conversation
957 return self.encode(ret)
959 def encode(self, inp):
961 # Invoke binascii.b2a_base64 iteratively with
962 # short even length buffers, strip the trailing
963 # line feed from the result and append. "Even"
964 # means a number that factors to both 6 and 8,
965 # so when it gets to the end of the 8-bit input
966 # there's no partial 6-bit output.
968 oup = ''
969 while inp:
970 if len(inp) > 48:
971 t = inp[:48]
972 inp = inp[48:]
973 else:
974 t = inp
975 inp = ''
976 e = binascii.b2a_base64(t)
977 if e:
978 oup = oup + e[:-1]
979 return oup
981 def decode(self, inp):
982 if not inp:
983 return ''
984 return binascii.a2b_base64(inp)
988 Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
989 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
991 def Internaldate2tuple(resp):
992 """Convert IMAP4 INTERNALDATE to UT.
994 Returns Python time module tuple.
997 mo = InternalDate.match(resp)
998 if not mo:
999 return None
1001 mon = Mon2num[mo.group('mon')]
1002 zonen = mo.group('zonen')
1004 day = int(mo.group('day'))
1005 year = int(mo.group('year'))
1006 hour = int(mo.group('hour'))
1007 min = int(mo.group('min'))
1008 sec = int(mo.group('sec'))
1009 zoneh = int(mo.group('zoneh'))
1010 zonem = int(mo.group('zonem'))
1012 # INTERNALDATE timezone must be subtracted to get UT
1014 zone = (zoneh*60 + zonem)*60
1015 if zonen == '-':
1016 zone = -zone
1018 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
1020 utc = time.mktime(tt)
1022 # Following is necessary because the time module has no 'mkgmtime'.
1023 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
1025 lt = time.localtime(utc)
1026 if time.daylight and lt[-1]:
1027 zone = zone + time.altzone
1028 else:
1029 zone = zone + time.timezone
1031 return time.localtime(utc - zone)
1035 def Int2AP(num):
1037 """Convert integer to A-P string representation."""
1039 val = ''; AP = 'ABCDEFGHIJKLMNOP'
1040 num = int(abs(num))
1041 while num:
1042 num, mod = divmod(num, 16)
1043 val = AP[mod] + val
1044 return val
1048 def ParseFlags(resp):
1050 """Convert IMAP4 flags response to python tuple."""
1052 mo = Flags.match(resp)
1053 if not mo:
1054 return ()
1056 return tuple(mo.group('flags').split())
1059 def Time2Internaldate(date_time):
1061 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
1063 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
1066 dttype = type(date_time)
1067 if dttype is type(1) or dttype is type(1.1):
1068 tt = time.localtime(date_time)
1069 elif dttype is type(()):
1070 tt = date_time
1071 elif dttype is type(""):
1072 return date_time # Assume in correct format
1073 else: raise ValueError
1075 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
1076 if dt[0] == '0':
1077 dt = ' ' + dt[1:]
1078 if time.daylight and tt[-1]:
1079 zone = -time.altzone
1080 else:
1081 zone = -time.timezone
1082 return '"' + dt + " %+02d%02d" % divmod(zone/60, 60) + '"'
1086 if __debug__:
1088 def _mesg(s, secs=None):
1089 if secs is None:
1090 secs = time.time()
1091 tm = time.strftime('%M:%S', time.localtime(secs))
1092 sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
1093 sys.stderr.flush()
1095 def _dump_ur(dict):
1096 # Dump untagged responses (in `dict').
1097 l = dict.items()
1098 if not l: return
1099 t = '\n\t\t'
1100 l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l)
1101 _mesg('untagged responses dump:%s%s' % (t, t.join(l)))
1103 _cmd_log = [] # Last `_cmd_log_len' interactions
1104 _cmd_log_len = 10
1106 def _log(line):
1107 # Keep log of last `_cmd_log_len' interactions for debugging.
1108 if len(_cmd_log) == _cmd_log_len:
1109 del _cmd_log[0]
1110 _cmd_log.append((time.time(), line))
1112 def print_log():
1113 _mesg('last %d IMAP4 interactions:' % len(_cmd_log))
1114 for secs,line in _cmd_log:
1115 _mesg(line, secs)
1119 if __name__ == '__main__':
1121 import getopt, getpass
1123 try:
1124 optlist, args = getopt.getopt(sys.argv[1:], 'd:')
1125 except getopt.error, val:
1126 pass
1128 for opt,val in optlist:
1129 if opt == '-d':
1130 Debug = int(val)
1132 if not args: args = ('',)
1134 host = args[0]
1136 USER = getpass.getuser()
1137 PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost"))
1139 test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER, 'lf':CRLF}
1140 test_seq1 = (
1141 ('login', (USER, PASSWD)),
1142 ('create', ('/tmp/xxx 1',)),
1143 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1144 ('CREATE', ('/tmp/yyz 2',)),
1145 ('append', ('/tmp/yyz 2', None, None, test_mesg)),
1146 ('list', ('/tmp', 'yy*')),
1147 ('select', ('/tmp/yyz 2',)),
1148 ('search', (None, 'SUBJECT', 'test')),
1149 ('partial', ('1', 'RFC822', 1, 1024)),
1150 ('store', ('1', 'FLAGS', '(\Deleted)')),
1151 ('namespace', ()),
1152 ('expunge', ()),
1153 ('recent', ()),
1154 ('close', ()),
1157 test_seq2 = (
1158 ('select', ()),
1159 ('response',('UIDVALIDITY',)),
1160 ('uid', ('SEARCH', 'ALL')),
1161 ('response', ('EXISTS',)),
1162 ('append', (None, None, None, test_mesg)),
1163 ('recent', ()),
1164 ('logout', ()),
1167 def run(cmd, args):
1168 _mesg('%s %s' % (cmd, args))
1169 typ, dat = apply(getattr(M, cmd), args)
1170 _mesg('%s => %s %s' % (cmd, typ, dat))
1171 return dat
1173 try:
1174 M = IMAP4(host)
1175 _mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
1176 _mesg('CAPABILITIES = %s' % `M.capabilities`)
1178 for cmd,args in test_seq1:
1179 run(cmd, args)
1181 for ml in run('list', ('/tmp/', 'yy%')):
1182 mo = re.match(r'.*"([^"]+)"$', ml)
1183 if mo: path = mo.group(1)
1184 else: path = ml.split()[-1]
1185 run('delete', (path,))
1187 for cmd,args in test_seq2:
1188 dat = run(cmd, args)
1190 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
1191 continue
1193 uid = dat[-1].split()
1194 if not uid: continue
1195 run('uid', ('FETCH', '%s' % uid[-1],
1196 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1198 print '\nAll tests OK.'
1200 except:
1201 print '\nTests failed.'
1203 if not Debug:
1204 print '''
1205 If you would like to see debugging output,
1206 try: %s -d5
1207 ''' % sys.argv[0]
1209 raise