Issue #7295: Do not use a hardcoded file name in test_tarfile.
[python.git] / Lib / imaplib.py
blobf13350e3223d98cadca32f33649302082c609041
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.
21 # GET/SETANNOTATION contributed by Tomas Lindroos <skitta@abo.fi> June 2005.
23 __version__ = "2.58"
25 import binascii, os, random, re, socket, sys, time
27 __all__ = ["IMAP4", "IMAP4_stream", "Internaldate2tuple",
28 "Int2AP", "ParseFlags", "Time2Internaldate"]
30 # Globals
32 CRLF = '\r\n'
33 Debug = 0
34 IMAP4_PORT = 143
35 IMAP4_SSL_PORT = 993
36 AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first
38 # Commands
40 Commands = {
41 # name valid states
42 'APPEND': ('AUTH', 'SELECTED'),
43 'AUTHENTICATE': ('NONAUTH',),
44 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
45 'CHECK': ('SELECTED',),
46 'CLOSE': ('SELECTED',),
47 'COPY': ('SELECTED',),
48 'CREATE': ('AUTH', 'SELECTED'),
49 'DELETE': ('AUTH', 'SELECTED'),
50 'DELETEACL': ('AUTH', 'SELECTED'),
51 'EXAMINE': ('AUTH', 'SELECTED'),
52 'EXPUNGE': ('SELECTED',),
53 'FETCH': ('SELECTED',),
54 'GETACL': ('AUTH', 'SELECTED'),
55 'GETANNOTATION':('AUTH', 'SELECTED'),
56 'GETQUOTA': ('AUTH', 'SELECTED'),
57 'GETQUOTAROOT': ('AUTH', 'SELECTED'),
58 'MYRIGHTS': ('AUTH', 'SELECTED'),
59 'LIST': ('AUTH', 'SELECTED'),
60 'LOGIN': ('NONAUTH',),
61 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
62 'LSUB': ('AUTH', 'SELECTED'),
63 'NAMESPACE': ('AUTH', 'SELECTED'),
64 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
65 'PARTIAL': ('SELECTED',), # NB: obsolete
66 'PROXYAUTH': ('AUTH',),
67 'RENAME': ('AUTH', 'SELECTED'),
68 'SEARCH': ('SELECTED',),
69 'SELECT': ('AUTH', 'SELECTED'),
70 'SETACL': ('AUTH', 'SELECTED'),
71 'SETANNOTATION':('AUTH', 'SELECTED'),
72 'SETQUOTA': ('AUTH', 'SELECTED'),
73 'SORT': ('SELECTED',),
74 'STATUS': ('AUTH', 'SELECTED'),
75 'STORE': ('SELECTED',),
76 'SUBSCRIBE': ('AUTH', 'SELECTED'),
77 'THREAD': ('SELECTED',),
78 'UID': ('SELECTED',),
79 'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
82 # Patterns to match server responses
84 Continuation = re.compile(r'\+( (?P<data>.*))?')
85 Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
86 InternalDate = re.compile(r'.*INTERNALDATE "'
87 r'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
88 r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
89 r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
90 r'"')
91 Literal = re.compile(r'.*{(?P<size>\d+)}$')
92 MapCRLF = re.compile(r'\r\n|\r|\n')
93 Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
94 Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
95 Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
99 class IMAP4:
101 """IMAP4 client class.
103 Instantiate with: IMAP4([host[, port]])
105 host - host's name (default: localhost);
106 port - port number (default: standard IMAP4 port).
108 All IMAP4rev1 commands are supported by methods of the same
109 name (in lower-case).
111 All arguments to commands are converted to strings, except for
112 AUTHENTICATE, and the last argument to APPEND which is passed as
113 an IMAP4 literal. If necessary (the string contains any
114 non-printing characters or white-space and isn't enclosed with
115 either parentheses or double quotes) each string is quoted.
116 However, the 'password' argument to the LOGIN command is always
117 quoted. If you want to avoid having an argument string quoted
118 (eg: the 'flags' argument to STORE) then enclose the string in
119 parentheses (eg: "(\Deleted)").
121 Each command returns a tuple: (type, [data, ...]) where 'type'
122 is usually 'OK' or 'NO', and 'data' is either the text from the
123 tagged response, or untagged results from command. Each 'data'
124 is either a string, or a tuple. If a tuple, then the first part
125 is the header of the response, and the second part contains
126 the data (ie: 'literal' value).
128 Errors raise the exception class <instance>.error("<reason>").
129 IMAP4 server errors raise <instance>.abort("<reason>"),
130 which is a sub-class of 'error'. Mailbox status changes
131 from READ-WRITE to READ-ONLY raise the exception class
132 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
134 "error" exceptions imply a program error.
135 "abort" exceptions imply the connection should be reset, and
136 the command re-tried.
137 "readonly" exceptions imply the command should be re-tried.
139 Note: to use this module, you must read the RFCs pertaining to the
140 IMAP4 protocol, as the semantics of the arguments to each IMAP4
141 command are left to the invoker, not to mention the results. Also,
142 most IMAP servers implement a sub-set of the commands available here.
145 class error(Exception): pass # Logical errors - debug required
146 class abort(error): pass # Service errors - close and retry
147 class readonly(abort): pass # Mailbox status changed to READ-ONLY
149 mustquote = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]")
151 def __init__(self, host = '', port = IMAP4_PORT):
152 self.debug = Debug
153 self.state = 'LOGOUT'
154 self.literal = None # A literal argument to a command
155 self.tagged_commands = {} # Tagged commands awaiting response
156 self.untagged_responses = {} # {typ: [data, ...], ...}
157 self.continuation_response = '' # Last continuation response
158 self.is_readonly = False # READ-ONLY desired state
159 self.tagnum = 0
161 # Open socket to server.
163 self.open(host, port)
165 # Create unique tag for this session,
166 # and compile tagged response matcher.
168 self.tagpre = Int2AP(random.randint(4096, 65535))
169 self.tagre = re.compile(r'(?P<tag>'
170 + self.tagpre
171 + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
173 # Get server welcome message,
174 # request and store CAPABILITY response.
176 if __debug__:
177 self._cmd_log_len = 10
178 self._cmd_log_idx = 0
179 self._cmd_log = {} # Last `_cmd_log_len' interactions
180 if self.debug >= 1:
181 self._mesg('imaplib version %s' % __version__)
182 self._mesg('new IMAP4 connection, tag=%s' % self.tagpre)
184 self.welcome = self._get_response()
185 if 'PREAUTH' in self.untagged_responses:
186 self.state = 'AUTH'
187 elif 'OK' in self.untagged_responses:
188 self.state = 'NONAUTH'
189 else:
190 raise self.error(self.welcome)
192 typ, dat = self.capability()
193 if dat == [None]:
194 raise self.error('no CAPABILITY response from server')
195 self.capabilities = tuple(dat[-1].upper().split())
197 if __debug__:
198 if self.debug >= 3:
199 self._mesg('CAPABILITIES: %r' % (self.capabilities,))
201 for version in AllowedVersions:
202 if not version in self.capabilities:
203 continue
204 self.PROTOCOL_VERSION = version
205 return
207 raise self.error('server not IMAP4 compliant')
210 def __getattr__(self, attr):
211 # Allow UPPERCASE variants of IMAP4 command methods.
212 if attr in Commands:
213 return getattr(self, attr.lower())
214 raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
218 # Overridable methods
221 def open(self, host = '', port = IMAP4_PORT):
222 """Setup connection to remote server on "host:port"
223 (default: localhost:standard IMAP4 port).
224 This connection will be used by the routines:
225 read, readline, send, shutdown.
227 self.host = host
228 self.port = port
229 self.sock = socket.create_connection((host, port))
230 self.file = self.sock.makefile('rb')
233 def read(self, size):
234 """Read 'size' bytes from remote."""
235 return self.file.read(size)
238 def readline(self):
239 """Read line from remote."""
240 return self.file.readline()
243 def send(self, data):
244 """Send data to remote."""
245 self.sock.sendall(data)
248 def shutdown(self):
249 """Close I/O established in "open"."""
250 self.file.close()
251 self.sock.close()
254 def socket(self):
255 """Return socket instance used to connect to IMAP4 server.
257 socket = <instance>.socket()
259 return self.sock
263 # Utility methods
266 def recent(self):
267 """Return most recent 'RECENT' responses if any exist,
268 else prompt server for an update using the 'NOOP' command.
270 (typ, [data]) = <instance>.recent()
272 'data' is None if no new messages,
273 else list of RECENT responses, most recent last.
275 name = 'RECENT'
276 typ, dat = self._untagged_response('OK', [None], name)
277 if dat[-1]:
278 return typ, dat
279 typ, dat = self.noop() # Prod server for response
280 return self._untagged_response(typ, dat, name)
283 def response(self, code):
284 """Return data for response 'code' if received, or None.
286 Old value for response 'code' is cleared.
288 (code, [data]) = <instance>.response(code)
290 return self._untagged_response(code, [None], code.upper())
294 # IMAP4 commands
297 def append(self, mailbox, flags, date_time, message):
298 """Append message to named mailbox.
300 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
302 All args except `message' can be None.
304 name = 'APPEND'
305 if not mailbox:
306 mailbox = 'INBOX'
307 if flags:
308 if (flags[0],flags[-1]) != ('(',')'):
309 flags = '(%s)' % flags
310 else:
311 flags = None
312 if date_time:
313 date_time = Time2Internaldate(date_time)
314 else:
315 date_time = None
316 self.literal = MapCRLF.sub(CRLF, message)
317 return self._simple_command(name, mailbox, flags, date_time)
320 def authenticate(self, mechanism, authobject):
321 """Authenticate command - requires response processing.
323 'mechanism' specifies which authentication mechanism is to
324 be used - it must appear in <instance>.capabilities in the
325 form AUTH=<mechanism>.
327 'authobject' must be a callable object:
329 data = authobject(response)
331 It will be called to process server continuation responses.
332 It should return data that will be encoded and sent to server.
333 It should return None if the client abort response '*' should
334 be sent instead.
336 mech = mechanism.upper()
337 # XXX: shouldn't this code be removed, not commented out?
338 #cap = 'AUTH=%s' % mech
339 #if not cap in self.capabilities: # Let the server decide!
340 # raise self.error("Server doesn't allow %s authentication." % mech)
341 self.literal = _Authenticator(authobject).process
342 typ, dat = self._simple_command('AUTHENTICATE', mech)
343 if typ != 'OK':
344 raise self.error(dat[-1])
345 self.state = 'AUTH'
346 return typ, dat
349 def capability(self):
350 """(typ, [data]) = <instance>.capability()
351 Fetch capabilities list from server."""
353 name = 'CAPABILITY'
354 typ, dat = self._simple_command(name)
355 return self._untagged_response(typ, dat, name)
358 def check(self):
359 """Checkpoint mailbox on server.
361 (typ, [data]) = <instance>.check()
363 return self._simple_command('CHECK')
366 def close(self):
367 """Close currently selected mailbox.
369 Deleted messages are removed from writable mailbox.
370 This is the recommended command before 'LOGOUT'.
372 (typ, [data]) = <instance>.close()
374 try:
375 typ, dat = self._simple_command('CLOSE')
376 finally:
377 self.state = 'AUTH'
378 return typ, dat
381 def copy(self, message_set, new_mailbox):
382 """Copy 'message_set' messages onto end of 'new_mailbox'.
384 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
386 return self._simple_command('COPY', message_set, new_mailbox)
389 def create(self, mailbox):
390 """Create new mailbox.
392 (typ, [data]) = <instance>.create(mailbox)
394 return self._simple_command('CREATE', mailbox)
397 def delete(self, mailbox):
398 """Delete old mailbox.
400 (typ, [data]) = <instance>.delete(mailbox)
402 return self._simple_command('DELETE', mailbox)
404 def deleteacl(self, mailbox, who):
405 """Delete the ACLs (remove any rights) set for who on mailbox.
407 (typ, [data]) = <instance>.deleteacl(mailbox, who)
409 return self._simple_command('DELETEACL', mailbox, who)
411 def expunge(self):
412 """Permanently remove deleted items from selected mailbox.
414 Generates 'EXPUNGE' response for each deleted message.
416 (typ, [data]) = <instance>.expunge()
418 'data' is list of 'EXPUNGE'd message numbers in order received.
420 name = 'EXPUNGE'
421 typ, dat = self._simple_command(name)
422 return self._untagged_response(typ, dat, name)
425 def fetch(self, message_set, message_parts):
426 """Fetch (parts of) messages.
428 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
430 'message_parts' should be a string of selected parts
431 enclosed in parentheses, eg: "(UID BODY[TEXT])".
433 'data' are tuples of message part envelope and data.
435 name = 'FETCH'
436 typ, dat = self._simple_command(name, message_set, message_parts)
437 return self._untagged_response(typ, dat, name)
440 def getacl(self, mailbox):
441 """Get the ACLs for a mailbox.
443 (typ, [data]) = <instance>.getacl(mailbox)
445 typ, dat = self._simple_command('GETACL', mailbox)
446 return self._untagged_response(typ, dat, 'ACL')
449 def getannotation(self, mailbox, entry, attribute):
450 """(typ, [data]) = <instance>.getannotation(mailbox, entry, attribute)
451 Retrieve ANNOTATIONs."""
453 typ, dat = self._simple_command('GETANNOTATION', mailbox, entry, attribute)
454 return self._untagged_response(typ, dat, 'ANNOTATION')
457 def getquota(self, root):
458 """Get the quota root's resource usage and limits.
460 Part of the IMAP4 QUOTA extension defined in rfc2087.
462 (typ, [data]) = <instance>.getquota(root)
464 typ, dat = self._simple_command('GETQUOTA', root)
465 return self._untagged_response(typ, dat, 'QUOTA')
468 def getquotaroot(self, mailbox):
469 """Get the list of quota roots for the named mailbox.
471 (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox)
473 typ, dat = self._simple_command('GETQUOTAROOT', mailbox)
474 typ, quota = self._untagged_response(typ, dat, 'QUOTA')
475 typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT')
476 return typ, [quotaroot, quota]
479 def list(self, directory='""', pattern='*'):
480 """List mailbox names in directory matching pattern.
482 (typ, [data]) = <instance>.list(directory='""', pattern='*')
484 'data' is list of LIST responses.
486 name = 'LIST'
487 typ, dat = self._simple_command(name, directory, pattern)
488 return self._untagged_response(typ, dat, name)
491 def login(self, user, password):
492 """Identify client using plaintext password.
494 (typ, [data]) = <instance>.login(user, password)
496 NB: 'password' will be quoted.
498 typ, dat = self._simple_command('LOGIN', user, self._quote(password))
499 if typ != 'OK':
500 raise self.error(dat[-1])
501 self.state = 'AUTH'
502 return typ, dat
505 def login_cram_md5(self, user, password):
506 """ Force use of CRAM-MD5 authentication.
508 (typ, [data]) = <instance>.login_cram_md5(user, password)
510 self.user, self.password = user, password
511 return self.authenticate('CRAM-MD5', self._CRAM_MD5_AUTH)
514 def _CRAM_MD5_AUTH(self, challenge):
515 """ Authobject to use with CRAM-MD5 authentication. """
516 import hmac
517 return self.user + " " + hmac.HMAC(self.password, challenge).hexdigest()
520 def logout(self):
521 """Shutdown connection to server.
523 (typ, [data]) = <instance>.logout()
525 Returns server 'BYE' response.
527 self.state = 'LOGOUT'
528 try: typ, dat = self._simple_command('LOGOUT')
529 except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
530 self.shutdown()
531 if 'BYE' in self.untagged_responses:
532 return 'BYE', self.untagged_responses['BYE']
533 return typ, dat
536 def lsub(self, directory='""', pattern='*'):
537 """List 'subscribed' mailbox names in directory matching pattern.
539 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
541 'data' are tuples of message part envelope and data.
543 name = 'LSUB'
544 typ, dat = self._simple_command(name, directory, pattern)
545 return self._untagged_response(typ, dat, name)
547 def myrights(self, mailbox):
548 """Show my ACLs for a mailbox (i.e. the rights that I have on mailbox).
550 (typ, [data]) = <instance>.myrights(mailbox)
552 typ,dat = self._simple_command('MYRIGHTS', mailbox)
553 return self._untagged_response(typ, dat, 'MYRIGHTS')
555 def namespace(self):
556 """ Returns IMAP namespaces ala rfc2342
558 (typ, [data, ...]) = <instance>.namespace()
560 name = 'NAMESPACE'
561 typ, dat = self._simple_command(name)
562 return self._untagged_response(typ, dat, name)
565 def noop(self):
566 """Send NOOP command.
568 (typ, [data]) = <instance>.noop()
570 if __debug__:
571 if self.debug >= 3:
572 self._dump_ur(self.untagged_responses)
573 return self._simple_command('NOOP')
576 def partial(self, message_num, message_part, start, length):
577 """Fetch truncated part of a message.
579 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
581 'data' is tuple of message part envelope and data.
583 name = 'PARTIAL'
584 typ, dat = self._simple_command(name, message_num, message_part, start, length)
585 return self._untagged_response(typ, dat, 'FETCH')
588 def proxyauth(self, user):
589 """Assume authentication as "user".
591 Allows an authorised administrator to proxy into any user's
592 mailbox.
594 (typ, [data]) = <instance>.proxyauth(user)
597 name = 'PROXYAUTH'
598 return self._simple_command('PROXYAUTH', user)
601 def rename(self, oldmailbox, newmailbox):
602 """Rename old mailbox name to new.
604 (typ, [data]) = <instance>.rename(oldmailbox, newmailbox)
606 return self._simple_command('RENAME', oldmailbox, newmailbox)
609 def search(self, charset, *criteria):
610 """Search mailbox for matching messages.
612 (typ, [data]) = <instance>.search(charset, criterion, ...)
614 'data' is space separated list of matching message numbers.
616 name = 'SEARCH'
617 if charset:
618 typ, dat = self._simple_command(name, 'CHARSET', charset, *criteria)
619 else:
620 typ, dat = self._simple_command(name, *criteria)
621 return self._untagged_response(typ, dat, name)
624 def select(self, mailbox='INBOX', readonly=False):
625 """Select a mailbox.
627 Flush all untagged responses.
629 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=False)
631 'data' is count of messages in mailbox ('EXISTS' response).
633 Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
634 other responses should be obtained via <instance>.response('FLAGS') etc.
636 self.untagged_responses = {} # Flush old responses.
637 self.is_readonly = readonly
638 if readonly:
639 name = 'EXAMINE'
640 else:
641 name = 'SELECT'
642 typ, dat = self._simple_command(name, mailbox)
643 if typ != 'OK':
644 self.state = 'AUTH' # Might have been 'SELECTED'
645 return typ, dat
646 self.state = 'SELECTED'
647 if 'READ-ONLY' in self.untagged_responses \
648 and not readonly:
649 if __debug__:
650 if self.debug >= 1:
651 self._dump_ur(self.untagged_responses)
652 raise self.readonly('%s is not writable' % mailbox)
653 return typ, self.untagged_responses.get('EXISTS', [None])
656 def setacl(self, mailbox, who, what):
657 """Set a mailbox acl.
659 (typ, [data]) = <instance>.setacl(mailbox, who, what)
661 return self._simple_command('SETACL', mailbox, who, what)
664 def setannotation(self, *args):
665 """(typ, [data]) = <instance>.setannotation(mailbox[, entry, attribute]+)
666 Set ANNOTATIONs."""
668 typ, dat = self._simple_command('SETANNOTATION', *args)
669 return self._untagged_response(typ, dat, 'ANNOTATION')
672 def setquota(self, root, limits):
673 """Set the quota root's resource limits.
675 (typ, [data]) = <instance>.setquota(root, limits)
677 typ, dat = self._simple_command('SETQUOTA', root, limits)
678 return self._untagged_response(typ, dat, 'QUOTA')
681 def sort(self, sort_criteria, charset, *search_criteria):
682 """IMAP4rev1 extension SORT command.
684 (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
686 name = 'SORT'
687 #if not name in self.capabilities: # Let the server decide!
688 # raise self.error('unimplemented extension command: %s' % name)
689 if (sort_criteria[0],sort_criteria[-1]) != ('(',')'):
690 sort_criteria = '(%s)' % sort_criteria
691 typ, dat = self._simple_command(name, sort_criteria, charset, *search_criteria)
692 return self._untagged_response(typ, dat, name)
695 def status(self, mailbox, names):
696 """Request named status conditions for mailbox.
698 (typ, [data]) = <instance>.status(mailbox, names)
700 name = 'STATUS'
701 #if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide!
702 # raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
703 typ, dat = self._simple_command(name, mailbox, names)
704 return self._untagged_response(typ, dat, name)
707 def store(self, message_set, command, flags):
708 """Alters flag dispositions for messages in mailbox.
710 (typ, [data]) = <instance>.store(message_set, command, flags)
712 if (flags[0],flags[-1]) != ('(',')'):
713 flags = '(%s)' % flags # Avoid quoting the flags
714 typ, dat = self._simple_command('STORE', message_set, command, flags)
715 return self._untagged_response(typ, dat, 'FETCH')
718 def subscribe(self, mailbox):
719 """Subscribe to new mailbox.
721 (typ, [data]) = <instance>.subscribe(mailbox)
723 return self._simple_command('SUBSCRIBE', mailbox)
726 def thread(self, threading_algorithm, charset, *search_criteria):
727 """IMAPrev1 extension THREAD command.
729 (type, [data]) = <instance>.thread(threading_alogrithm, charset, search_criteria, ...)
731 name = 'THREAD'
732 typ, dat = self._simple_command(name, threading_algorithm, charset, *search_criteria)
733 return self._untagged_response(typ, dat, name)
736 def uid(self, command, *args):
737 """Execute "command arg ..." with messages identified by UID,
738 rather than message number.
740 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
742 Returns response appropriate to 'command'.
744 command = command.upper()
745 if not command in Commands:
746 raise self.error("Unknown IMAP4 UID command: %s" % command)
747 if self.state not in Commands[command]:
748 raise self.error("command %s illegal in state %s, "
749 "only allowed in states %s" %
750 (command, self.state,
751 ', '.join(Commands[command])))
752 name = 'UID'
753 typ, dat = self._simple_command(name, command, *args)
754 if command in ('SEARCH', 'SORT'):
755 name = command
756 else:
757 name = 'FETCH'
758 return self._untagged_response(typ, dat, name)
761 def unsubscribe(self, mailbox):
762 """Unsubscribe from old mailbox.
764 (typ, [data]) = <instance>.unsubscribe(mailbox)
766 return self._simple_command('UNSUBSCRIBE', mailbox)
769 def xatom(self, name, *args):
770 """Allow simple extension commands
771 notified by server in CAPABILITY response.
773 Assumes command is legal in current state.
775 (typ, [data]) = <instance>.xatom(name, arg, ...)
777 Returns response appropriate to extension command `name'.
779 name = name.upper()
780 #if not name in self.capabilities: # Let the server decide!
781 # raise self.error('unknown extension command: %s' % name)
782 if not name in Commands:
783 Commands[name] = (self.state,)
784 return self._simple_command(name, *args)
788 # Private methods
791 def _append_untagged(self, typ, dat):
793 if dat is None: dat = ''
794 ur = self.untagged_responses
795 if __debug__:
796 if self.debug >= 5:
797 self._mesg('untagged_responses[%s] %s += ["%s"]' %
798 (typ, len(ur.get(typ,'')), dat))
799 if typ in ur:
800 ur[typ].append(dat)
801 else:
802 ur[typ] = [dat]
805 def _check_bye(self):
806 bye = self.untagged_responses.get('BYE')
807 if bye:
808 raise self.abort(bye[-1])
811 def _command(self, name, *args):
813 if self.state not in Commands[name]:
814 self.literal = None
815 raise self.error("command %s illegal in state %s, "
816 "only allowed in states %s" %
817 (name, self.state,
818 ', '.join(Commands[name])))
820 for typ in ('OK', 'NO', 'BAD'):
821 if typ in self.untagged_responses:
822 del self.untagged_responses[typ]
824 if 'READ-ONLY' in self.untagged_responses \
825 and not self.is_readonly:
826 raise self.readonly('mailbox status changed to READ-ONLY')
828 tag = self._new_tag()
829 data = '%s %s' % (tag, name)
830 for arg in args:
831 if arg is None: continue
832 data = '%s %s' % (data, self._checkquote(arg))
834 literal = self.literal
835 if literal is not None:
836 self.literal = None
837 if type(literal) is type(self._command):
838 literator = literal
839 else:
840 literator = None
841 data = '%s {%s}' % (data, len(literal))
843 if __debug__:
844 if self.debug >= 4:
845 self._mesg('> %s' % data)
846 else:
847 self._log('> %s' % data)
849 try:
850 self.send('%s%s' % (data, CRLF))
851 except (socket.error, OSError), val:
852 raise self.abort('socket error: %s' % val)
854 if literal is None:
855 return tag
857 while 1:
858 # Wait for continuation response
860 while self._get_response():
861 if self.tagged_commands[tag]: # BAD/NO?
862 return tag
864 # Send literal
866 if literator:
867 literal = literator(self.continuation_response)
869 if __debug__:
870 if self.debug >= 4:
871 self._mesg('write literal size %s' % len(literal))
873 try:
874 self.send(literal)
875 self.send(CRLF)
876 except (socket.error, OSError), val:
877 raise self.abort('socket error: %s' % val)
879 if not literator:
880 break
882 return tag
885 def _command_complete(self, name, tag):
886 self._check_bye()
887 try:
888 typ, data = self._get_tagged_response(tag)
889 except self.abort, val:
890 raise self.abort('command: %s => %s' % (name, val))
891 except self.error, val:
892 raise self.error('command: %s => %s' % (name, val))
893 self._check_bye()
894 if typ == 'BAD':
895 raise self.error('%s command error: %s %s' % (name, typ, data))
896 return typ, data
899 def _get_response(self):
901 # Read response and store.
903 # Returns None for continuation responses,
904 # otherwise first response line received.
906 resp = self._get_line()
908 # Command completion response?
910 if self._match(self.tagre, resp):
911 tag = self.mo.group('tag')
912 if not tag in self.tagged_commands:
913 raise self.abort('unexpected tagged response: %s' % resp)
915 typ = self.mo.group('type')
916 dat = self.mo.group('data')
917 self.tagged_commands[tag] = (typ, [dat])
918 else:
919 dat2 = None
921 # '*' (untagged) responses?
923 if not self._match(Untagged_response, resp):
924 if self._match(Untagged_status, resp):
925 dat2 = self.mo.group('data2')
927 if self.mo is None:
928 # Only other possibility is '+' (continuation) response...
930 if self._match(Continuation, resp):
931 self.continuation_response = self.mo.group('data')
932 return None # NB: indicates continuation
934 raise self.abort("unexpected response: '%s'" % resp)
936 typ = self.mo.group('type')
937 dat = self.mo.group('data')
938 if dat is None: dat = '' # Null untagged response
939 if dat2: dat = dat + ' ' + dat2
941 # Is there a literal to come?
943 while self._match(Literal, dat):
945 # Read literal direct from connection.
947 size = int(self.mo.group('size'))
948 if __debug__:
949 if self.debug >= 4:
950 self._mesg('read literal size %s' % size)
951 data = self.read(size)
953 # Store response with literal as tuple
955 self._append_untagged(typ, (dat, data))
957 # Read trailer - possibly containing another literal
959 dat = self._get_line()
961 self._append_untagged(typ, dat)
963 # Bracketed response information?
965 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
966 self._append_untagged(self.mo.group('type'), self.mo.group('data'))
968 if __debug__:
969 if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
970 self._mesg('%s response: %s' % (typ, dat))
972 return resp
975 def _get_tagged_response(self, tag):
977 while 1:
978 result = self.tagged_commands[tag]
979 if result is not None:
980 del self.tagged_commands[tag]
981 return result
983 # Some have reported "unexpected response" exceptions.
984 # Note that ignoring them here causes loops.
985 # Instead, send me details of the unexpected response and
986 # I'll update the code in `_get_response()'.
988 try:
989 self._get_response()
990 except self.abort, val:
991 if __debug__:
992 if self.debug >= 1:
993 self.print_log()
994 raise
997 def _get_line(self):
999 line = self.readline()
1000 if not line:
1001 raise self.abort('socket error: EOF')
1003 # Protocol mandates all lines terminated by CRLF
1005 line = line[:-2]
1006 if __debug__:
1007 if self.debug >= 4:
1008 self._mesg('< %s' % line)
1009 else:
1010 self._log('< %s' % line)
1011 return line
1014 def _match(self, cre, s):
1016 # Run compiled regular expression match method on 's'.
1017 # Save result, return success.
1019 self.mo = cre.match(s)
1020 if __debug__:
1021 if self.mo is not None and self.debug >= 5:
1022 self._mesg("\tmatched r'%s' => %r" % (cre.pattern, self.mo.groups()))
1023 return self.mo is not None
1026 def _new_tag(self):
1028 tag = '%s%s' % (self.tagpre, self.tagnum)
1029 self.tagnum = self.tagnum + 1
1030 self.tagged_commands[tag] = None
1031 return tag
1034 def _checkquote(self, arg):
1036 # Must quote command args if non-alphanumeric chars present,
1037 # and not already quoted.
1039 if type(arg) is not type(''):
1040 return arg
1041 if len(arg) >= 2 and (arg[0],arg[-1]) in (('(',')'),('"','"')):
1042 return arg
1043 if arg and self.mustquote.search(arg) is None:
1044 return arg
1045 return self._quote(arg)
1048 def _quote(self, arg):
1050 arg = arg.replace('\\', '\\\\')
1051 arg = arg.replace('"', '\\"')
1053 return '"%s"' % arg
1056 def _simple_command(self, name, *args):
1058 return self._command_complete(name, self._command(name, *args))
1061 def _untagged_response(self, typ, dat, name):
1063 if typ == 'NO':
1064 return typ, dat
1065 if not name in self.untagged_responses:
1066 return typ, [None]
1067 data = self.untagged_responses.pop(name)
1068 if __debug__:
1069 if self.debug >= 5:
1070 self._mesg('untagged_responses[%s] => %s' % (name, data))
1071 return typ, data
1074 if __debug__:
1076 def _mesg(self, s, secs=None):
1077 if secs is None:
1078 secs = time.time()
1079 tm = time.strftime('%M:%S', time.localtime(secs))
1080 sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
1081 sys.stderr.flush()
1083 def _dump_ur(self, dict):
1084 # Dump untagged responses (in `dict').
1085 l = dict.items()
1086 if not l: return
1087 t = '\n\t\t'
1088 l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l)
1089 self._mesg('untagged responses dump:%s%s' % (t, t.join(l)))
1091 def _log(self, line):
1092 # Keep log of last `_cmd_log_len' interactions for debugging.
1093 self._cmd_log[self._cmd_log_idx] = (line, time.time())
1094 self._cmd_log_idx += 1
1095 if self._cmd_log_idx >= self._cmd_log_len:
1096 self._cmd_log_idx = 0
1098 def print_log(self):
1099 self._mesg('last %d IMAP4 interactions:' % len(self._cmd_log))
1100 i, n = self._cmd_log_idx, self._cmd_log_len
1101 while n:
1102 try:
1103 self._mesg(*self._cmd_log[i])
1104 except:
1105 pass
1106 i += 1
1107 if i >= self._cmd_log_len:
1108 i = 0
1109 n -= 1
1113 try:
1114 import ssl
1115 except ImportError:
1116 pass
1117 else:
1118 class IMAP4_SSL(IMAP4):
1120 """IMAP4 client class over SSL connection
1122 Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile]]]])
1124 host - host's name (default: localhost);
1125 port - port number (default: standard IMAP4 SSL port).
1126 keyfile - PEM formatted file that contains your private key (default: None);
1127 certfile - PEM formatted certificate chain file (default: None);
1129 for more documentation see the docstring of the parent class IMAP4.
1133 def __init__(self, host = '', port = IMAP4_SSL_PORT, keyfile = None, certfile = None):
1134 self.keyfile = keyfile
1135 self.certfile = certfile
1136 IMAP4.__init__(self, host, port)
1139 def open(self, host = '', port = IMAP4_SSL_PORT):
1140 """Setup connection to remote server on "host:port".
1141 (default: localhost:standard IMAP4 SSL port).
1142 This connection will be used by the routines:
1143 read, readline, send, shutdown.
1145 self.host = host
1146 self.port = port
1147 self.sock = socket.create_connection((host, port))
1148 self.sslobj = ssl.wrap_socket(self.sock, self.keyfile, self.certfile)
1151 def read(self, size):
1152 """Read 'size' bytes from remote."""
1153 # sslobj.read() sometimes returns < size bytes
1154 chunks = []
1155 read = 0
1156 while read < size:
1157 data = self.sslobj.read(min(size-read, 16384))
1158 read += len(data)
1159 chunks.append(data)
1161 return ''.join(chunks)
1164 def readline(self):
1165 """Read line from remote."""
1166 line = []
1167 while 1:
1168 char = self.sslobj.read(1)
1169 line.append(char)
1170 if char == "\n": return ''.join(line)
1173 def send(self, data):
1174 """Send data to remote."""
1175 bytes = len(data)
1176 while bytes > 0:
1177 sent = self.sslobj.write(data)
1178 if sent == bytes:
1179 break # avoid copy
1180 data = data[sent:]
1181 bytes = bytes - sent
1184 def shutdown(self):
1185 """Close I/O established in "open"."""
1186 self.sock.close()
1189 def socket(self):
1190 """Return socket instance used to connect to IMAP4 server.
1192 socket = <instance>.socket()
1194 return self.sock
1197 def ssl(self):
1198 """Return SSLObject instance used to communicate with the IMAP4 server.
1200 ssl = ssl.wrap_socket(<instance>.socket)
1202 return self.sslobj
1204 __all__.append("IMAP4_SSL")
1207 class IMAP4_stream(IMAP4):
1209 """IMAP4 client class over a stream
1211 Instantiate with: IMAP4_stream(command)
1213 where "command" is a string that can be passed to os.popen2()
1215 for more documentation see the docstring of the parent class IMAP4.
1219 def __init__(self, command):
1220 self.command = command
1221 IMAP4.__init__(self)
1224 def open(self, host = None, port = None):
1225 """Setup a stream connection.
1226 This connection will be used by the routines:
1227 read, readline, send, shutdown.
1229 self.host = None # For compatibility with parent class
1230 self.port = None
1231 self.sock = None
1232 self.file = None
1233 self.writefile, self.readfile = os.popen2(self.command)
1236 def read(self, size):
1237 """Read 'size' bytes from remote."""
1238 return self.readfile.read(size)
1241 def readline(self):
1242 """Read line from remote."""
1243 return self.readfile.readline()
1246 def send(self, data):
1247 """Send data to remote."""
1248 self.writefile.write(data)
1249 self.writefile.flush()
1252 def shutdown(self):
1253 """Close I/O established in "open"."""
1254 self.readfile.close()
1255 self.writefile.close()
1259 class _Authenticator:
1261 """Private class to provide en/decoding
1262 for base64-based authentication conversation.
1265 def __init__(self, mechinst):
1266 self.mech = mechinst # Callable object to provide/process data
1268 def process(self, data):
1269 ret = self.mech(self.decode(data))
1270 if ret is None:
1271 return '*' # Abort conversation
1272 return self.encode(ret)
1274 def encode(self, inp):
1276 # Invoke binascii.b2a_base64 iteratively with
1277 # short even length buffers, strip the trailing
1278 # line feed from the result and append. "Even"
1279 # means a number that factors to both 6 and 8,
1280 # so when it gets to the end of the 8-bit input
1281 # there's no partial 6-bit output.
1283 oup = ''
1284 while inp:
1285 if len(inp) > 48:
1286 t = inp[:48]
1287 inp = inp[48:]
1288 else:
1289 t = inp
1290 inp = ''
1291 e = binascii.b2a_base64(t)
1292 if e:
1293 oup = oup + e[:-1]
1294 return oup
1296 def decode(self, inp):
1297 if not inp:
1298 return ''
1299 return binascii.a2b_base64(inp)
1303 Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
1304 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
1306 def Internaldate2tuple(resp):
1307 """Convert IMAP4 INTERNALDATE to UT.
1309 Returns Python time module tuple.
1312 mo = InternalDate.match(resp)
1313 if not mo:
1314 return None
1316 mon = Mon2num[mo.group('mon')]
1317 zonen = mo.group('zonen')
1319 day = int(mo.group('day'))
1320 year = int(mo.group('year'))
1321 hour = int(mo.group('hour'))
1322 min = int(mo.group('min'))
1323 sec = int(mo.group('sec'))
1324 zoneh = int(mo.group('zoneh'))
1325 zonem = int(mo.group('zonem'))
1327 # INTERNALDATE timezone must be subtracted to get UT
1329 zone = (zoneh*60 + zonem)*60
1330 if zonen == '-':
1331 zone = -zone
1333 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
1335 utc = time.mktime(tt)
1337 # Following is necessary because the time module has no 'mkgmtime'.
1338 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
1340 lt = time.localtime(utc)
1341 if time.daylight and lt[-1]:
1342 zone = zone + time.altzone
1343 else:
1344 zone = zone + time.timezone
1346 return time.localtime(utc - zone)
1350 def Int2AP(num):
1352 """Convert integer to A-P string representation."""
1354 val = ''; AP = 'ABCDEFGHIJKLMNOP'
1355 num = int(abs(num))
1356 while num:
1357 num, mod = divmod(num, 16)
1358 val = AP[mod] + val
1359 return val
1363 def ParseFlags(resp):
1365 """Convert IMAP4 flags response to python tuple."""
1367 mo = Flags.match(resp)
1368 if not mo:
1369 return ()
1371 return tuple(mo.group('flags').split())
1374 def Time2Internaldate(date_time):
1376 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
1378 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
1381 if isinstance(date_time, (int, float)):
1382 tt = time.localtime(date_time)
1383 elif isinstance(date_time, (tuple, time.struct_time)):
1384 tt = date_time
1385 elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'):
1386 return date_time # Assume in correct format
1387 else:
1388 raise ValueError("date_time not of a known type")
1390 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
1391 if dt[0] == '0':
1392 dt = ' ' + dt[1:]
1393 if time.daylight and tt[-1]:
1394 zone = -time.altzone
1395 else:
1396 zone = -time.timezone
1397 return '"' + dt + " %+03d%02d" % divmod(zone//60, 60) + '"'
1401 if __name__ == '__main__':
1403 # To test: invoke either as 'python imaplib.py [IMAP4_server_hostname]'
1404 # or 'python imaplib.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"'
1405 # to test the IMAP4_stream class
1407 import getopt, getpass
1409 try:
1410 optlist, args = getopt.getopt(sys.argv[1:], 'd:s:')
1411 except getopt.error, val:
1412 optlist, args = (), ()
1414 stream_command = None
1415 for opt,val in optlist:
1416 if opt == '-d':
1417 Debug = int(val)
1418 elif opt == '-s':
1419 stream_command = val
1420 if not args: args = (stream_command,)
1422 if not args: args = ('',)
1424 host = args[0]
1426 USER = getpass.getuser()
1427 PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost"))
1429 test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER, 'lf':'\n'}
1430 test_seq1 = (
1431 ('login', (USER, PASSWD)),
1432 ('create', ('/tmp/xxx 1',)),
1433 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1434 ('CREATE', ('/tmp/yyz 2',)),
1435 ('append', ('/tmp/yyz 2', None, None, test_mesg)),
1436 ('list', ('/tmp', 'yy*')),
1437 ('select', ('/tmp/yyz 2',)),
1438 ('search', (None, 'SUBJECT', 'test')),
1439 ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
1440 ('store', ('1', 'FLAGS', '(\Deleted)')),
1441 ('namespace', ()),
1442 ('expunge', ()),
1443 ('recent', ()),
1444 ('close', ()),
1447 test_seq2 = (
1448 ('select', ()),
1449 ('response',('UIDVALIDITY',)),
1450 ('uid', ('SEARCH', 'ALL')),
1451 ('response', ('EXISTS',)),
1452 ('append', (None, None, None, test_mesg)),
1453 ('recent', ()),
1454 ('logout', ()),
1457 def run(cmd, args):
1458 M._mesg('%s %s' % (cmd, args))
1459 typ, dat = getattr(M, cmd)(*args)
1460 M._mesg('%s => %s %s' % (cmd, typ, dat))
1461 if typ == 'NO': raise dat[0]
1462 return dat
1464 try:
1465 if stream_command:
1466 M = IMAP4_stream(stream_command)
1467 else:
1468 M = IMAP4(host)
1469 if M.state == 'AUTH':
1470 test_seq1 = test_seq1[1:] # Login not needed
1471 M._mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
1472 M._mesg('CAPABILITIES = %r' % (M.capabilities,))
1474 for cmd,args in test_seq1:
1475 run(cmd, args)
1477 for ml in run('list', ('/tmp/', 'yy%')):
1478 mo = re.match(r'.*"([^"]+)"$', ml)
1479 if mo: path = mo.group(1)
1480 else: path = ml.split()[-1]
1481 run('delete', (path,))
1483 for cmd,args in test_seq2:
1484 dat = run(cmd, args)
1486 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
1487 continue
1489 uid = dat[-1].split()
1490 if not uid: continue
1491 run('uid', ('FETCH', '%s' % uid[-1],
1492 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1494 print '\nAll tests OK.'
1496 except:
1497 print '\nTests failed.'
1499 if not Debug:
1500 print '''
1501 If you would like to see debugging output,
1502 try: %s -d5
1503 ''' % sys.argv[0]
1505 raise