This commit was manufactured by cvs2svn to create tag
[python/dscho.git] / Lib / imaplib.py
blobcbcf107dc60c1ffc7b846de4309814bca4542c1c
1 """IMAP4 client.
3 Based on RFC 2060.
5 Public class: IMAP4
6 Public variable: Debug
7 Public functions: Internaldate2tuple
8 Int2AP
9 ParseFlags
10 Time2Internaldate
11 """
13 # Author: Piers Lauder <piers@cs.su.oz.au> December 1997.
15 # Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
16 # String method conversion by ESR, February 2001.
17 # GET/SETACL contributed by Anthony Baxter <anthony@interlink.com.au> April 2001.
19 __version__ = "2.49"
21 import binascii, re, socket, time, random, sys
23 __all__ = ["IMAP4", "Internaldate2tuple",
24 "Int2AP", "ParseFlags", "Time2Internaldate"]
26 # Globals
28 CRLF = '\r\n'
29 Debug = 0
30 IMAP4_PORT = 143
31 AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first
33 # Commands
35 Commands = {
36 # name valid states
37 'APPEND': ('AUTH', 'SELECTED'),
38 'AUTHENTICATE': ('NONAUTH',),
39 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
40 'CHECK': ('SELECTED',),
41 'CLOSE': ('SELECTED',),
42 'COPY': ('SELECTED',),
43 'CREATE': ('AUTH', 'SELECTED'),
44 'DELETE': ('AUTH', 'SELECTED'),
45 'EXAMINE': ('AUTH', 'SELECTED'),
46 'EXPUNGE': ('SELECTED',),
47 'FETCH': ('SELECTED',),
48 'GETACL': ('AUTH', 'SELECTED'),
49 'LIST': ('AUTH', 'SELECTED'),
50 'LOGIN': ('NONAUTH',),
51 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
52 'LSUB': ('AUTH', 'SELECTED'),
53 'NAMESPACE': ('AUTH', 'SELECTED'),
54 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
55 'PARTIAL': ('SELECTED',),
56 'RENAME': ('AUTH', 'SELECTED'),
57 'SEARCH': ('SELECTED',),
58 'SELECT': ('AUTH', 'SELECTED'),
59 'SETACL': ('AUTH', 'SELECTED'),
60 'SORT': ('SELECTED',),
61 'STATUS': ('AUTH', 'SELECTED'),
62 'STORE': ('SELECTED',),
63 'SUBSCRIBE': ('AUTH', 'SELECTED'),
64 'UID': ('SELECTED',),
65 'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
68 # Patterns to match server responses
70 Continuation = re.compile(r'\+( (?P<data>.*))?')
71 Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
72 InternalDate = re.compile(r'.*INTERNALDATE "'
73 r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
74 r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
75 r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
76 r'"')
77 Literal = re.compile(r'.*{(?P<size>\d+)}$')
78 Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
79 Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
80 Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
84 class IMAP4:
86 """IMAP4 client class.
88 Instantiate with: IMAP4([host[, port]])
90 host - host's name (default: localhost);
91 port - port number (default: standard IMAP4 port).
93 All IMAP4rev1 commands are supported by methods of the same
94 name (in lower-case).
96 All arguments to commands are converted to strings, except for
97 AUTHENTICATE, and the last argument to APPEND which is passed as
98 an IMAP4 literal. If necessary (the string contains any
99 non-printing characters or white-space and isn't enclosed with
100 either parentheses or double quotes) each string is quoted.
101 However, the 'password' argument to the LOGIN command is always
102 quoted. If you want to avoid having an argument string quoted
103 (eg: the 'flags' argument to STORE) then enclose the string in
104 parentheses (eg: "(\Deleted)").
106 Each command returns a tuple: (type, [data, ...]) where 'type'
107 is usually 'OK' or 'NO', and 'data' is either the text from the
108 tagged response, or untagged results from command.
110 Errors raise the exception class <instance>.error("<reason>").
111 IMAP4 server errors raise <instance>.abort("<reason>"),
112 which is a sub-class of 'error'. Mailbox status changes
113 from READ-WRITE to READ-ONLY raise the exception class
114 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
116 "error" exceptions imply a program error.
117 "abort" exceptions imply the connection should be reset, and
118 the command re-tried.
119 "readonly" exceptions imply the command should be re-tried.
121 Note: to use this module, you must read the RFCs pertaining
122 to the IMAP4 protocol, as the semantics of the arguments to
123 each IMAP4 command are left to the invoker, not to mention
124 the results.
127 class error(Exception): pass # Logical errors - debug required
128 class abort(error): pass # Service errors - close and retry
129 class readonly(abort): pass # Mailbox status changed to READ-ONLY
131 mustquote = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]")
133 def __init__(self, host = '', port = IMAP4_PORT):
134 self.host = host
135 self.port = port
136 self.debug = Debug
137 self.state = 'LOGOUT'
138 self.literal = None # A literal argument to a command
139 self.tagged_commands = {} # Tagged commands awaiting response
140 self.untagged_responses = {} # {typ: [data, ...], ...}
141 self.continuation_response = '' # Last continuation response
142 self.is_readonly = None # READ-ONLY desired state
143 self.tagnum = 0
145 # Open socket to server.
147 self.open(host, port)
149 # Create unique tag for this session,
150 # and compile tagged response matcher.
152 self.tagpre = Int2AP(random.randint(0, 31999))
153 self.tagre = re.compile(r'(?P<tag>'
154 + self.tagpre
155 + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
157 # Get server welcome message,
158 # request and store CAPABILITY response.
160 if __debug__:
161 if self.debug >= 1:
162 _mesg('imaplib version %s' % __version__)
163 _mesg('new IMAP4 connection, tag=%s' % self.tagpre)
165 self.welcome = self._get_response()
166 if self.untagged_responses.has_key('PREAUTH'):
167 self.state = 'AUTH'
168 elif self.untagged_responses.has_key('OK'):
169 self.state = 'NONAUTH'
170 else:
171 raise self.error(self.welcome)
173 cap = 'CAPABILITY'
174 self._simple_command(cap)
175 if not self.untagged_responses.has_key(cap):
176 raise self.error('no CAPABILITY response from server')
177 self.capabilities = tuple(self.untagged_responses[cap][-1].upper().split())
179 if __debug__:
180 if self.debug >= 3:
181 _mesg('CAPABILITIES: %s' % `self.capabilities`)
183 for version in AllowedVersions:
184 if not version in self.capabilities:
185 continue
186 self.PROTOCOL_VERSION = version
187 return
189 raise self.error('server not IMAP4 compliant')
192 def __getattr__(self, attr):
193 # Allow UPPERCASE variants of IMAP4 command methods.
194 if Commands.has_key(attr):
195 return getattr(self, attr.lower())
196 raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
200 # Overridable methods
203 def open(self, host, port):
204 """Setup connection to remote server on "host:port".
205 This connection will be used by the routines:
206 read, readline, send, shutdown.
208 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
209 self.sock.connect((self.host, self.port))
210 self.file = self.sock.makefile('rb')
213 def read(self, size):
214 """Read 'size' bytes from remote."""
215 return self.file.read(size)
218 def readline(self):
219 """Read line from remote."""
220 return self.file.readline()
223 def send(self, data):
224 """Send data to remote."""
225 bytes = len(data)
226 while bytes > 0:
227 sent = self.sock.send(data)
228 if sent == bytes:
229 break # avoid copy
230 data = data[sent:]
231 bytes = bytes - sent
234 def shutdown(self):
235 """Close I/O established in "open"."""
236 self.file.close()
237 self.sock.close()
240 def socket(self):
241 """Return socket instance used to connect to IMAP4 server.
243 socket = <instance>.socket()
245 return self.sock
249 # Utility methods
252 def recent(self):
253 """Return most recent 'RECENT' responses if any exist,
254 else prompt server for an update using the 'NOOP' command.
256 (typ, [data]) = <instance>.recent()
258 'data' is None if no new messages,
259 else list of RECENT responses, most recent last.
261 name = 'RECENT'
262 typ, dat = self._untagged_response('OK', [None], name)
263 if dat[-1]:
264 return typ, dat
265 typ, dat = self.noop() # Prod server for response
266 return self._untagged_response(typ, dat, name)
269 def response(self, code):
270 """Return data for response 'code' if received, or None.
272 Old value for response 'code' is cleared.
274 (code, [data]) = <instance>.response(code)
276 return self._untagged_response(code, [None], code.upper())
280 # IMAP4 commands
283 def append(self, mailbox, flags, date_time, message):
284 """Append message to named mailbox.
286 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
288 All args except `message' can be None.
290 name = 'APPEND'
291 if not mailbox:
292 mailbox = 'INBOX'
293 if flags:
294 if (flags[0],flags[-1]) != ('(',')'):
295 flags = '(%s)' % flags
296 else:
297 flags = None
298 if date_time:
299 date_time = Time2Internaldate(date_time)
300 else:
301 date_time = None
302 self.literal = message
303 return self._simple_command(name, mailbox, flags, date_time)
306 def authenticate(self, mechanism, authobject):
307 """Authenticate command - requires response processing.
309 'mechanism' specifies which authentication mechanism is to
310 be used - it must appear in <instance>.capabilities in the
311 form AUTH=<mechanism>.
313 'authobject' must be a callable object:
315 data = authobject(response)
317 It will be called to process server continuation responses.
318 It should return data that will be encoded and sent to server.
319 It should return None if the client abort response '*' should
320 be sent instead.
322 mech = mechanism.upper()
323 cap = 'AUTH=%s' % mech
324 if not cap in self.capabilities:
325 raise self.error("Server doesn't allow %s authentication." % mech)
326 self.literal = _Authenticator(authobject).process
327 typ, dat = self._simple_command('AUTHENTICATE', mech)
328 if typ != 'OK':
329 raise self.error(dat[-1])
330 self.state = 'AUTH'
331 return typ, dat
334 def check(self):
335 """Checkpoint mailbox on server.
337 (typ, [data]) = <instance>.check()
339 return self._simple_command('CHECK')
342 def close(self):
343 """Close currently selected mailbox.
345 Deleted messages are removed from writable mailbox.
346 This is the recommended command before 'LOGOUT'.
348 (typ, [data]) = <instance>.close()
350 try:
351 typ, dat = self._simple_command('CLOSE')
352 finally:
353 self.state = 'AUTH'
354 return typ, dat
357 def copy(self, message_set, new_mailbox):
358 """Copy 'message_set' messages onto end of 'new_mailbox'.
360 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
362 return self._simple_command('COPY', message_set, new_mailbox)
365 def create(self, mailbox):
366 """Create new mailbox.
368 (typ, [data]) = <instance>.create(mailbox)
370 return self._simple_command('CREATE', mailbox)
373 def delete(self, mailbox):
374 """Delete old mailbox.
376 (typ, [data]) = <instance>.delete(mailbox)
378 return self._simple_command('DELETE', mailbox)
381 def expunge(self):
382 """Permanently remove deleted items from selected mailbox.
384 Generates 'EXPUNGE' response for each deleted message.
386 (typ, [data]) = <instance>.expunge()
388 'data' is list of 'EXPUNGE'd message numbers in order received.
390 name = 'EXPUNGE'
391 typ, dat = self._simple_command(name)
392 return self._untagged_response(typ, dat, name)
395 def fetch(self, message_set, message_parts):
396 """Fetch (parts of) messages.
398 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
400 'message_parts' should be a string of selected parts
401 enclosed in parentheses, eg: "(UID BODY[TEXT])".
403 'data' are tuples of message part envelope and data.
405 name = 'FETCH'
406 typ, dat = self._simple_command(name, message_set, message_parts)
407 return self._untagged_response(typ, dat, name)
410 def getacl(self, mailbox):
411 """Get the ACLs for a mailbox.
413 (typ, [data]) = <instance>.getacl(mailbox)
415 typ, dat = self._simple_command('GETACL', mailbox)
416 return self._untagged_response(typ, dat, 'ACL')
419 def list(self, directory='""', pattern='*'):
420 """List mailbox names in directory matching pattern.
422 (typ, [data]) = <instance>.list(directory='""', pattern='*')
424 'data' is list of LIST responses.
426 name = 'LIST'
427 typ, dat = self._simple_command(name, directory, pattern)
428 return self._untagged_response(typ, dat, name)
431 def login(self, user, password):
432 """Identify client using plaintext password.
434 (typ, [data]) = <instance>.login(user, password)
436 NB: 'password' will be quoted.
438 #if not 'AUTH=LOGIN' in self.capabilities:
439 # raise self.error("Server doesn't allow LOGIN authentication." % mech)
440 typ, dat = self._simple_command('LOGIN', user, self._quote(password))
441 if typ != 'OK':
442 raise self.error(dat[-1])
443 self.state = 'AUTH'
444 return typ, dat
447 def logout(self):
448 """Shutdown connection to server.
450 (typ, [data]) = <instance>.logout()
452 Returns server 'BYE' response.
454 self.state = 'LOGOUT'
455 try: typ, dat = self._simple_command('LOGOUT')
456 except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
457 self.shutdown()
458 if self.untagged_responses.has_key('BYE'):
459 return 'BYE', self.untagged_responses['BYE']
460 return typ, dat
463 def lsub(self, directory='""', pattern='*'):
464 """List 'subscribed' mailbox names in directory matching pattern.
466 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
468 'data' are tuples of message part envelope and data.
470 name = 'LSUB'
471 typ, dat = self._simple_command(name, directory, pattern)
472 return self._untagged_response(typ, dat, name)
475 def namespace(self):
476 """ Returns IMAP namespaces ala rfc2342
478 (typ, [data, ...]) = <instance>.namespace()
480 name = 'NAMESPACE'
481 typ, dat = self._simple_command(name)
482 return self._untagged_response(typ, dat, name)
485 def noop(self):
486 """Send NOOP command.
488 (typ, data) = <instance>.noop()
490 if __debug__:
491 if self.debug >= 3:
492 _dump_ur(self.untagged_responses)
493 return self._simple_command('NOOP')
496 def partial(self, message_num, message_part, start, length):
497 """Fetch truncated part of a message.
499 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
501 'data' is tuple of message part envelope and data.
503 name = 'PARTIAL'
504 typ, dat = self._simple_command(name, message_num, message_part, start, length)
505 return self._untagged_response(typ, dat, 'FETCH')
508 def rename(self, oldmailbox, newmailbox):
509 """Rename old mailbox name to new.
511 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
513 return self._simple_command('RENAME', oldmailbox, newmailbox)
516 def search(self, charset, *criteria):
517 """Search mailbox for matching messages.
519 (typ, [data]) = <instance>.search(charset, criterium, ...)
521 'data' is space separated list of matching message numbers.
523 name = 'SEARCH'
524 if charset:
525 typ, dat = apply(self._simple_command, (name, 'CHARSET', charset) + criteria)
526 else:
527 typ, dat = apply(self._simple_command, (name,) + criteria)
528 return self._untagged_response(typ, dat, name)
531 def select(self, mailbox='INBOX', readonly=None):
532 """Select a mailbox.
534 Flush all untagged responses.
536 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
538 'data' is count of messages in mailbox ('EXISTS' response).
540 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
541 self.untagged_responses = {} # Flush old responses.
542 self.is_readonly = readonly
543 if readonly:
544 name = 'EXAMINE'
545 else:
546 name = 'SELECT'
547 typ, dat = self._simple_command(name, mailbox)
548 if typ != 'OK':
549 self.state = 'AUTH' # Might have been 'SELECTED'
550 return typ, dat
551 self.state = 'SELECTED'
552 if self.untagged_responses.has_key('READ-ONLY') \
553 and not readonly:
554 if __debug__:
555 if self.debug >= 1:
556 _dump_ur(self.untagged_responses)
557 raise self.readonly('%s is not writable' % mailbox)
558 return typ, self.untagged_responses.get('EXISTS', [None])
561 def setacl(self, mailbox, who, what):
562 """Set a mailbox acl.
564 (typ, [data]) = <instance>.create(mailbox, who, what)
566 return self._simple_command('SETACL', mailbox, who, what)
569 def sort(self, sort_criteria, charset, *search_criteria):
570 """IMAP4rev1 extension SORT command.
572 (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
574 name = 'SORT'
575 #if not name in self.capabilities: # Let the server decide!
576 # raise self.error('unimplemented extension command: %s' % name)
577 if (sort_criteria[0],sort_criteria[-1]) != ('(',')'):
578 sort_criteria = '(%s)' % sort_criteria
579 typ, dat = apply(self._simple_command, (name, sort_criteria, charset) + search_criteria)
580 return self._untagged_response(typ, dat, name)
583 def status(self, mailbox, names):
584 """Request named status conditions for mailbox.
586 (typ, [data]) = <instance>.status(mailbox, names)
588 name = 'STATUS'
589 #if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide!
590 # raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
591 typ, dat = self._simple_command(name, mailbox, names)
592 return self._untagged_response(typ, dat, name)
595 def store(self, message_set, command, flags):
596 """Alters flag dispositions for messages in mailbox.
598 (typ, [data]) = <instance>.store(message_set, command, flags)
600 if (flags[0],flags[-1]) != ('(',')'):
601 flags = '(%s)' % flags # Avoid quoting the flags
602 typ, dat = self._simple_command('STORE', message_set, command, flags)
603 return self._untagged_response(typ, dat, 'FETCH')
606 def subscribe(self, mailbox):
607 """Subscribe to new mailbox.
609 (typ, [data]) = <instance>.subscribe(mailbox)
611 return self._simple_command('SUBSCRIBE', mailbox)
614 def uid(self, command, *args):
615 """Execute "command arg ..." with messages identified by UID,
616 rather than message number.
618 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
620 Returns response appropriate to 'command'.
622 command = command.upper()
623 if not Commands.has_key(command):
624 raise self.error("Unknown IMAP4 UID command: %s" % command)
625 if self.state not in Commands[command]:
626 raise self.error('command %s illegal in state %s'
627 % (command, self.state))
628 name = 'UID'
629 typ, dat = apply(self._simple_command, (name, command) + args)
630 if command in ('SEARCH', 'SORT'):
631 name = command
632 else:
633 name = 'FETCH'
634 return self._untagged_response(typ, dat, name)
637 def unsubscribe(self, mailbox):
638 """Unsubscribe from old mailbox.
640 (typ, [data]) = <instance>.unsubscribe(mailbox)
642 return self._simple_command('UNSUBSCRIBE', mailbox)
645 def xatom(self, name, *args):
646 """Allow simple extension commands
647 notified by server in CAPABILITY response.
649 Assumes command is legal in current state.
651 (typ, [data]) = <instance>.xatom(name, arg, ...)
653 Returns response appropriate to extension command `name'.
655 name = name.upper()
656 #if not name in self.capabilities: # Let the server decide!
657 # raise self.error('unknown extension command: %s' % name)
658 if not Commands.has_key(name):
659 Commands[name] = (self.state,)
660 return apply(self._simple_command, (name,) + args)
664 # Private methods
667 def _append_untagged(self, typ, dat):
669 if dat is None: dat = ''
670 ur = self.untagged_responses
671 if __debug__:
672 if self.debug >= 5:
673 _mesg('untagged_responses[%s] %s += ["%s"]' %
674 (typ, len(ur.get(typ,'')), dat))
675 if ur.has_key(typ):
676 ur[typ].append(dat)
677 else:
678 ur[typ] = [dat]
681 def _check_bye(self):
682 bye = self.untagged_responses.get('BYE')
683 if bye:
684 raise self.abort(bye[-1])
687 def _command(self, name, *args):
689 if self.state not in Commands[name]:
690 self.literal = None
691 raise self.error(
692 'command %s illegal in state %s' % (name, self.state))
694 for typ in ('OK', 'NO', 'BAD'):
695 if self.untagged_responses.has_key(typ):
696 del self.untagged_responses[typ]
698 if self.untagged_responses.has_key('READ-ONLY') \
699 and not self.is_readonly:
700 raise self.readonly('mailbox status changed to READ-ONLY')
702 tag = self._new_tag()
703 data = '%s %s' % (tag, name)
704 for arg in args:
705 if arg is None: continue
706 data = '%s %s' % (data, self._checkquote(arg))
708 literal = self.literal
709 if literal is not None:
710 self.literal = None
711 if type(literal) is type(self._command):
712 literator = literal
713 else:
714 literator = None
715 data = '%s {%s}' % (data, len(literal))
717 if __debug__:
718 if self.debug >= 4:
719 _mesg('> %s' % data)
720 else:
721 _log('> %s' % data)
723 try:
724 self.send('%s%s' % (data, CRLF))
725 except (socket.error, OSError), val:
726 raise self.abort('socket error: %s' % val)
728 if literal is None:
729 return tag
731 while 1:
732 # Wait for continuation response
734 while self._get_response():
735 if self.tagged_commands[tag]: # BAD/NO?
736 return tag
738 # Send literal
740 if literator:
741 literal = literator(self.continuation_response)
743 if __debug__:
744 if self.debug >= 4:
745 _mesg('write literal size %s' % len(literal))
747 try:
748 self.send(literal)
749 self.send(CRLF)
750 except (socket.error, OSError), val:
751 raise self.abort('socket error: %s' % val)
753 if not literator:
754 break
756 return tag
759 def _command_complete(self, name, tag):
760 self._check_bye()
761 try:
762 typ, data = self._get_tagged_response(tag)
763 except self.abort, val:
764 raise self.abort('command: %s => %s' % (name, val))
765 except self.error, val:
766 raise self.error('command: %s => %s' % (name, val))
767 self._check_bye()
768 if typ == 'BAD':
769 raise self.error('%s command error: %s %s' % (name, typ, data))
770 return typ, data
773 def _get_response(self):
775 # Read response and store.
777 # Returns None for continuation responses,
778 # otherwise first response line received.
780 resp = self._get_line()
782 # Command completion response?
784 if self._match(self.tagre, resp):
785 tag = self.mo.group('tag')
786 if not self.tagged_commands.has_key(tag):
787 raise self.abort('unexpected tagged response: %s' % resp)
789 typ = self.mo.group('type')
790 dat = self.mo.group('data')
791 self.tagged_commands[tag] = (typ, [dat])
792 else:
793 dat2 = None
795 # '*' (untagged) responses?
797 if not self._match(Untagged_response, resp):
798 if self._match(Untagged_status, resp):
799 dat2 = self.mo.group('data2')
801 if self.mo is None:
802 # Only other possibility is '+' (continuation) response...
804 if self._match(Continuation, resp):
805 self.continuation_response = self.mo.group('data')
806 return None # NB: indicates continuation
808 raise self.abort("unexpected response: '%s'" % resp)
810 typ = self.mo.group('type')
811 dat = self.mo.group('data')
812 if dat is None: dat = '' # Null untagged response
813 if dat2: dat = dat + ' ' + dat2
815 # Is there a literal to come?
817 while self._match(Literal, dat):
819 # Read literal direct from connection.
821 size = int(self.mo.group('size'))
822 if __debug__:
823 if self.debug >= 4:
824 _mesg('read literal size %s' % size)
825 data = self.read(size)
827 # Store response with literal as tuple
829 self._append_untagged(typ, (dat, data))
831 # Read trailer - possibly containing another literal
833 dat = self._get_line()
835 self._append_untagged(typ, dat)
837 # Bracketed response information?
839 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
840 self._append_untagged(self.mo.group('type'), self.mo.group('data'))
842 if __debug__:
843 if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
844 _mesg('%s response: %s' % (typ, dat))
846 return resp
849 def _get_tagged_response(self, tag):
851 while 1:
852 result = self.tagged_commands[tag]
853 if result is not None:
854 del self.tagged_commands[tag]
855 return result
857 # Some have reported "unexpected response" exceptions.
858 # Note that ignoring them here causes loops.
859 # Instead, send me details of the unexpected response and
860 # I'll update the code in `_get_response()'.
862 try:
863 self._get_response()
864 except self.abort, val:
865 if __debug__:
866 if self.debug >= 1:
867 print_log()
868 raise
871 def _get_line(self):
873 line = self.readline()
874 if not line:
875 raise self.abort('socket error: EOF')
877 # Protocol mandates all lines terminated by CRLF
879 line = line[:-2]
880 if __debug__:
881 if self.debug >= 4:
882 _mesg('< %s' % line)
883 else:
884 _log('< %s' % line)
885 return line
888 def _match(self, cre, s):
890 # Run compiled regular expression match method on 's'.
891 # Save result, return success.
893 self.mo = cre.match(s)
894 if __debug__:
895 if self.mo is not None and self.debug >= 5:
896 _mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`))
897 return self.mo is not None
900 def _new_tag(self):
902 tag = '%s%s' % (self.tagpre, self.tagnum)
903 self.tagnum = self.tagnum + 1
904 self.tagged_commands[tag] = None
905 return tag
908 def _checkquote(self, arg):
910 # Must quote command args if non-alphanumeric chars present,
911 # and not already quoted.
913 if type(arg) is not type(''):
914 return arg
915 if (arg[0],arg[-1]) in (('(',')'),('"','"')):
916 return arg
917 if self.mustquote.search(arg) is None:
918 return arg
919 return self._quote(arg)
922 def _quote(self, arg):
924 arg = arg.replace('\\', '\\\\')
925 arg = arg.replace('"', '\\"')
927 return '"%s"' % arg
930 def _simple_command(self, name, *args):
932 return self._command_complete(name, apply(self._command, (name,) + args))
935 def _untagged_response(self, typ, dat, name):
937 if typ == 'NO':
938 return typ, dat
939 if not self.untagged_responses.has_key(name):
940 return typ, [None]
941 data = self.untagged_responses[name]
942 if __debug__:
943 if self.debug >= 5:
944 _mesg('untagged_responses[%s] => %s' % (name, data))
945 del self.untagged_responses[name]
946 return typ, data
950 class _Authenticator:
952 """Private class to provide en/decoding
953 for base64-based authentication conversation.
956 def __init__(self, mechinst):
957 self.mech = mechinst # Callable object to provide/process data
959 def process(self, data):
960 ret = self.mech(self.decode(data))
961 if ret is None:
962 return '*' # Abort conversation
963 return self.encode(ret)
965 def encode(self, inp):
967 # Invoke binascii.b2a_base64 iteratively with
968 # short even length buffers, strip the trailing
969 # line feed from the result and append. "Even"
970 # means a number that factors to both 6 and 8,
971 # so when it gets to the end of the 8-bit input
972 # there's no partial 6-bit output.
974 oup = ''
975 while inp:
976 if len(inp) > 48:
977 t = inp[:48]
978 inp = inp[48:]
979 else:
980 t = inp
981 inp = ''
982 e = binascii.b2a_base64(t)
983 if e:
984 oup = oup + e[:-1]
985 return oup
987 def decode(self, inp):
988 if not inp:
989 return ''
990 return binascii.a2b_base64(inp)
994 Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
995 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
997 def Internaldate2tuple(resp):
998 """Convert IMAP4 INTERNALDATE to UT.
1000 Returns Python time module tuple.
1003 mo = InternalDate.match(resp)
1004 if not mo:
1005 return None
1007 mon = Mon2num[mo.group('mon')]
1008 zonen = mo.group('zonen')
1010 day = int(mo.group('day'))
1011 year = int(mo.group('year'))
1012 hour = int(mo.group('hour'))
1013 min = int(mo.group('min'))
1014 sec = int(mo.group('sec'))
1015 zoneh = int(mo.group('zoneh'))
1016 zonem = int(mo.group('zonem'))
1018 # INTERNALDATE timezone must be subtracted to get UT
1020 zone = (zoneh*60 + zonem)*60
1021 if zonen == '-':
1022 zone = -zone
1024 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
1026 utc = time.mktime(tt)
1028 # Following is necessary because the time module has no 'mkgmtime'.
1029 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
1031 lt = time.localtime(utc)
1032 if time.daylight and lt[-1]:
1033 zone = zone + time.altzone
1034 else:
1035 zone = zone + time.timezone
1037 return time.localtime(utc - zone)
1041 def Int2AP(num):
1043 """Convert integer to A-P string representation."""
1045 val = ''; AP = 'ABCDEFGHIJKLMNOP'
1046 num = int(abs(num))
1047 while num:
1048 num, mod = divmod(num, 16)
1049 val = AP[mod] + val
1050 return val
1054 def ParseFlags(resp):
1056 """Convert IMAP4 flags response to python tuple."""
1058 mo = Flags.match(resp)
1059 if not mo:
1060 return ()
1062 return tuple(mo.group('flags').split())
1065 def Time2Internaldate(date_time):
1067 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
1069 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
1072 dttype = type(date_time)
1073 if dttype is type(1) or dttype is type(1.1):
1074 tt = time.localtime(date_time)
1075 elif dttype is type(()):
1076 tt = date_time
1077 elif dttype is type(""):
1078 return date_time # Assume in correct format
1079 else: raise ValueError
1081 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
1082 if dt[0] == '0':
1083 dt = ' ' + dt[1:]
1084 if time.daylight and tt[-1]:
1085 zone = -time.altzone
1086 else:
1087 zone = -time.timezone
1088 return '"' + dt + " %+02d%02d" % divmod(zone/60, 60) + '"'
1092 if __debug__:
1094 def _mesg(s, secs=None):
1095 if secs is None:
1096 secs = time.time()
1097 tm = time.strftime('%M:%S', time.localtime(secs))
1098 sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
1099 sys.stderr.flush()
1101 def _dump_ur(dict):
1102 # Dump untagged responses (in `dict').
1103 l = dict.items()
1104 if not l: return
1105 t = '\n\t\t'
1106 l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l)
1107 _mesg('untagged responses dump:%s%s' % (t, t.join(l)))
1109 _cmd_log = [] # Last `_cmd_log_len' interactions
1110 _cmd_log_len = 10
1112 def _log(line):
1113 # Keep log of last `_cmd_log_len' interactions for debugging.
1114 if len(_cmd_log) == _cmd_log_len:
1115 del _cmd_log[0]
1116 _cmd_log.append((time.time(), line))
1118 def print_log():
1119 _mesg('last %d IMAP4 interactions:' % len(_cmd_log))
1120 for secs,line in _cmd_log:
1121 _mesg(line, secs)
1125 if __name__ == '__main__':
1127 import getopt, getpass
1129 try:
1130 optlist, args = getopt.getopt(sys.argv[1:], 'd:')
1131 except getopt.error, val:
1132 pass
1134 for opt,val in optlist:
1135 if opt == '-d':
1136 Debug = int(val)
1138 if not args: args = ('',)
1140 host = args[0]
1142 USER = getpass.getuser()
1143 PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost"))
1145 test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER, 'lf':CRLF}
1146 test_seq1 = (
1147 ('login', (USER, PASSWD)),
1148 ('create', ('/tmp/xxx 1',)),
1149 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1150 ('CREATE', ('/tmp/yyz 2',)),
1151 ('append', ('/tmp/yyz 2', None, None, test_mesg)),
1152 ('list', ('/tmp', 'yy*')),
1153 ('select', ('/tmp/yyz 2',)),
1154 ('search', (None, 'SUBJECT', 'test')),
1155 ('partial', ('1', 'RFC822', 1, 1024)),
1156 ('store', ('1', 'FLAGS', '(\Deleted)')),
1157 ('namespace', ()),
1158 ('expunge', ()),
1159 ('recent', ()),
1160 ('close', ()),
1163 test_seq2 = (
1164 ('select', ()),
1165 ('response',('UIDVALIDITY',)),
1166 ('uid', ('SEARCH', 'ALL')),
1167 ('response', ('EXISTS',)),
1168 ('append', (None, None, None, test_mesg)),
1169 ('recent', ()),
1170 ('logout', ()),
1173 def run(cmd, args):
1174 _mesg('%s %s' % (cmd, args))
1175 typ, dat = apply(getattr(M, cmd), args)
1176 _mesg('%s => %s %s' % (cmd, typ, dat))
1177 return dat
1179 try:
1180 M = IMAP4(host)
1181 _mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
1182 _mesg('CAPABILITIES = %s' % `M.capabilities`)
1184 for cmd,args in test_seq1:
1185 run(cmd, args)
1187 for ml in run('list', ('/tmp/', 'yy%')):
1188 mo = re.match(r'.*"([^"]+)"$', ml)
1189 if mo: path = mo.group(1)
1190 else: path = ml.split()[-1]
1191 run('delete', (path,))
1193 for cmd,args in test_seq2:
1194 dat = run(cmd, args)
1196 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
1197 continue
1199 uid = dat[-1].split()
1200 if not uid: continue
1201 run('uid', ('FETCH', '%s' % uid[-1],
1202 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1204 print '\nAll tests OK.'
1206 except:
1207 print '\nTests failed.'
1209 if not Debug:
1210 print '''
1211 If you would like to see debugging output,
1212 try: %s -d5
1213 ''' % sys.argv[0]
1215 raise