Fix the tag.
[python/dscho.git] / Lib / imaplib.py
blob0d9704e9d72d4a0786c200c407eba998145453d6
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.socket(socket.AF_INET, socket.SOCK_STREAM)
230 self.sock.connect((host, port))
231 self.file = self.sock.makefile('rb')
234 def read(self, size):
235 """Read 'size' bytes from remote."""
236 return self.file.read(size)
239 def readline(self):
240 """Read line from remote."""
241 return self.file.readline()
244 def send(self, data):
245 """Send data to remote."""
246 self.sock.sendall(data)
249 def shutdown(self):
250 """Close I/O established in "open"."""
251 self.file.close()
252 self.sock.close()
255 def socket(self):
256 """Return socket instance used to connect to IMAP4 server.
258 socket = <instance>.socket()
260 return self.sock
264 # Utility methods
267 def recent(self):
268 """Return most recent 'RECENT' responses if any exist,
269 else prompt server for an update using the 'NOOP' command.
271 (typ, [data]) = <instance>.recent()
273 'data' is None if no new messages,
274 else list of RECENT responses, most recent last.
276 name = 'RECENT'
277 typ, dat = self._untagged_response('OK', [None], name)
278 if dat[-1]:
279 return typ, dat
280 typ, dat = self.noop() # Prod server for response
281 return self._untagged_response(typ, dat, name)
284 def response(self, code):
285 """Return data for response 'code' if received, or None.
287 Old value for response 'code' is cleared.
289 (code, [data]) = <instance>.response(code)
291 return self._untagged_response(code, [None], code.upper())
295 # IMAP4 commands
298 def append(self, mailbox, flags, date_time, message):
299 """Append message to named mailbox.
301 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
303 All args except `message' can be None.
305 name = 'APPEND'
306 if not mailbox:
307 mailbox = 'INBOX'
308 if flags:
309 if (flags[0],flags[-1]) != ('(',')'):
310 flags = '(%s)' % flags
311 else:
312 flags = None
313 if date_time:
314 date_time = Time2Internaldate(date_time)
315 else:
316 date_time = None
317 self.literal = MapCRLF.sub(CRLF, message)
318 return self._simple_command(name, mailbox, flags, date_time)
321 def authenticate(self, mechanism, authobject):
322 """Authenticate command - requires response processing.
324 'mechanism' specifies which authentication mechanism is to
325 be used - it must appear in <instance>.capabilities in the
326 form AUTH=<mechanism>.
328 'authobject' must be a callable object:
330 data = authobject(response)
332 It will be called to process server continuation responses.
333 It should return data that will be encoded and sent to server.
334 It should return None if the client abort response '*' should
335 be sent instead.
337 mech = mechanism.upper()
338 # XXX: shouldn't this code be removed, not commented out?
339 #cap = 'AUTH=%s' % mech
340 #if not cap in self.capabilities: # Let the server decide!
341 # raise self.error("Server doesn't allow %s authentication." % mech)
342 self.literal = _Authenticator(authobject).process
343 typ, dat = self._simple_command('AUTHENTICATE', mech)
344 if typ != 'OK':
345 raise self.error(dat[-1])
346 self.state = 'AUTH'
347 return typ, dat
350 def capability(self):
351 """(typ, [data]) = <instance>.capability()
352 Fetch capabilities list from server."""
354 name = 'CAPABILITY'
355 typ, dat = self._simple_command(name)
356 return self._untagged_response(typ, dat, name)
359 def check(self):
360 """Checkpoint mailbox on server.
362 (typ, [data]) = <instance>.check()
364 return self._simple_command('CHECK')
367 def close(self):
368 """Close currently selected mailbox.
370 Deleted messages are removed from writable mailbox.
371 This is the recommended command before 'LOGOUT'.
373 (typ, [data]) = <instance>.close()
375 try:
376 typ, dat = self._simple_command('CLOSE')
377 finally:
378 self.state = 'AUTH'
379 return typ, dat
382 def copy(self, message_set, new_mailbox):
383 """Copy 'message_set' messages onto end of 'new_mailbox'.
385 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
387 return self._simple_command('COPY', message_set, new_mailbox)
390 def create(self, mailbox):
391 """Create new mailbox.
393 (typ, [data]) = <instance>.create(mailbox)
395 return self._simple_command('CREATE', mailbox)
398 def delete(self, mailbox):
399 """Delete old mailbox.
401 (typ, [data]) = <instance>.delete(mailbox)
403 return self._simple_command('DELETE', mailbox)
405 def deleteacl(self, mailbox, who):
406 """Delete the ACLs (remove any rights) set for who on mailbox.
408 (typ, [data]) = <instance>.deleteacl(mailbox, who)
410 return self._simple_command('DELETEACL', mailbox, who)
412 def expunge(self):
413 """Permanently remove deleted items from selected mailbox.
415 Generates 'EXPUNGE' response for each deleted message.
417 (typ, [data]) = <instance>.expunge()
419 'data' is list of 'EXPUNGE'd message numbers in order received.
421 name = 'EXPUNGE'
422 typ, dat = self._simple_command(name)
423 return self._untagged_response(typ, dat, name)
426 def fetch(self, message_set, message_parts):
427 """Fetch (parts of) messages.
429 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
431 'message_parts' should be a string of selected parts
432 enclosed in parentheses, eg: "(UID BODY[TEXT])".
434 'data' are tuples of message part envelope and data.
436 name = 'FETCH'
437 typ, dat = self._simple_command(name, message_set, message_parts)
438 return self._untagged_response(typ, dat, name)
441 def getacl(self, mailbox):
442 """Get the ACLs for a mailbox.
444 (typ, [data]) = <instance>.getacl(mailbox)
446 typ, dat = self._simple_command('GETACL', mailbox)
447 return self._untagged_response(typ, dat, 'ACL')
450 def getannotation(self, mailbox, entry, attribute):
451 """(typ, [data]) = <instance>.getannotation(mailbox, entry, attribute)
452 Retrieve ANNOTATIONs."""
454 typ, dat = self._simple_command('GETANNOTATION', mailbox, entry, attribute)
455 return self._untagged_response(typ, dat, 'ANNOTATION')
458 def getquota(self, root):
459 """Get the quota root's resource usage and limits.
461 Part of the IMAP4 QUOTA extension defined in rfc2087.
463 (typ, [data]) = <instance>.getquota(root)
465 typ, dat = self._simple_command('GETQUOTA', root)
466 return self._untagged_response(typ, dat, 'QUOTA')
469 def getquotaroot(self, mailbox):
470 """Get the list of quota roots for the named mailbox.
472 (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox)
474 typ, dat = self._simple_command('GETQUOTAROOT', mailbox)
475 typ, quota = self._untagged_response(typ, dat, 'QUOTA')
476 typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT')
477 return typ, [quotaroot, quota]
480 def list(self, directory='""', pattern='*'):
481 """List mailbox names in directory matching pattern.
483 (typ, [data]) = <instance>.list(directory='""', pattern='*')
485 'data' is list of LIST responses.
487 name = 'LIST'
488 typ, dat = self._simple_command(name, directory, pattern)
489 return self._untagged_response(typ, dat, name)
492 def login(self, user, password):
493 """Identify client using plaintext password.
495 (typ, [data]) = <instance>.login(user, password)
497 NB: 'password' will be quoted.
499 typ, dat = self._simple_command('LOGIN', user, self._quote(password))
500 if typ != 'OK':
501 raise self.error(dat[-1])
502 self.state = 'AUTH'
503 return typ, dat
506 def login_cram_md5(self, user, password):
507 """ Force use of CRAM-MD5 authentication.
509 (typ, [data]) = <instance>.login_cram_md5(user, password)
511 self.user, self.password = user, password
512 return self.authenticate('CRAM-MD5', self._CRAM_MD5_AUTH)
515 def _CRAM_MD5_AUTH(self, challenge):
516 """ Authobject to use with CRAM-MD5 authentication. """
517 import hmac
518 return self.user + " " + hmac.HMAC(self.password, challenge).hexdigest()
521 def logout(self):
522 """Shutdown connection to server.
524 (typ, [data]) = <instance>.logout()
526 Returns server 'BYE' response.
528 self.state = 'LOGOUT'
529 try: typ, dat = self._simple_command('LOGOUT')
530 except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
531 self.shutdown()
532 if 'BYE' in self.untagged_responses:
533 return 'BYE', self.untagged_responses['BYE']
534 return typ, dat
537 def lsub(self, directory='""', pattern='*'):
538 """List 'subscribed' mailbox names in directory matching pattern.
540 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
542 'data' are tuples of message part envelope and data.
544 name = 'LSUB'
545 typ, dat = self._simple_command(name, directory, pattern)
546 return self._untagged_response(typ, dat, name)
548 def myrights(self, mailbox):
549 """Show my ACLs for a mailbox (i.e. the rights that I have on mailbox).
551 (typ, [data]) = <instance>.myrights(mailbox)
553 typ,dat = self._simple_command('MYRIGHTS', mailbox)
554 return self._untagged_response(typ, dat, 'MYRIGHTS')
556 def namespace(self):
557 """ Returns IMAP namespaces ala rfc2342
559 (typ, [data, ...]) = <instance>.namespace()
561 name = 'NAMESPACE'
562 typ, dat = self._simple_command(name)
563 return self._untagged_response(typ, dat, name)
566 def noop(self):
567 """Send NOOP command.
569 (typ, [data]) = <instance>.noop()
571 if __debug__:
572 if self.debug >= 3:
573 self._dump_ur(self.untagged_responses)
574 return self._simple_command('NOOP')
577 def partial(self, message_num, message_part, start, length):
578 """Fetch truncated part of a message.
580 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
582 'data' is tuple of message part envelope and data.
584 name = 'PARTIAL'
585 typ, dat = self._simple_command(name, message_num, message_part, start, length)
586 return self._untagged_response(typ, dat, 'FETCH')
589 def proxyauth(self, user):
590 """Assume authentication as "user".
592 Allows an authorised administrator to proxy into any user's
593 mailbox.
595 (typ, [data]) = <instance>.proxyauth(user)
598 name = 'PROXYAUTH'
599 return self._simple_command('PROXYAUTH', user)
602 def rename(self, oldmailbox, newmailbox):
603 """Rename old mailbox name to new.
605 (typ, [data]) = <instance>.rename(oldmailbox, newmailbox)
607 return self._simple_command('RENAME', oldmailbox, newmailbox)
610 def search(self, charset, *criteria):
611 """Search mailbox for matching messages.
613 (typ, [data]) = <instance>.search(charset, criterion, ...)
615 'data' is space separated list of matching message numbers.
617 name = 'SEARCH'
618 if charset:
619 typ, dat = self._simple_command(name, 'CHARSET', charset, *criteria)
620 else:
621 typ, dat = self._simple_command(name, *criteria)
622 return self._untagged_response(typ, dat, name)
625 def select(self, mailbox='INBOX', readonly=False):
626 """Select a mailbox.
628 Flush all untagged responses.
630 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=False)
632 'data' is count of messages in mailbox ('EXISTS' response).
634 Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
635 other responses should be obtained via <instance>.response('FLAGS') etc.
637 self.untagged_responses = {} # Flush old responses.
638 self.is_readonly = readonly
639 if readonly:
640 name = 'EXAMINE'
641 else:
642 name = 'SELECT'
643 typ, dat = self._simple_command(name, mailbox)
644 if typ != 'OK':
645 self.state = 'AUTH' # Might have been 'SELECTED'
646 return typ, dat
647 self.state = 'SELECTED'
648 if 'READ-ONLY' in self.untagged_responses \
649 and not readonly:
650 if __debug__:
651 if self.debug >= 1:
652 self._dump_ur(self.untagged_responses)
653 raise self.readonly('%s is not writable' % mailbox)
654 return typ, self.untagged_responses.get('EXISTS', [None])
657 def setacl(self, mailbox, who, what):
658 """Set a mailbox acl.
660 (typ, [data]) = <instance>.setacl(mailbox, who, what)
662 return self._simple_command('SETACL', mailbox, who, what)
665 def setannotation(self, *args):
666 """(typ, [data]) = <instance>.setannotation(mailbox[, entry, attribute]+)
667 Set ANNOTATIONs."""
669 typ, dat = self._simple_command('SETANNOTATION', *args)
670 return self._untagged_response(typ, dat, 'ANNOTATION')
673 def setquota(self, root, limits):
674 """Set the quota root's resource limits.
676 (typ, [data]) = <instance>.setquota(root, limits)
678 typ, dat = self._simple_command('SETQUOTA', root, limits)
679 return self._untagged_response(typ, dat, 'QUOTA')
682 def sort(self, sort_criteria, charset, *search_criteria):
683 """IMAP4rev1 extension SORT command.
685 (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
687 name = 'SORT'
688 #if not name in self.capabilities: # Let the server decide!
689 # raise self.error('unimplemented extension command: %s' % name)
690 if (sort_criteria[0],sort_criteria[-1]) != ('(',')'):
691 sort_criteria = '(%s)' % sort_criteria
692 typ, dat = self._simple_command(name, sort_criteria, charset, *search_criteria)
693 return self._untagged_response(typ, dat, name)
696 def status(self, mailbox, names):
697 """Request named status conditions for mailbox.
699 (typ, [data]) = <instance>.status(mailbox, names)
701 name = 'STATUS'
702 #if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide!
703 # raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
704 typ, dat = self._simple_command(name, mailbox, names)
705 return self._untagged_response(typ, dat, name)
708 def store(self, message_set, command, flags):
709 """Alters flag dispositions for messages in mailbox.
711 (typ, [data]) = <instance>.store(message_set, command, flags)
713 if (flags[0],flags[-1]) != ('(',')'):
714 flags = '(%s)' % flags # Avoid quoting the flags
715 typ, dat = self._simple_command('STORE', message_set, command, flags)
716 return self._untagged_response(typ, dat, 'FETCH')
719 def subscribe(self, mailbox):
720 """Subscribe to new mailbox.
722 (typ, [data]) = <instance>.subscribe(mailbox)
724 return self._simple_command('SUBSCRIBE', mailbox)
727 def thread(self, threading_algorithm, charset, *search_criteria):
728 """IMAPrev1 extension THREAD command.
730 (type, [data]) = <instance>.thread(threading_alogrithm, charset, search_criteria, ...)
732 name = 'THREAD'
733 typ, dat = self._simple_command(name, threading_algorithm, charset, *search_criteria)
734 return self._untagged_response(typ, dat, name)
737 def uid(self, command, *args):
738 """Execute "command arg ..." with messages identified by UID,
739 rather than message number.
741 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
743 Returns response appropriate to 'command'.
745 command = command.upper()
746 if not command in Commands:
747 raise self.error("Unknown IMAP4 UID command: %s" % command)
748 if self.state not in Commands[command]:
749 raise self.error("command %s illegal in state %s, "
750 "only allowed in states %s" %
751 (command, self.state,
752 ', '.join(Commands[command])))
753 name = 'UID'
754 typ, dat = self._simple_command(name, command, *args)
755 if command in ('SEARCH', 'SORT'):
756 name = command
757 else:
758 name = 'FETCH'
759 return self._untagged_response(typ, dat, name)
762 def unsubscribe(self, mailbox):
763 """Unsubscribe from old mailbox.
765 (typ, [data]) = <instance>.unsubscribe(mailbox)
767 return self._simple_command('UNSUBSCRIBE', mailbox)
770 def xatom(self, name, *args):
771 """Allow simple extension commands
772 notified by server in CAPABILITY response.
774 Assumes command is legal in current state.
776 (typ, [data]) = <instance>.xatom(name, arg, ...)
778 Returns response appropriate to extension command `name'.
780 name = name.upper()
781 #if not name in self.capabilities: # Let the server decide!
782 # raise self.error('unknown extension command: %s' % name)
783 if not name in Commands:
784 Commands[name] = (self.state,)
785 return self._simple_command(name, *args)
789 # Private methods
792 def _append_untagged(self, typ, dat):
794 if dat is None: dat = ''
795 ur = self.untagged_responses
796 if __debug__:
797 if self.debug >= 5:
798 self._mesg('untagged_responses[%s] %s += ["%s"]' %
799 (typ, len(ur.get(typ,'')), dat))
800 if typ in ur:
801 ur[typ].append(dat)
802 else:
803 ur[typ] = [dat]
806 def _check_bye(self):
807 bye = self.untagged_responses.get('BYE')
808 if bye:
809 raise self.abort(bye[-1])
812 def _command(self, name, *args):
814 if self.state not in Commands[name]:
815 self.literal = None
816 raise self.error("command %s illegal in state %s, "
817 "only allowed in states %s" %
818 (name, self.state,
819 ', '.join(Commands[name])))
821 for typ in ('OK', 'NO', 'BAD'):
822 if typ in self.untagged_responses:
823 del self.untagged_responses[typ]
825 if 'READ-ONLY' in self.untagged_responses \
826 and not self.is_readonly:
827 raise self.readonly('mailbox status changed to READ-ONLY')
829 tag = self._new_tag()
830 data = '%s %s' % (tag, name)
831 for arg in args:
832 if arg is None: continue
833 data = '%s %s' % (data, self._checkquote(arg))
835 literal = self.literal
836 if literal is not None:
837 self.literal = None
838 if type(literal) is type(self._command):
839 literator = literal
840 else:
841 literator = None
842 data = '%s {%s}' % (data, len(literal))
844 if __debug__:
845 if self.debug >= 4:
846 self._mesg('> %s' % data)
847 else:
848 self._log('> %s' % data)
850 try:
851 self.send('%s%s' % (data, CRLF))
852 except (socket.error, OSError) as val:
853 raise self.abort('socket error: %s' % val)
855 if literal is None:
856 return tag
858 while 1:
859 # Wait for continuation response
861 while self._get_response():
862 if self.tagged_commands[tag]: # BAD/NO?
863 return tag
865 # Send literal
867 if literator:
868 literal = literator(self.continuation_response)
870 if __debug__:
871 if self.debug >= 4:
872 self._mesg('write literal size %s' % len(literal))
874 try:
875 self.send(literal)
876 self.send(CRLF)
877 except (socket.error, OSError) as val:
878 raise self.abort('socket error: %s' % val)
880 if not literator:
881 break
883 return tag
886 def _command_complete(self, name, tag):
887 self._check_bye()
888 try:
889 typ, data = self._get_tagged_response(tag)
890 except self.abort as val:
891 raise self.abort('command: %s => %s' % (name, val))
892 except self.error as val:
893 raise self.error('command: %s => %s' % (name, val))
894 self._check_bye()
895 if typ == 'BAD':
896 raise self.error('%s command error: %s %s' % (name, typ, data))
897 return typ, data
900 def _get_response(self):
902 # Read response and store.
904 # Returns None for continuation responses,
905 # otherwise first response line received.
907 resp = self._get_line()
909 # Command completion response?
911 if self._match(self.tagre, resp):
912 tag = self.mo.group('tag')
913 if not tag in self.tagged_commands:
914 raise self.abort('unexpected tagged response: %s' % resp)
916 typ = self.mo.group('type')
917 dat = self.mo.group('data')
918 self.tagged_commands[tag] = (typ, [dat])
919 else:
920 dat2 = None
922 # '*' (untagged) responses?
924 if not self._match(Untagged_response, resp):
925 if self._match(Untagged_status, resp):
926 dat2 = self.mo.group('data2')
928 if self.mo is None:
929 # Only other possibility is '+' (continuation) response...
931 if self._match(Continuation, resp):
932 self.continuation_response = self.mo.group('data')
933 return None # NB: indicates continuation
935 raise self.abort("unexpected response: '%s'" % resp)
937 typ = self.mo.group('type')
938 dat = self.mo.group('data')
939 if dat is None: dat = '' # Null untagged response
940 if dat2: dat = dat + ' ' + dat2
942 # Is there a literal to come?
944 while self._match(Literal, dat):
946 # Read literal direct from connection.
948 size = int(self.mo.group('size'))
949 if __debug__:
950 if self.debug >= 4:
951 self._mesg('read literal size %s' % size)
952 data = self.read(size)
954 # Store response with literal as tuple
956 self._append_untagged(typ, (dat, data))
958 # Read trailer - possibly containing another literal
960 dat = self._get_line()
962 self._append_untagged(typ, dat)
964 # Bracketed response information?
966 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
967 self._append_untagged(self.mo.group('type'), self.mo.group('data'))
969 if __debug__:
970 if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
971 self._mesg('%s response: %s' % (typ, dat))
973 return resp
976 def _get_tagged_response(self, tag):
978 while 1:
979 result = self.tagged_commands[tag]
980 if result is not None:
981 del self.tagged_commands[tag]
982 return result
984 # Some have reported "unexpected response" exceptions.
985 # Note that ignoring them here causes loops.
986 # Instead, send me details of the unexpected response and
987 # I'll update the code in `_get_response()'.
989 try:
990 self._get_response()
991 except self.abort as val:
992 if __debug__:
993 if self.debug >= 1:
994 self.print_log()
995 raise
998 def _get_line(self):
1000 line = self.readline()
1001 if not line:
1002 raise self.abort('socket error: EOF')
1004 # Protocol mandates all lines terminated by CRLF
1006 line = line[:-2]
1007 if __debug__:
1008 if self.debug >= 4:
1009 self._mesg('< %s' % line)
1010 else:
1011 self._log('< %s' % line)
1012 return line
1015 def _match(self, cre, s):
1017 # Run compiled regular expression match method on 's'.
1018 # Save result, return success.
1020 self.mo = cre.match(s)
1021 if __debug__:
1022 if self.mo is not None and self.debug >= 5:
1023 self._mesg("\tmatched r'%s' => %r" % (cre.pattern, self.mo.groups()))
1024 return self.mo is not None
1027 def _new_tag(self):
1029 tag = '%s%s' % (self.tagpre, self.tagnum)
1030 self.tagnum = self.tagnum + 1
1031 self.tagged_commands[tag] = None
1032 return tag
1035 def _checkquote(self, arg):
1037 # Must quote command args if non-alphanumeric chars present,
1038 # and not already quoted.
1040 if type(arg) is not type(''):
1041 return arg
1042 if len(arg) >= 2 and (arg[0],arg[-1]) in (('(',')'),('"','"')):
1043 return arg
1044 if arg and self.mustquote.search(arg) is None:
1045 return arg
1046 return self._quote(arg)
1049 def _quote(self, arg):
1051 arg = arg.replace('\\', '\\\\')
1052 arg = arg.replace('"', '\\"')
1054 return '"%s"' % arg
1057 def _simple_command(self, name, *args):
1059 return self._command_complete(name, self._command(name, *args))
1062 def _untagged_response(self, typ, dat, name):
1064 if typ == 'NO':
1065 return typ, dat
1066 if not name in self.untagged_responses:
1067 return typ, [None]
1068 data = self.untagged_responses.pop(name)
1069 if __debug__:
1070 if self.debug >= 5:
1071 self._mesg('untagged_responses[%s] => %s' % (name, data))
1072 return typ, data
1075 if __debug__:
1077 def _mesg(self, s, secs=None):
1078 if secs is None:
1079 secs = time.time()
1080 tm = time.strftime('%M:%S', time.localtime(secs))
1081 sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
1082 sys.stderr.flush()
1084 def _dump_ur(self, dict):
1085 # Dump untagged responses (in `dict').
1086 l = dict.items()
1087 if not l: return
1088 t = '\n\t\t'
1089 l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l)
1090 self._mesg('untagged responses dump:%s%s' % (t, t.join(l)))
1092 def _log(self, line):
1093 # Keep log of last `_cmd_log_len' interactions for debugging.
1094 self._cmd_log[self._cmd_log_idx] = (line, time.time())
1095 self._cmd_log_idx += 1
1096 if self._cmd_log_idx >= self._cmd_log_len:
1097 self._cmd_log_idx = 0
1099 def print_log(self):
1100 self._mesg('last %d IMAP4 interactions:' % len(self._cmd_log))
1101 i, n = self._cmd_log_idx, self._cmd_log_len
1102 while n:
1103 try:
1104 self._mesg(*self._cmd_log[i])
1105 except:
1106 pass
1107 i += 1
1108 if i >= self._cmd_log_len:
1109 i = 0
1110 n -= 1
1114 try:
1115 import ssl
1116 except ImportError:
1117 pass
1118 else:
1119 class IMAP4_SSL(IMAP4):
1121 """IMAP4 client class over SSL connection
1123 Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile]]]])
1125 host - host's name (default: localhost);
1126 port - port number (default: standard IMAP4 SSL port).
1127 keyfile - PEM formatted file that contains your private key (default: None);
1128 certfile - PEM formatted certificate chain file (default: None);
1130 for more documentation see the docstring of the parent class IMAP4.
1134 def __init__(self, host = '', port = IMAP4_SSL_PORT, keyfile = None, certfile = None):
1135 self.keyfile = keyfile
1136 self.certfile = certfile
1137 IMAP4.__init__(self, host, port)
1140 def open(self, host = '', port = IMAP4_SSL_PORT):
1141 """Setup connection to remote server on "host:port".
1142 (default: localhost:standard IMAP4 SSL port).
1143 This connection will be used by the routines:
1144 read, readline, send, shutdown.
1146 self.host = host
1147 self.port = port
1148 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1149 sock.connect((host, port))
1150 self.sock = ssl.wrap_socket(sock, self.keyfile, self.certfile)
1151 self.file = self.sock.makefile('rb')
1154 def read(self, size):
1155 """Read 'size' bytes from remote."""
1156 # sslobj.read() sometimes returns < size bytes
1157 chunks = []
1158 read = 0
1159 while read < size:
1160 data = self.sslobj.read(min(size-read, 16384))
1161 read += len(data)
1162 chunks.append(data)
1164 return b''.join(chunks)
1167 def readline(self):
1168 """Read line from remote."""
1169 line = []
1170 while 1:
1171 char = self.sslobj.read(1)
1172 line.append(char)
1173 if char == b"\n": return b''.join(line)
1176 def send(self, data):
1177 """Send data to remote."""
1178 bytes = len(data)
1179 while bytes > 0:
1180 sent = self.sslobj.write(data)
1181 if sent == bytes:
1182 break # avoid copy
1183 data = data[sent:]
1184 bytes = bytes - sent
1187 def shutdown(self):
1188 """Close I/O established in "open"."""
1189 self.sock.close()
1192 def socket(self):
1193 """Return socket instance used to connect to IMAP4 server.
1195 socket = <instance>.socket()
1197 return self.sock
1200 def ssl(self):
1201 """Return SSLObject instance used to communicate with the IMAP4 server.
1203 ssl = ssl.wrap_socket(<instance>.socket)
1205 return self.sock
1207 __all__.append("IMAP4_SSL")
1210 class IMAP4_stream(IMAP4):
1212 """IMAP4 client class over a stream
1214 Instantiate with: IMAP4_stream(command)
1216 where "command" is a string that can be passed to os.popen2()
1218 for more documentation see the docstring of the parent class IMAP4.
1222 def __init__(self, command):
1223 self.command = command
1224 IMAP4.__init__(self)
1227 def open(self, host = None, port = None):
1228 """Setup a stream connection.
1229 This connection will be used by the routines:
1230 read, readline, send, shutdown.
1232 self.host = None # For compatibility with parent class
1233 self.port = None
1234 self.sock = None
1235 self.file = None
1236 self.writefile, self.readfile = os.popen2(self.command)
1239 def read(self, size):
1240 """Read 'size' bytes from remote."""
1241 return self.readfile.read(size)
1244 def readline(self):
1245 """Read line from remote."""
1246 return self.readfile.readline()
1249 def send(self, data):
1250 """Send data to remote."""
1251 self.writefile.write(data)
1252 self.writefile.flush()
1255 def shutdown(self):
1256 """Close I/O established in "open"."""
1257 self.readfile.close()
1258 self.writefile.close()
1262 class _Authenticator:
1264 """Private class to provide en/decoding
1265 for base64-based authentication conversation.
1268 def __init__(self, mechinst):
1269 self.mech = mechinst # Callable object to provide/process data
1271 def process(self, data):
1272 ret = self.mech(self.decode(data))
1273 if ret is None:
1274 return '*' # Abort conversation
1275 return self.encode(ret)
1277 def encode(self, inp):
1279 # Invoke binascii.b2a_base64 iteratively with
1280 # short even length buffers, strip the trailing
1281 # line feed from the result and append. "Even"
1282 # means a number that factors to both 6 and 8,
1283 # so when it gets to the end of the 8-bit input
1284 # there's no partial 6-bit output.
1286 oup = ''
1287 while inp:
1288 if len(inp) > 48:
1289 t = inp[:48]
1290 inp = inp[48:]
1291 else:
1292 t = inp
1293 inp = ''
1294 e = binascii.b2a_base64(t)
1295 if e:
1296 oup = oup + e[:-1]
1297 return oup
1299 def decode(self, inp):
1300 if not inp:
1301 return ''
1302 return binascii.a2b_base64(inp)
1306 Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
1307 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
1309 def Internaldate2tuple(resp):
1310 """Convert IMAP4 INTERNALDATE to UT.
1312 Returns Python time module tuple.
1315 mo = InternalDate.match(resp)
1316 if not mo:
1317 return None
1319 mon = Mon2num[mo.group('mon')]
1320 zonen = mo.group('zonen')
1322 day = int(mo.group('day'))
1323 year = int(mo.group('year'))
1324 hour = int(mo.group('hour'))
1325 min = int(mo.group('min'))
1326 sec = int(mo.group('sec'))
1327 zoneh = int(mo.group('zoneh'))
1328 zonem = int(mo.group('zonem'))
1330 # INTERNALDATE timezone must be subtracted to get UT
1332 zone = (zoneh*60 + zonem)*60
1333 if zonen == '-':
1334 zone = -zone
1336 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
1338 utc = time.mktime(tt)
1340 # Following is necessary because the time module has no 'mkgmtime'.
1341 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
1343 lt = time.localtime(utc)
1344 if time.daylight and lt[-1]:
1345 zone = zone + time.altzone
1346 else:
1347 zone = zone + time.timezone
1349 return time.localtime(utc - zone)
1353 def Int2AP(num):
1355 """Convert integer to A-P string representation."""
1357 val = ''; AP = 'ABCDEFGHIJKLMNOP'
1358 num = int(abs(num))
1359 while num:
1360 num, mod = divmod(num, 16)
1361 val = AP[mod] + val
1362 return val
1366 def ParseFlags(resp):
1368 """Convert IMAP4 flags response to python tuple."""
1370 mo = Flags.match(resp)
1371 if not mo:
1372 return ()
1374 return tuple(mo.group('flags').split())
1377 def Time2Internaldate(date_time):
1379 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
1381 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
1384 if isinstance(date_time, (int, float)):
1385 tt = time.localtime(date_time)
1386 elif isinstance(date_time, (tuple, time.struct_time)):
1387 tt = date_time
1388 elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'):
1389 return date_time # Assume in correct format
1390 else:
1391 raise ValueError("date_time not of a known type")
1393 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
1394 if dt[0] == '0':
1395 dt = ' ' + dt[1:]
1396 if time.daylight and tt[-1]:
1397 zone = -time.altzone
1398 else:
1399 zone = -time.timezone
1400 return '"' + dt + " %+03d%02d" % divmod(zone//60, 60) + '"'
1404 if __name__ == '__main__':
1406 # To test: invoke either as 'python imaplib.py [IMAP4_server_hostname]'
1407 # or 'python imaplib.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"'
1408 # to test the IMAP4_stream class
1410 import getopt, getpass
1412 try:
1413 optlist, args = getopt.getopt(sys.argv[1:], 'd:s:')
1414 except getopt.error as val:
1415 optlist, args = (), ()
1417 stream_command = None
1418 for opt,val in optlist:
1419 if opt == '-d':
1420 Debug = int(val)
1421 elif opt == '-s':
1422 stream_command = val
1423 if not args: args = (stream_command,)
1425 if not args: args = ('',)
1427 host = args[0]
1429 USER = getpass.getuser()
1430 PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost"))
1432 test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER, 'lf':'\n'}
1433 test_seq1 = (
1434 ('login', (USER, PASSWD)),
1435 ('create', ('/tmp/xxx 1',)),
1436 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1437 ('CREATE', ('/tmp/yyz 2',)),
1438 ('append', ('/tmp/yyz 2', None, None, test_mesg)),
1439 ('list', ('/tmp', 'yy*')),
1440 ('select', ('/tmp/yyz 2',)),
1441 ('search', (None, 'SUBJECT', 'test')),
1442 ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
1443 ('store', ('1', 'FLAGS', '(\Deleted)')),
1444 ('namespace', ()),
1445 ('expunge', ()),
1446 ('recent', ()),
1447 ('close', ()),
1450 test_seq2 = (
1451 ('select', ()),
1452 ('response',('UIDVALIDITY',)),
1453 ('uid', ('SEARCH', 'ALL')),
1454 ('response', ('EXISTS',)),
1455 ('append', (None, None, None, test_mesg)),
1456 ('recent', ()),
1457 ('logout', ()),
1460 def run(cmd, args):
1461 M._mesg('%s %s' % (cmd, args))
1462 typ, dat = getattr(M, cmd)(*args)
1463 M._mesg('%s => %s %s' % (cmd, typ, dat))
1464 if typ == 'NO': raise dat[0]
1465 return dat
1467 try:
1468 if stream_command:
1469 M = IMAP4_stream(stream_command)
1470 else:
1471 M = IMAP4(host)
1472 if M.state == 'AUTH':
1473 test_seq1 = test_seq1[1:] # Login not needed
1474 M._mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
1475 M._mesg('CAPABILITIES = %r' % (M.capabilities,))
1477 for cmd,args in test_seq1:
1478 run(cmd, args)
1480 for ml in run('list', ('/tmp/', 'yy%')):
1481 mo = re.match(r'.*"([^"]+)"$', ml)
1482 if mo: path = mo.group(1)
1483 else: path = ml.split()[-1]
1484 run('delete', (path,))
1486 for cmd,args in test_seq2:
1487 dat = run(cmd, args)
1489 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
1490 continue
1492 uid = dat[-1].split()
1493 if not uid: continue
1494 run('uid', ('FETCH', '%s' % uid[-1],
1495 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1497 print('\nAll tests OK.')
1499 except:
1500 print('\nTests failed.')
1502 if not Debug:
1503 print('''
1504 If you would like to see debugging output,
1505 try: %s -d5
1506 ''' % sys.argv[0])
1508 raise