Fix sf bug 666219: assertion error in httplib.
[python/dscho.git] / Lib / imaplib.py
blobd9166e08530317b6b4cecf013c5e94f9e52f697e
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.
18 # IMAP4_SSL contributed by Tino Lange <Tino.Lange@isg.de> March 2002.
19 # GET/SETQUOTA contributed by Andreas Zeidler <az@kreativkombinat.de> June 2002.
20 # PROXYAUTH contributed by Rick Holbert <holbert.13@osu.edu> November 2002.
22 __version__ = "2.54"
24 import binascii, os, random, re, socket, sys, time
26 __all__ = ["IMAP4", "IMAP4_SSL", "Internaldate2tuple",
27 "Int2AP", "ParseFlags", "Time2Internaldate"]
29 # Globals
31 CRLF = '\r\n'
32 Debug = 0
33 IMAP4_PORT = 143
34 IMAP4_SSL_PORT = 993
35 AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first
37 # Commands
39 Commands = {
40 # name valid states
41 'APPEND': ('AUTH', 'SELECTED'),
42 'AUTHENTICATE': ('NONAUTH',),
43 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
44 'CHECK': ('SELECTED',),
45 'CLOSE': ('SELECTED',),
46 'COPY': ('SELECTED',),
47 'CREATE': ('AUTH', 'SELECTED'),
48 'DELETE': ('AUTH', 'SELECTED'),
49 'EXAMINE': ('AUTH', 'SELECTED'),
50 'EXPUNGE': ('SELECTED',),
51 'FETCH': ('SELECTED',),
52 'GETACL': ('AUTH', 'SELECTED'),
53 'GETQUOTA': ('AUTH', 'SELECTED'),
54 'GETQUOTAROOT': ('AUTH', 'SELECTED'),
55 'LIST': ('AUTH', 'SELECTED'),
56 'LOGIN': ('NONAUTH',),
57 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
58 'LSUB': ('AUTH', 'SELECTED'),
59 'NAMESPACE': ('AUTH', 'SELECTED'),
60 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
61 'PARTIAL': ('SELECTED',), # NB: obsolete
62 'PROXYAUTH': ('AUTH',),
63 'RENAME': ('AUTH', 'SELECTED'),
64 'SEARCH': ('SELECTED',),
65 'SELECT': ('AUTH', 'SELECTED'),
66 'SETACL': ('AUTH', 'SELECTED'),
67 'SETQUOTA': ('AUTH', 'SELECTED'),
68 'SORT': ('SELECTED',),
69 'STATUS': ('AUTH', 'SELECTED'),
70 'STORE': ('SELECTED',),
71 'SUBSCRIBE': ('AUTH', 'SELECTED'),
72 'UID': ('SELECTED',),
73 'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
76 # Patterns to match server responses
78 Continuation = re.compile(r'\+( (?P<data>.*))?')
79 Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
80 InternalDate = re.compile(r'.*INTERNALDATE "'
81 r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
82 r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
83 r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
84 r'"')
85 Literal = re.compile(r'.*{(?P<size>\d+)}$')
86 MapCRLF = re.compile(r'\r\n|\r|\n')
87 Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
88 Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
89 Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
93 class IMAP4:
95 """IMAP4 client class.
97 Instantiate with: IMAP4([host[, port]])
99 host - host's name (default: localhost);
100 port - port number (default: standard IMAP4 port).
102 All IMAP4rev1 commands are supported by methods of the same
103 name (in lower-case).
105 All arguments to commands are converted to strings, except for
106 AUTHENTICATE, and the last argument to APPEND which is passed as
107 an IMAP4 literal. If necessary (the string contains any
108 non-printing characters or white-space and isn't enclosed with
109 either parentheses or double quotes) each string is quoted.
110 However, the 'password' argument to the LOGIN command is always
111 quoted. If you want to avoid having an argument string quoted
112 (eg: the 'flags' argument to STORE) then enclose the string in
113 parentheses (eg: "(\Deleted)").
115 Each command returns a tuple: (type, [data, ...]) where 'type'
116 is usually 'OK' or 'NO', and 'data' is either the text from the
117 tagged response, or untagged results from command. Each 'data'
118 is either a string, or a tuple. If a tuple, then the first part
119 is the header of the response, and the second part contains
120 the data (ie: 'literal' value).
122 Errors raise the exception class <instance>.error("<reason>").
123 IMAP4 server errors raise <instance>.abort("<reason>"),
124 which is a sub-class of 'error'. Mailbox status changes
125 from READ-WRITE to READ-ONLY raise the exception class
126 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
128 "error" exceptions imply a program error.
129 "abort" exceptions imply the connection should be reset, and
130 the command re-tried.
131 "readonly" exceptions imply the command should be re-tried.
133 Note: to use this module, you must read the RFCs pertaining
134 to the IMAP4 protocol, as the semantics of the arguments to
135 each IMAP4 command are left to the invoker, not to mention
136 the results.
139 class error(Exception): pass # Logical errors - debug required
140 class abort(error): pass # Service errors - close and retry
141 class readonly(abort): pass # Mailbox status changed to READ-ONLY
143 mustquote = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]")
145 def __init__(self, host = '', port = IMAP4_PORT):
146 self.debug = Debug
147 self.state = 'LOGOUT'
148 self.literal = None # A literal argument to a command
149 self.tagged_commands = {} # Tagged commands awaiting response
150 self.untagged_responses = {} # {typ: [data, ...], ...}
151 self.continuation_response = '' # Last continuation response
152 self.is_readonly = None # READ-ONLY desired state
153 self.tagnum = 0
155 # Open socket to server.
157 self.open(host, port)
159 # Create unique tag for this session,
160 # and compile tagged response matcher.
162 self.tagpre = Int2AP(random.randint(0, 31999))
163 self.tagre = re.compile(r'(?P<tag>'
164 + self.tagpre
165 + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
167 # Get server welcome message,
168 # request and store CAPABILITY response.
170 if __debug__:
171 self._cmd_log_len = 10
172 self._cmd_log_idx = 0
173 self._cmd_log = {} # Last `_cmd_log_len' interactions
174 if self.debug >= 1:
175 self._mesg('imaplib version %s' % __version__)
176 self._mesg('new IMAP4 connection, tag=%s' % self.tagpre)
178 self.welcome = self._get_response()
179 if 'PREAUTH' in self.untagged_responses:
180 self.state = 'AUTH'
181 elif 'OK' in self.untagged_responses:
182 self.state = 'NONAUTH'
183 else:
184 raise self.error(self.welcome)
186 cap = 'CAPABILITY'
187 self._simple_command(cap)
188 if not cap in self.untagged_responses:
189 raise self.error('no CAPABILITY response from server')
190 self.capabilities = tuple(self.untagged_responses[cap][-1].upper().split())
192 if __debug__:
193 if self.debug >= 3:
194 self._mesg('CAPABILITIES: %s' % `self.capabilities`)
196 for version in AllowedVersions:
197 if not version in self.capabilities:
198 continue
199 self.PROTOCOL_VERSION = version
200 return
202 raise self.error('server not IMAP4 compliant')
205 def __getattr__(self, attr):
206 # Allow UPPERCASE variants of IMAP4 command methods.
207 if attr in Commands:
208 return getattr(self, attr.lower())
209 raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
213 # Overridable methods
216 def open(self, host = '', port = IMAP4_PORT):
217 """Setup connection to remote server on "host:port"
218 (default: localhost:standard IMAP4 port).
219 This connection will be used by the routines:
220 read, readline, send, shutdown.
222 self.host = host
223 self.port = port
224 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
225 self.sock.connect((host, port))
226 self.file = self.sock.makefile('rb')
229 def read(self, size):
230 """Read 'size' bytes from remote."""
231 return self.file.read(size)
234 def readline(self):
235 """Read line from remote."""
236 return self.file.readline()
239 def send(self, data):
240 """Send data to remote."""
241 self.sock.sendall(data)
244 def shutdown(self):
245 """Close I/O established in "open"."""
246 self.file.close()
247 self.sock.close()
250 def socket(self):
251 """Return socket instance used to connect to IMAP4 server.
253 socket = <instance>.socket()
255 return self.sock
259 # Utility methods
262 def recent(self):
263 """Return most recent 'RECENT' responses if any exist,
264 else prompt server for an update using the 'NOOP' command.
266 (typ, [data]) = <instance>.recent()
268 'data' is None if no new messages,
269 else list of RECENT responses, most recent last.
271 name = 'RECENT'
272 typ, dat = self._untagged_response('OK', [None], name)
273 if dat[-1]:
274 return typ, dat
275 typ, dat = self.noop() # Prod server for response
276 return self._untagged_response(typ, dat, name)
279 def response(self, code):
280 """Return data for response 'code' if received, or None.
282 Old value for response 'code' is cleared.
284 (code, [data]) = <instance>.response(code)
286 return self._untagged_response(code, [None], code.upper())
290 # IMAP4 commands
293 def append(self, mailbox, flags, date_time, message):
294 """Append message to named mailbox.
296 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
298 All args except `message' can be None.
300 name = 'APPEND'
301 if not mailbox:
302 mailbox = 'INBOX'
303 if flags:
304 if (flags[0],flags[-1]) != ('(',')'):
305 flags = '(%s)' % flags
306 else:
307 flags = None
308 if date_time:
309 date_time = Time2Internaldate(date_time)
310 else:
311 date_time = None
312 self.literal = MapCRLF.sub(CRLF, message)
313 return self._simple_command(name, mailbox, flags, date_time)
316 def authenticate(self, mechanism, authobject):
317 """Authenticate command - requires response processing.
319 'mechanism' specifies which authentication mechanism is to
320 be used - it must appear in <instance>.capabilities in the
321 form AUTH=<mechanism>.
323 'authobject' must be a callable object:
325 data = authobject(response)
327 It will be called to process server continuation responses.
328 It should return data that will be encoded and sent to server.
329 It should return None if the client abort response '*' should
330 be sent instead.
332 mech = mechanism.upper()
333 # XXX: shouldn't this code be removed, not commented out?
334 #cap = 'AUTH=%s' % mech
335 #if not cap in self.capabilities: # Let the server decide!
336 # raise self.error("Server doesn't allow %s authentication." % mech)
337 self.literal = _Authenticator(authobject).process
338 typ, dat = self._simple_command('AUTHENTICATE', mech)
339 if typ != 'OK':
340 raise self.error(dat[-1])
341 self.state = 'AUTH'
342 return typ, dat
345 def check(self):
346 """Checkpoint mailbox on server.
348 (typ, [data]) = <instance>.check()
350 return self._simple_command('CHECK')
353 def close(self):
354 """Close currently selected mailbox.
356 Deleted messages are removed from writable mailbox.
357 This is the recommended command before 'LOGOUT'.
359 (typ, [data]) = <instance>.close()
361 try:
362 typ, dat = self._simple_command('CLOSE')
363 finally:
364 self.state = 'AUTH'
365 return typ, dat
368 def copy(self, message_set, new_mailbox):
369 """Copy 'message_set' messages onto end of 'new_mailbox'.
371 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
373 return self._simple_command('COPY', message_set, new_mailbox)
376 def create(self, mailbox):
377 """Create new mailbox.
379 (typ, [data]) = <instance>.create(mailbox)
381 return self._simple_command('CREATE', mailbox)
384 def delete(self, mailbox):
385 """Delete old mailbox.
387 (typ, [data]) = <instance>.delete(mailbox)
389 return self._simple_command('DELETE', mailbox)
392 def expunge(self):
393 """Permanently remove deleted items from selected mailbox.
395 Generates 'EXPUNGE' response for each deleted message.
397 (typ, [data]) = <instance>.expunge()
399 'data' is list of 'EXPUNGE'd message numbers in order received.
401 name = 'EXPUNGE'
402 typ, dat = self._simple_command(name)
403 return self._untagged_response(typ, dat, name)
406 def fetch(self, message_set, message_parts):
407 """Fetch (parts of) messages.
409 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
411 'message_parts' should be a string of selected parts
412 enclosed in parentheses, eg: "(UID BODY[TEXT])".
414 'data' are tuples of message part envelope and data.
416 name = 'FETCH'
417 typ, dat = self._simple_command(name, message_set, message_parts)
418 return self._untagged_response(typ, dat, name)
421 def getacl(self, mailbox):
422 """Get the ACLs for a mailbox.
424 (typ, [data]) = <instance>.getacl(mailbox)
426 typ, dat = self._simple_command('GETACL', mailbox)
427 return self._untagged_response(typ, dat, 'ACL')
430 def getquota(self, root):
431 """Get the quota root's resource usage and limits.
433 Part of the IMAP4 QUOTA extension defined in rfc2087.
435 (typ, [data]) = <instance>.getquota(root)
437 typ, dat = self._simple_command('GETQUOTA', root)
438 return self._untagged_response(typ, dat, 'QUOTA')
441 def getquotaroot(self, mailbox):
442 """Get the list of quota roots for the named mailbox.
444 (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox)
446 typ, dat = self._simple_command('GETQUOTA', mailbox)
447 typ, quota = self._untagged_response(typ, dat, 'QUOTA')
448 typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT')
449 return typ, [quotaroot, quota]
452 def list(self, directory='""', pattern='*'):
453 """List mailbox names in directory matching pattern.
455 (typ, [data]) = <instance>.list(directory='""', pattern='*')
457 'data' is list of LIST responses.
459 name = 'LIST'
460 typ, dat = self._simple_command(name, directory, pattern)
461 return self._untagged_response(typ, dat, name)
464 def login(self, user, password):
465 """Identify client using plaintext password.
467 (typ, [data]) = <instance>.login(user, password)
469 NB: 'password' will be quoted.
471 typ, dat = self._simple_command('LOGIN', user, self._quote(password))
472 if typ != 'OK':
473 raise self.error(dat[-1])
474 self.state = 'AUTH'
475 return typ, dat
478 def login_cram_md5(self, user, password):
479 """ Force use of CRAM-MD5 authentication.
481 (typ, [data]) = <instance>.login_cram_md5(user, password)
483 self.user, self.password = user, password
484 return self.authenticate('CRAM-MD5', self._CRAM_MD5_AUTH)
487 def _CRAM_MD5_AUTH(self, challenge):
488 """ Authobject to use with CRAM-MD5 authentication. """
489 import hmac
490 return self.user + " " + hmac.HMAC(self.password, challenge).hexdigest()
493 def logout(self):
494 """Shutdown connection to server.
496 (typ, [data]) = <instance>.logout()
498 Returns server 'BYE' response.
500 self.state = 'LOGOUT'
501 try: typ, dat = self._simple_command('LOGOUT')
502 except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
503 self.shutdown()
504 if 'BYE' in self.untagged_responses:
505 return 'BYE', self.untagged_responses['BYE']
506 return typ, dat
509 def lsub(self, directory='""', pattern='*'):
510 """List 'subscribed' mailbox names in directory matching pattern.
512 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
514 'data' are tuples of message part envelope and data.
516 name = 'LSUB'
517 typ, dat = self._simple_command(name, directory, pattern)
518 return self._untagged_response(typ, dat, name)
521 def namespace(self):
522 """ Returns IMAP namespaces ala rfc2342
524 (typ, [data, ...]) = <instance>.namespace()
526 name = 'NAMESPACE'
527 typ, dat = self._simple_command(name)
528 return self._untagged_response(typ, dat, name)
531 def noop(self):
532 """Send NOOP command.
534 (typ, [data]) = <instance>.noop()
536 if __debug__:
537 if self.debug >= 3:
538 self._dump_ur(self.untagged_responses)
539 return self._simple_command('NOOP')
542 def partial(self, message_num, message_part, start, length):
543 """Fetch truncated part of a message.
545 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
547 'data' is tuple of message part envelope and data.
549 name = 'PARTIAL'
550 typ, dat = self._simple_command(name, message_num, message_part, start, length)
551 return self._untagged_response(typ, dat, 'FETCH')
554 def proxyauth(self, user):
555 """Assume authentication as "user".
557 Allows an authorised administrator to proxy into any user's
558 mailbox.
560 (typ, [data]) = <instance>.proxyauth(user)
563 name = 'PROXYAUTH'
564 return self._simple_command('PROXYAUTH', user)
567 def rename(self, oldmailbox, newmailbox):
568 """Rename old mailbox name to new.
570 (typ, [data]) = <instance>.rename(oldmailbox, newmailbox)
572 return self._simple_command('RENAME', oldmailbox, newmailbox)
575 def search(self, charset, *criteria):
576 """Search mailbox for matching messages.
578 (typ, [data]) = <instance>.search(charset, criterion, ...)
580 'data' is space separated list of matching message numbers.
582 name = 'SEARCH'
583 if charset:
584 typ, dat = self._simple_command(name, 'CHARSET', charset, *criteria)
585 else:
586 typ, dat = self._simple_command(name, *criteria)
587 return self._untagged_response(typ, dat, name)
590 def select(self, mailbox='INBOX', readonly=None):
591 """Select a mailbox.
593 Flush all untagged responses.
595 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
597 'data' is count of messages in mailbox ('EXISTS' response).
599 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
600 self.untagged_responses = {} # Flush old responses.
601 self.is_readonly = readonly
602 if readonly is not None:
603 name = 'EXAMINE'
604 else:
605 name = 'SELECT'
606 typ, dat = self._simple_command(name, mailbox)
607 if typ != 'OK':
608 self.state = 'AUTH' # Might have been 'SELECTED'
609 return typ, dat
610 self.state = 'SELECTED'
611 if 'READ-ONLY' in self.untagged_responses \
612 and not readonly:
613 if __debug__:
614 if self.debug >= 1:
615 self._dump_ur(self.untagged_responses)
616 raise self.readonly('%s is not writable' % mailbox)
617 return typ, self.untagged_responses.get('EXISTS', [None])
620 def setacl(self, mailbox, who, what):
621 """Set a mailbox acl.
623 (typ, [data]) = <instance>.create(mailbox, who, what)
625 return self._simple_command('SETACL', mailbox, who, what)
628 def setquota(self, root, limits):
629 """Set the quota root's resource limits.
631 (typ, [data]) = <instance>.setquota(root, limits)
633 typ, dat = self._simple_command('SETQUOTA', root, limits)
634 return self._untagged_response(typ, dat, 'QUOTA')
637 def sort(self, sort_criteria, charset, *search_criteria):
638 """IMAP4rev1 extension SORT command.
640 (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
642 name = 'SORT'
643 #if not name in self.capabilities: # Let the server decide!
644 # raise self.error('unimplemented extension command: %s' % name)
645 if (sort_criteria[0],sort_criteria[-1]) != ('(',')'):
646 sort_criteria = '(%s)' % sort_criteria
647 typ, dat = self._simple_command(name, sort_criteria, charset, *search_criteria)
648 return self._untagged_response(typ, dat, name)
651 def status(self, mailbox, names):
652 """Request named status conditions for mailbox.
654 (typ, [data]) = <instance>.status(mailbox, names)
656 name = 'STATUS'
657 #if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide!
658 # raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
659 typ, dat = self._simple_command(name, mailbox, names)
660 return self._untagged_response(typ, dat, name)
663 def store(self, message_set, command, flags):
664 """Alters flag dispositions for messages in mailbox.
666 (typ, [data]) = <instance>.store(message_set, command, flags)
668 if (flags[0],flags[-1]) != ('(',')'):
669 flags = '(%s)' % flags # Avoid quoting the flags
670 typ, dat = self._simple_command('STORE', message_set, command, flags)
671 return self._untagged_response(typ, dat, 'FETCH')
674 def subscribe(self, mailbox):
675 """Subscribe to new mailbox.
677 (typ, [data]) = <instance>.subscribe(mailbox)
679 return self._simple_command('SUBSCRIBE', mailbox)
682 def uid(self, command, *args):
683 """Execute "command arg ..." with messages identified by UID,
684 rather than message number.
686 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
688 Returns response appropriate to 'command'.
690 command = command.upper()
691 if not command in Commands:
692 raise self.error("Unknown IMAP4 UID command: %s" % command)
693 if self.state not in Commands[command]:
694 raise self.error('command %s illegal in state %s'
695 % (command, self.state))
696 name = 'UID'
697 typ, dat = self._simple_command(name, command, *args)
698 if command in ('SEARCH', 'SORT'):
699 name = command
700 else:
701 name = 'FETCH'
702 return self._untagged_response(typ, dat, name)
705 def unsubscribe(self, mailbox):
706 """Unsubscribe from old mailbox.
708 (typ, [data]) = <instance>.unsubscribe(mailbox)
710 return self._simple_command('UNSUBSCRIBE', mailbox)
713 def xatom(self, name, *args):
714 """Allow simple extension commands
715 notified by server in CAPABILITY response.
717 Assumes command is legal in current state.
719 (typ, [data]) = <instance>.xatom(name, arg, ...)
721 Returns response appropriate to extension command `name'.
723 name = name.upper()
724 #if not name in self.capabilities: # Let the server decide!
725 # raise self.error('unknown extension command: %s' % name)
726 if not name in Commands:
727 Commands[name] = (self.state,)
728 return self._simple_command(name, *args)
732 # Private methods
735 def _append_untagged(self, typ, dat):
737 if dat is None: dat = ''
738 ur = self.untagged_responses
739 if __debug__:
740 if self.debug >= 5:
741 self._mesg('untagged_responses[%s] %s += ["%s"]' %
742 (typ, len(ur.get(typ,'')), dat))
743 if typ in ur:
744 ur[typ].append(dat)
745 else:
746 ur[typ] = [dat]
749 def _check_bye(self):
750 bye = self.untagged_responses.get('BYE')
751 if bye:
752 raise self.abort(bye[-1])
755 def _command(self, name, *args):
757 if self.state not in Commands[name]:
758 self.literal = None
759 raise self.error(
760 'command %s illegal in state %s' % (name, self.state))
762 for typ in ('OK', 'NO', 'BAD'):
763 if typ in self.untagged_responses:
764 del self.untagged_responses[typ]
766 if 'READ-ONLY' in self.untagged_responses \
767 and not self.is_readonly:
768 raise self.readonly('mailbox status changed to READ-ONLY')
770 tag = self._new_tag()
771 data = '%s %s' % (tag, name)
772 for arg in args:
773 if arg is None: continue
774 data = '%s %s' % (data, self._checkquote(arg))
776 literal = self.literal
777 if literal is not None:
778 self.literal = None
779 if type(literal) is type(self._command):
780 literator = literal
781 else:
782 literator = None
783 data = '%s {%s}' % (data, len(literal))
785 if __debug__:
786 if self.debug >= 4:
787 self._mesg('> %s' % data)
788 else:
789 self._log('> %s' % data)
791 try:
792 self.send('%s%s' % (data, CRLF))
793 except (socket.error, OSError), val:
794 raise self.abort('socket error: %s' % val)
796 if literal is None:
797 return tag
799 while 1:
800 # Wait for continuation response
802 while self._get_response():
803 if self.tagged_commands[tag]: # BAD/NO?
804 return tag
806 # Send literal
808 if literator:
809 literal = literator(self.continuation_response)
811 if __debug__:
812 if self.debug >= 4:
813 self._mesg('write literal size %s' % len(literal))
815 try:
816 self.send(literal)
817 self.send(CRLF)
818 except (socket.error, OSError), val:
819 raise self.abort('socket error: %s' % val)
821 if not literator:
822 break
824 return tag
827 def _command_complete(self, name, tag):
828 self._check_bye()
829 try:
830 typ, data = self._get_tagged_response(tag)
831 except self.abort, val:
832 raise self.abort('command: %s => %s' % (name, val))
833 except self.error, val:
834 raise self.error('command: %s => %s' % (name, val))
835 self._check_bye()
836 if typ == 'BAD':
837 raise self.error('%s command error: %s %s' % (name, typ, data))
838 return typ, data
841 def _get_response(self):
843 # Read response and store.
845 # Returns None for continuation responses,
846 # otherwise first response line received.
848 resp = self._get_line()
850 # Command completion response?
852 if self._match(self.tagre, resp):
853 tag = self.mo.group('tag')
854 if not tag in self.tagged_commands:
855 raise self.abort('unexpected tagged response: %s' % resp)
857 typ = self.mo.group('type')
858 dat = self.mo.group('data')
859 self.tagged_commands[tag] = (typ, [dat])
860 else:
861 dat2 = None
863 # '*' (untagged) responses?
865 if not self._match(Untagged_response, resp):
866 if self._match(Untagged_status, resp):
867 dat2 = self.mo.group('data2')
869 if self.mo is None:
870 # Only other possibility is '+' (continuation) response...
872 if self._match(Continuation, resp):
873 self.continuation_response = self.mo.group('data')
874 return None # NB: indicates continuation
876 raise self.abort("unexpected response: '%s'" % resp)
878 typ = self.mo.group('type')
879 dat = self.mo.group('data')
880 if dat is None: dat = '' # Null untagged response
881 if dat2: dat = dat + ' ' + dat2
883 # Is there a literal to come?
885 while self._match(Literal, dat):
887 # Read literal direct from connection.
889 size = int(self.mo.group('size'))
890 if __debug__:
891 if self.debug >= 4:
892 self._mesg('read literal size %s' % size)
893 data = self.read(size)
895 # Store response with literal as tuple
897 self._append_untagged(typ, (dat, data))
899 # Read trailer - possibly containing another literal
901 dat = self._get_line()
903 self._append_untagged(typ, dat)
905 # Bracketed response information?
907 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
908 self._append_untagged(self.mo.group('type'), self.mo.group('data'))
910 if __debug__:
911 if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
912 self._mesg('%s response: %s' % (typ, dat))
914 return resp
917 def _get_tagged_response(self, tag):
919 while 1:
920 result = self.tagged_commands[tag]
921 if result is not None:
922 del self.tagged_commands[tag]
923 return result
925 # Some have reported "unexpected response" exceptions.
926 # Note that ignoring them here causes loops.
927 # Instead, send me details of the unexpected response and
928 # I'll update the code in `_get_response()'.
930 try:
931 self._get_response()
932 except self.abort, val:
933 if __debug__:
934 if self.debug >= 1:
935 self.print_log()
936 raise
939 def _get_line(self):
941 line = self.readline()
942 if not line:
943 raise self.abort('socket error: EOF')
945 # Protocol mandates all lines terminated by CRLF
947 line = line[:-2]
948 if __debug__:
949 if self.debug >= 4:
950 self._mesg('< %s' % line)
951 else:
952 self._log('< %s' % line)
953 return line
956 def _match(self, cre, s):
958 # Run compiled regular expression match method on 's'.
959 # Save result, return success.
961 self.mo = cre.match(s)
962 if __debug__:
963 if self.mo is not None and self.debug >= 5:
964 self._mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`))
965 return self.mo is not None
968 def _new_tag(self):
970 tag = '%s%s' % (self.tagpre, self.tagnum)
971 self.tagnum = self.tagnum + 1
972 self.tagged_commands[tag] = None
973 return tag
976 def _checkquote(self, arg):
978 # Must quote command args if non-alphanumeric chars present,
979 # and not already quoted.
981 if type(arg) is not type(''):
982 return arg
983 if (arg[0],arg[-1]) in (('(',')'),('"','"')):
984 return arg
985 if self.mustquote.search(arg) is None:
986 return arg
987 return self._quote(arg)
990 def _quote(self, arg):
992 arg = arg.replace('\\', '\\\\')
993 arg = arg.replace('"', '\\"')
995 return '"%s"' % arg
998 def _simple_command(self, name, *args):
1000 return self._command_complete(name, self._command(name, *args))
1003 def _untagged_response(self, typ, dat, name):
1005 if typ == 'NO':
1006 return typ, dat
1007 if not name in self.untagged_responses:
1008 return typ, [None]
1009 data = self.untagged_responses.pop(name)
1010 if __debug__:
1011 if self.debug >= 5:
1012 self._mesg('untagged_responses[%s] => %s' % (name, data))
1013 return typ, data
1016 if __debug__:
1018 def _mesg(self, s, secs=None):
1019 if secs is None:
1020 secs = time.time()
1021 tm = time.strftime('%M:%S', time.localtime(secs))
1022 sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
1023 sys.stderr.flush()
1025 def _dump_ur(self, dict):
1026 # Dump untagged responses (in `dict').
1027 l = dict.items()
1028 if not l: return
1029 t = '\n\t\t'
1030 l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l)
1031 self._mesg('untagged responses dump:%s%s' % (t, t.join(l)))
1033 def _log(self, line):
1034 # Keep log of last `_cmd_log_len' interactions for debugging.
1035 self._cmd_log[self._cmd_log_idx] = (line, time.time())
1036 self._cmd_log_idx += 1
1037 if self._cmd_log_idx >= self._cmd_log_len:
1038 self._cmd_log_idx = 0
1040 def print_log(self):
1041 self._mesg('last %d IMAP4 interactions:' % len(self._cmd_log))
1042 i, n = self._cmd_log_idx, self._cmd_log_len
1043 while n:
1044 try:
1045 self._mesg(*self._cmd_log[i])
1046 except:
1047 pass
1048 i += 1
1049 if i >= self._cmd_log_len:
1050 i = 0
1051 n -= 1
1055 class IMAP4_SSL(IMAP4):
1057 """IMAP4 client class over SSL connection
1059 Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile]]]])
1061 host - host's name (default: localhost);
1062 port - port number (default: standard IMAP4 SSL port).
1063 keyfile - PEM formatted file that contains your private key (default: None);
1064 certfile - PEM formatted certificate chain file (default: None);
1066 for more documentation see the docstring of the parent class IMAP4.
1070 def __init__(self, host = '', port = IMAP4_SSL_PORT, keyfile = None, certfile = None):
1071 self.keyfile = keyfile
1072 self.certfile = certfile
1073 IMAP4.__init__(self, host, port)
1076 def open(self, host = '', port = IMAP4_SSL_PORT):
1077 """Setup connection to remote server on "host:port".
1078 (default: localhost:standard IMAP4 SSL port).
1079 This connection will be used by the routines:
1080 read, readline, send, shutdown.
1082 self.host = host
1083 self.port = port
1084 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1085 self.sock.connect((host, port))
1086 self.sslobj = socket.ssl(self.sock, self.keyfile, self.certfile)
1089 def read(self, size):
1090 """Read 'size' bytes from remote."""
1091 # sslobj.read() sometimes returns < size bytes
1092 data = self.sslobj.read(size)
1093 while len(data) < size:
1094 data += self.sslobj.read(size-len(data))
1096 return data
1099 def readline(self):
1100 """Read line from remote."""
1101 # NB: socket.ssl needs a "readline" method, or perhaps a "makefile" method.
1102 line = ""
1103 while 1:
1104 char = self.sslobj.read(1)
1105 line += char
1106 if char == "\n": return line
1109 def send(self, data):
1110 """Send data to remote."""
1111 # NB: socket.ssl needs a "sendall" method to match socket objects.
1112 bytes = len(data)
1113 while bytes > 0:
1114 sent = self.sslobj.write(data)
1115 if sent == bytes:
1116 break # avoid copy
1117 data = data[sent:]
1118 bytes = bytes - sent
1121 def shutdown(self):
1122 """Close I/O established in "open"."""
1123 self.sock.close()
1126 def socket(self):
1127 """Return socket instance used to connect to IMAP4 server.
1129 socket = <instance>.socket()
1131 return self.sock
1134 def ssl(self):
1135 """Return SSLObject instance used to communicate with the IMAP4 server.
1137 ssl = <instance>.socket.ssl()
1139 return self.sslobj
1143 class IMAP4_stream(IMAP4):
1145 """IMAP4 client class over a stream
1147 Instantiate with: IMAP4_stream(command)
1149 where "command" is a string that can be passed to os.popen2()
1151 for more documentation see the docstring of the parent class IMAP4.
1155 def __init__(self, command):
1156 self.command = command
1157 IMAP4.__init__(self)
1160 def open(self, host = None, port = None):
1161 """Setup a stream connection.
1162 This connection will be used by the routines:
1163 read, readline, send, shutdown.
1165 self.host = None # For compatibility with parent class
1166 self.port = None
1167 self.sock = None
1168 self.file = None
1169 self.writefile, self.readfile = os.popen2(self.command)
1172 def read(self, size):
1173 """Read 'size' bytes from remote."""
1174 return self.readfile.read(size)
1177 def readline(self):
1178 """Read line from remote."""
1179 return self.readfile.readline()
1182 def send(self, data):
1183 """Send data to remote."""
1184 self.writefile.write(data)
1185 self.writefile.flush()
1188 def shutdown(self):
1189 """Close I/O established in "open"."""
1190 self.readfile.close()
1191 self.writefile.close()
1195 class _Authenticator:
1197 """Private class to provide en/decoding
1198 for base64-based authentication conversation.
1201 def __init__(self, mechinst):
1202 self.mech = mechinst # Callable object to provide/process data
1204 def process(self, data):
1205 ret = self.mech(self.decode(data))
1206 if ret is None:
1207 return '*' # Abort conversation
1208 return self.encode(ret)
1210 def encode(self, inp):
1212 # Invoke binascii.b2a_base64 iteratively with
1213 # short even length buffers, strip the trailing
1214 # line feed from the result and append. "Even"
1215 # means a number that factors to both 6 and 8,
1216 # so when it gets to the end of the 8-bit input
1217 # there's no partial 6-bit output.
1219 oup = ''
1220 while inp:
1221 if len(inp) > 48:
1222 t = inp[:48]
1223 inp = inp[48:]
1224 else:
1225 t = inp
1226 inp = ''
1227 e = binascii.b2a_base64(t)
1228 if e:
1229 oup = oup + e[:-1]
1230 return oup
1232 def decode(self, inp):
1233 if not inp:
1234 return ''
1235 return binascii.a2b_base64(inp)
1239 Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
1240 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
1242 def Internaldate2tuple(resp):
1243 """Convert IMAP4 INTERNALDATE to UT.
1245 Returns Python time module tuple.
1248 mo = InternalDate.match(resp)
1249 if not mo:
1250 return None
1252 mon = Mon2num[mo.group('mon')]
1253 zonen = mo.group('zonen')
1255 day = int(mo.group('day'))
1256 year = int(mo.group('year'))
1257 hour = int(mo.group('hour'))
1258 min = int(mo.group('min'))
1259 sec = int(mo.group('sec'))
1260 zoneh = int(mo.group('zoneh'))
1261 zonem = int(mo.group('zonem'))
1263 # INTERNALDATE timezone must be subtracted to get UT
1265 zone = (zoneh*60 + zonem)*60
1266 if zonen == '-':
1267 zone = -zone
1269 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
1271 utc = time.mktime(tt)
1273 # Following is necessary because the time module has no 'mkgmtime'.
1274 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
1276 lt = time.localtime(utc)
1277 if time.daylight and lt[-1]:
1278 zone = zone + time.altzone
1279 else:
1280 zone = zone + time.timezone
1282 return time.localtime(utc - zone)
1286 def Int2AP(num):
1288 """Convert integer to A-P string representation."""
1290 val = ''; AP = 'ABCDEFGHIJKLMNOP'
1291 num = int(abs(num))
1292 while num:
1293 num, mod = divmod(num, 16)
1294 val = AP[mod] + val
1295 return val
1299 def ParseFlags(resp):
1301 """Convert IMAP4 flags response to python tuple."""
1303 mo = Flags.match(resp)
1304 if not mo:
1305 return ()
1307 return tuple(mo.group('flags').split())
1310 def Time2Internaldate(date_time):
1312 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
1314 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
1317 if isinstance(date_time, (int, float)):
1318 tt = time.localtime(date_time)
1319 elif isinstance(date_time, (tuple, time.struct_time)):
1320 tt = date_time
1321 elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'):
1322 return date_time # Assume in correct format
1323 else:
1324 raise ValueError("date_time not of a known type")
1326 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
1327 if dt[0] == '0':
1328 dt = ' ' + dt[1:]
1329 if time.daylight and tt[-1]:
1330 zone = -time.altzone
1331 else:
1332 zone = -time.timezone
1333 return '"' + dt + " %+03d%02d" % divmod(zone/60, 60) + '"'
1337 if __name__ == '__main__':
1339 # To test: invoke either as 'python imaplib.py [IMAP4_server_hostname]'
1340 # or 'python imaplib.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"'
1341 # to test the IMAP4_stream class
1343 import getopt, getpass
1345 try:
1346 optlist, args = getopt.getopt(sys.argv[1:], 'd:s:')
1347 except getopt.error, val:
1348 optlist, args = (), ()
1350 stream_command = None
1351 for opt,val in optlist:
1352 if opt == '-d':
1353 Debug = int(val)
1354 elif opt == '-s':
1355 stream_command = val
1356 if not args: args = (stream_command,)
1358 if not args: args = ('',)
1360 host = args[0]
1362 USER = getpass.getuser()
1363 PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost"))
1365 test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER, 'lf':'\n'}
1366 test_seq1 = (
1367 ('login', (USER, PASSWD)),
1368 ('create', ('/tmp/xxx 1',)),
1369 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1370 ('CREATE', ('/tmp/yyz 2',)),
1371 ('append', ('/tmp/yyz 2', None, None, test_mesg)),
1372 ('list', ('/tmp', 'yy*')),
1373 ('select', ('/tmp/yyz 2',)),
1374 ('search', (None, 'SUBJECT', 'test')),
1375 ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
1376 ('store', ('1', 'FLAGS', '(\Deleted)')),
1377 ('namespace', ()),
1378 ('expunge', ()),
1379 ('recent', ()),
1380 ('close', ()),
1383 test_seq2 = (
1384 ('select', ()),
1385 ('response',('UIDVALIDITY',)),
1386 ('uid', ('SEARCH', 'ALL')),
1387 ('response', ('EXISTS',)),
1388 ('append', (None, None, None, test_mesg)),
1389 ('recent', ()),
1390 ('logout', ()),
1393 def run(cmd, args):
1394 M._mesg('%s %s' % (cmd, args))
1395 typ, dat = getattr(M, cmd)(*args)
1396 M._mesg('%s => %s %s' % (cmd, typ, dat))
1397 if typ == 'NO': raise dat[0]
1398 return dat
1400 try:
1401 if stream_command:
1402 M = IMAP4_stream(stream_command)
1403 else:
1404 M = IMAP4(host)
1405 if M.state == 'AUTH':
1406 test_seq1 = test_seq1[1:] # Login not needed
1407 M._mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
1408 M._mesg('CAPABILITIES = %s' % `M.capabilities`)
1410 for cmd,args in test_seq1:
1411 run(cmd, args)
1413 for ml in run('list', ('/tmp/', 'yy%')):
1414 mo = re.match(r'.*"([^"]+)"$', ml)
1415 if mo: path = mo.group(1)
1416 else: path = ml.split()[-1]
1417 run('delete', (path,))
1419 for cmd,args in test_seq2:
1420 dat = run(cmd, args)
1422 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
1423 continue
1425 uid = dat[-1].split()
1426 if not uid: continue
1427 run('uid', ('FETCH', '%s' % uid[-1],
1428 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1430 print '\nAll tests OK.'
1432 except:
1433 print '\nTests failed.'
1435 if not Debug:
1436 print '''
1437 If you would like to see debugging output,
1438 try: %s -d5
1439 ''' % sys.argv[0]
1441 raise