py-cvs-rel2_1 (Rev 1.2) merge
[python/dscho.git] / Lib / imaplib.py
blob6aca2992c5b9e18debc434666c1705e385157520
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.
18 __version__ = "2.40"
20 import binascii, re, socket, time, random, sys
22 __all__ = ["IMAP4", "Internaldate2tuple",
23 "Int2AP", "ParseFlags", "Time2Internaldate"]
25 # Globals
27 CRLF = '\r\n'
28 Debug = 0
29 IMAP4_PORT = 143
30 AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first
32 # Commands
34 Commands = {
35 # name valid states
36 'APPEND': ('AUTH', 'SELECTED'),
37 'AUTHENTICATE': ('NONAUTH',),
38 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
39 'CHECK': ('SELECTED',),
40 'CLOSE': ('SELECTED',),
41 'COPY': ('SELECTED',),
42 'CREATE': ('AUTH', 'SELECTED'),
43 'DELETE': ('AUTH', 'SELECTED'),
44 'EXAMINE': ('AUTH', 'SELECTED'),
45 'EXPUNGE': ('SELECTED',),
46 'FETCH': ('SELECTED',),
47 'LIST': ('AUTH', 'SELECTED'),
48 'LOGIN': ('NONAUTH',),
49 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
50 'LSUB': ('AUTH', 'SELECTED'),
51 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
52 'PARTIAL': ('SELECTED',),
53 'RENAME': ('AUTH', 'SELECTED'),
54 'SEARCH': ('SELECTED',),
55 'SELECT': ('AUTH', 'SELECTED'),
56 'STATUS': ('AUTH', 'SELECTED'),
57 'STORE': ('SELECTED',),
58 'SUBSCRIBE': ('AUTH', 'SELECTED'),
59 'UID': ('SELECTED',),
60 'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
61 'NAMESPACE': ('AUTH', 'SELECTED'),
64 # Patterns to match server responses
66 Continuation = re.compile(r'\+( (?P<data>.*))?')
67 Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
68 InternalDate = re.compile(r'.*INTERNALDATE "'
69 r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
70 r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
71 r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
72 r'"')
73 Literal = re.compile(r'.*{(?P<size>\d+)}$')
74 Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
75 Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
76 Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
80 class IMAP4:
82 """IMAP4 client class.
84 Instantiate with: IMAP4([host[, port]])
86 host - host's name (default: localhost);
87 port - port number (default: standard IMAP4 port).
89 All IMAP4rev1 commands are supported by methods of the same
90 name (in lower-case).
92 All arguments to commands are converted to strings, except for
93 AUTHENTICATE, and the last argument to APPEND which is passed as
94 an IMAP4 literal. If necessary (the string contains any
95 non-printing characters or white-space and isn't enclosed with
96 either parentheses or double quotes) each string is quoted.
97 However, the 'password' argument to the LOGIN command is always
98 quoted. If you want to avoid having an argument string quoted
99 (eg: the 'flags' argument to STORE) then enclose the string in
100 parentheses (eg: "(\Deleted)").
102 Each command returns a tuple: (type, [data, ...]) where 'type'
103 is usually 'OK' or 'NO', and 'data' is either the text from the
104 tagged response, or untagged results from command.
106 Errors raise the exception class <instance>.error("<reason>").
107 IMAP4 server errors raise <instance>.abort("<reason>"),
108 which is a sub-class of 'error'. Mailbox status changes
109 from READ-WRITE to READ-ONLY raise the exception class
110 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
112 "error" exceptions imply a program error.
113 "abort" exceptions imply the connection should be reset, and
114 the command re-tried.
115 "readonly" exceptions imply the command should be re-tried.
117 Note: to use this module, you must read the RFCs pertaining
118 to the IMAP4 protocol, as the semantics of the arguments to
119 each IMAP4 command are left to the invoker, not to mention
120 the results.
123 class error(Exception): pass # Logical errors - debug required
124 class abort(error): pass # Service errors - close and retry
125 class readonly(abort): pass # Mailbox status changed to READ-ONLY
127 mustquote = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]")
129 def __init__(self, host = '', port = IMAP4_PORT):
130 self.host = host
131 self.port = port
132 self.debug = Debug
133 self.state = 'LOGOUT'
134 self.literal = None # A literal argument to a command
135 self.tagged_commands = {} # Tagged commands awaiting response
136 self.untagged_responses = {} # {typ: [data, ...], ...}
137 self.continuation_response = '' # Last continuation response
138 self.is_readonly = None # READ-ONLY desired state
139 self.tagnum = 0
141 # Open socket to server.
143 self.open(host, port)
145 # Create unique tag for this session,
146 # and compile tagged response matcher.
148 self.tagpre = Int2AP(random.randint(0, 31999))
149 self.tagre = re.compile(r'(?P<tag>'
150 + self.tagpre
151 + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
153 # Get server welcome message,
154 # request and store CAPABILITY response.
156 if __debug__:
157 if self.debug >= 1:
158 _mesg('new IMAP4 connection, tag=%s' % self.tagpre)
160 self.welcome = self._get_response()
161 if self.untagged_responses.has_key('PREAUTH'):
162 self.state = 'AUTH'
163 elif self.untagged_responses.has_key('OK'):
164 self.state = 'NONAUTH'
165 else:
166 raise self.error(self.welcome)
168 cap = 'CAPABILITY'
169 self._simple_command(cap)
170 if not self.untagged_responses.has_key(cap):
171 raise self.error('no CAPABILITY response from server')
172 self.capabilities = tuple(self.untagged_responses[cap][-1].upper().split())
174 if __debug__:
175 if self.debug >= 3:
176 _mesg('CAPABILITIES: %s' % `self.capabilities`)
178 for version in AllowedVersions:
179 if not version in self.capabilities:
180 continue
181 self.PROTOCOL_VERSION = version
182 return
184 raise self.error('server not IMAP4 compliant')
187 def __getattr__(self, attr):
188 # Allow UPPERCASE variants of IMAP4 command methods.
189 if Commands.has_key(attr):
190 return eval("self.%s" % attr.lower())
191 raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
195 # Public methods
198 def open(self, host, port):
199 """Setup 'self.sock' and 'self.file'."""
200 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
201 self.sock.connect((self.host, self.port))
202 self.file = self.sock.makefile('r')
205 def recent(self):
206 """Return most recent 'RECENT' responses if any exist,
207 else prompt server for an update using the 'NOOP' command.
209 (typ, [data]) = <instance>.recent()
211 'data' is None if no new messages,
212 else list of RECENT responses, most recent last.
214 name = 'RECENT'
215 typ, dat = self._untagged_response('OK', [None], name)
216 if dat[-1]:
217 return typ, dat
218 typ, dat = self.noop() # Prod server for response
219 return self._untagged_response(typ, dat, name)
222 def response(self, code):
223 """Return data for response 'code' if received, or None.
225 Old value for response 'code' is cleared.
227 (code, [data]) = <instance>.response(code)
229 return self._untagged_response(code, [None], code.upper())
232 def socket(self):
233 """Return socket instance used to connect to IMAP4 server.
235 socket = <instance>.socket()
237 return self.sock
241 # IMAP4 commands
244 def append(self, mailbox, flags, date_time, message):
245 """Append message to named mailbox.
247 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
249 All args except `message' can be None.
251 name = 'APPEND'
252 if not mailbox:
253 mailbox = 'INBOX'
254 if flags:
255 if (flags[0],flags[-1]) != ('(',')'):
256 flags = '(%s)' % flags
257 else:
258 flags = None
259 if date_time:
260 date_time = Time2Internaldate(date_time)
261 else:
262 date_time = None
263 self.literal = message
264 return self._simple_command(name, mailbox, flags, date_time)
267 def authenticate(self, mechanism, authobject):
268 """Authenticate command - requires response processing.
270 'mechanism' specifies which authentication mechanism is to
271 be used - it must appear in <instance>.capabilities in the
272 form AUTH=<mechanism>.
274 'authobject' must be a callable object:
276 data = authobject(response)
278 It will be called to process server continuation responses.
279 It should return data that will be encoded and sent to server.
280 It should return None if the client abort response '*' should
281 be sent instead.
283 mech = mechanism.upper()
284 cap = 'AUTH=%s' % mech
285 if not cap in self.capabilities:
286 raise self.error("Server doesn't allow %s authentication." % mech)
287 self.literal = _Authenticator(authobject).process
288 typ, dat = self._simple_command('AUTHENTICATE', mech)
289 if typ != 'OK':
290 raise self.error(dat[-1])
291 self.state = 'AUTH'
292 return typ, dat
295 def check(self):
296 """Checkpoint mailbox on server.
298 (typ, [data]) = <instance>.check()
300 return self._simple_command('CHECK')
303 def close(self):
304 """Close currently selected mailbox.
306 Deleted messages are removed from writable mailbox.
307 This is the recommended command before 'LOGOUT'.
309 (typ, [data]) = <instance>.close()
311 try:
312 typ, dat = self._simple_command('CLOSE')
313 finally:
314 self.state = 'AUTH'
315 return typ, dat
318 def copy(self, message_set, new_mailbox):
319 """Copy 'message_set' messages onto end of 'new_mailbox'.
321 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
323 return self._simple_command('COPY', message_set, new_mailbox)
326 def create(self, mailbox):
327 """Create new mailbox.
329 (typ, [data]) = <instance>.create(mailbox)
331 return self._simple_command('CREATE', mailbox)
334 def delete(self, mailbox):
335 """Delete old mailbox.
337 (typ, [data]) = <instance>.delete(mailbox)
339 return self._simple_command('DELETE', mailbox)
342 def expunge(self):
343 """Permanently remove deleted items from selected mailbox.
345 Generates 'EXPUNGE' response for each deleted message.
347 (typ, [data]) = <instance>.expunge()
349 'data' is list of 'EXPUNGE'd message numbers in order received.
351 name = 'EXPUNGE'
352 typ, dat = self._simple_command(name)
353 return self._untagged_response(typ, dat, name)
356 def fetch(self, message_set, message_parts):
357 """Fetch (parts of) messages.
359 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
361 'message_parts' should be a string of selected parts
362 enclosed in parentheses, eg: "(UID BODY[TEXT])".
364 'data' are tuples of message part envelope and data.
366 name = 'FETCH'
367 typ, dat = self._simple_command(name, message_set, message_parts)
368 return self._untagged_response(typ, dat, name)
371 def list(self, directory='""', pattern='*'):
372 """List mailbox names in directory matching pattern.
374 (typ, [data]) = <instance>.list(directory='""', pattern='*')
376 'data' is list of LIST responses.
378 name = 'LIST'
379 typ, dat = self._simple_command(name, directory, pattern)
380 return self._untagged_response(typ, dat, name)
383 def login(self, user, password):
384 """Identify client using plaintext password.
386 (typ, [data]) = <instance>.login(user, password)
388 NB: 'password' will be quoted.
390 #if not 'AUTH=LOGIN' in self.capabilities:
391 # raise self.error("Server doesn't allow LOGIN authentication." % mech)
392 typ, dat = self._simple_command('LOGIN', user, self._quote(password))
393 if typ != 'OK':
394 raise self.error(dat[-1])
395 self.state = 'AUTH'
396 return typ, dat
399 def logout(self):
400 """Shutdown connection to server.
402 (typ, [data]) = <instance>.logout()
404 Returns server 'BYE' response.
406 self.state = 'LOGOUT'
407 try: typ, dat = self._simple_command('LOGOUT')
408 except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
409 self.file.close()
410 self.sock.close()
411 if self.untagged_responses.has_key('BYE'):
412 return 'BYE', self.untagged_responses['BYE']
413 return typ, dat
416 def lsub(self, directory='""', pattern='*'):
417 """List 'subscribed' mailbox names in directory matching pattern.
419 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
421 'data' are tuples of message part envelope and data.
423 name = 'LSUB'
424 typ, dat = self._simple_command(name, directory, pattern)
425 return self._untagged_response(typ, dat, name)
428 def noop(self):
429 """Send NOOP command.
431 (typ, data) = <instance>.noop()
433 if __debug__:
434 if self.debug >= 3:
435 _dump_ur(self.untagged_responses)
436 return self._simple_command('NOOP')
439 def partial(self, message_num, message_part, start, length):
440 """Fetch truncated part of a message.
442 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
444 'data' is tuple of message part envelope and data.
446 name = 'PARTIAL'
447 typ, dat = self._simple_command(name, message_num, message_part, start, length)
448 return self._untagged_response(typ, dat, 'FETCH')
451 def rename(self, oldmailbox, newmailbox):
452 """Rename old mailbox name to new.
454 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
456 return self._simple_command('RENAME', oldmailbox, newmailbox)
459 def search(self, charset, *criteria):
460 """Search mailbox for matching messages.
462 (typ, [data]) = <instance>.search(charset, criterium, ...)
464 'data' is space separated list of matching message numbers.
466 name = 'SEARCH'
467 if charset:
468 charset = 'CHARSET ' + charset
469 typ, dat = apply(self._simple_command, (name, charset) + criteria)
470 return self._untagged_response(typ, dat, name)
473 def select(self, mailbox='INBOX', readonly=None):
474 """Select a mailbox.
476 Flush all untagged responses.
478 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
480 'data' is count of messages in mailbox ('EXISTS' response).
482 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
483 self.untagged_responses = {} # Flush old responses.
484 self.is_readonly = readonly
485 if readonly:
486 name = 'EXAMINE'
487 else:
488 name = 'SELECT'
489 typ, dat = self._simple_command(name, mailbox)
490 if typ != 'OK':
491 self.state = 'AUTH' # Might have been 'SELECTED'
492 return typ, dat
493 self.state = 'SELECTED'
494 if self.untagged_responses.has_key('READ-ONLY') \
495 and not readonly:
496 if __debug__:
497 if self.debug >= 1:
498 _dump_ur(self.untagged_responses)
499 raise self.readonly('%s is not writable' % mailbox)
500 return typ, self.untagged_responses.get('EXISTS', [None])
503 def status(self, mailbox, names):
504 """Request named status conditions for mailbox.
506 (typ, [data]) = <instance>.status(mailbox, names)
508 name = 'STATUS'
509 if self.PROTOCOL_VERSION == 'IMAP4':
510 raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
511 typ, dat = self._simple_command(name, mailbox, names)
512 return self._untagged_response(typ, dat, name)
515 def store(self, message_set, command, flags):
516 """Alters flag dispositions for messages in mailbox.
518 (typ, [data]) = <instance>.store(message_set, command, flags)
520 if (flags[0],flags[-1]) != ('(',')'):
521 flags = '(%s)' % flags # Avoid quoting the flags
522 typ, dat = self._simple_command('STORE', message_set, command, flags)
523 return self._untagged_response(typ, dat, 'FETCH')
526 def subscribe(self, mailbox):
527 """Subscribe to new mailbox.
529 (typ, [data]) = <instance>.subscribe(mailbox)
531 return self._simple_command('SUBSCRIBE', mailbox)
534 def uid(self, command, *args):
535 """Execute "command arg ..." with messages identified by UID,
536 rather than message number.
538 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
540 Returns response appropriate to 'command'.
542 command = command.upper()
543 if not Commands.has_key(command):
544 raise self.error("Unknown IMAP4 UID command: %s" % command)
545 if self.state not in Commands[command]:
546 raise self.error('command %s illegal in state %s'
547 % (command, self.state))
548 name = 'UID'
549 typ, dat = apply(self._simple_command, (name, command) + args)
550 if command == 'SEARCH':
551 name = 'SEARCH'
552 else:
553 name = 'FETCH'
554 return self._untagged_response(typ, dat, name)
557 def unsubscribe(self, mailbox):
558 """Unsubscribe from old mailbox.
560 (typ, [data]) = <instance>.unsubscribe(mailbox)
562 return self._simple_command('UNSUBSCRIBE', mailbox)
565 def xatom(self, name, *args):
566 """Allow simple extension commands
567 notified by server in CAPABILITY response.
569 (typ, [data]) = <instance>.xatom(name, arg, ...)
571 if name[0] != 'X' or not name in self.capabilities:
572 raise self.error('unknown extension command: %s' % name)
573 return apply(self._simple_command, (name,) + args)
575 def namespace(self):
576 """ Returns IMAP namespaces ala rfc2342
578 name = 'NAMESPACE'
579 typ, dat = self._simple_command(name)
580 return self._untagged_response(typ, dat, name)
583 # Private methods
586 def _append_untagged(self, typ, dat):
588 if dat is None: dat = ''
589 ur = self.untagged_responses
590 if __debug__:
591 if self.debug >= 5:
592 _mesg('untagged_responses[%s] %s += ["%s"]' %
593 (typ, len(ur.get(typ,'')), dat))
594 if ur.has_key(typ):
595 ur[typ].append(dat)
596 else:
597 ur[typ] = [dat]
600 def _check_bye(self):
601 bye = self.untagged_responses.get('BYE')
602 if bye:
603 raise self.abort(bye[-1])
606 def _command(self, name, *args):
608 if self.state not in Commands[name]:
609 self.literal = None
610 raise self.error(
611 'command %s illegal in state %s' % (name, self.state))
613 for typ in ('OK', 'NO', 'BAD'):
614 if self.untagged_responses.has_key(typ):
615 del self.untagged_responses[typ]
617 if self.untagged_responses.has_key('READ-ONLY') \
618 and not self.is_readonly:
619 raise self.readonly('mailbox status changed to READ-ONLY')
621 tag = self._new_tag()
622 data = '%s %s' % (tag, name)
623 for arg in args:
624 if arg is None: continue
625 data = '%s %s' % (data, self._checkquote(arg))
627 literal = self.literal
628 if literal is not None:
629 self.literal = None
630 if type(literal) is type(self._command):
631 literator = literal
632 else:
633 literator = None
634 data = '%s {%s}' % (data, len(literal))
636 if __debug__:
637 if self.debug >= 4:
638 _mesg('> %s' % data)
639 else:
640 _log('> %s' % data)
642 try:
643 self.sock.send('%s%s' % (data, CRLF))
644 except socket.error, val:
645 raise self.abort('socket error: %s' % val)
647 if literal is None:
648 return tag
650 while 1:
651 # Wait for continuation response
653 while self._get_response():
654 if self.tagged_commands[tag]: # BAD/NO?
655 return tag
657 # Send literal
659 if literator:
660 literal = literator(self.continuation_response)
662 if __debug__:
663 if self.debug >= 4:
664 _mesg('write literal size %s' % len(literal))
666 try:
667 self.sock.send(literal)
668 self.sock.send(CRLF)
669 except socket.error, val:
670 raise self.abort('socket error: %s' % val)
672 if not literator:
673 break
675 return tag
678 def _command_complete(self, name, tag):
679 self._check_bye()
680 try:
681 typ, data = self._get_tagged_response(tag)
682 except self.abort, val:
683 raise self.abort('command: %s => %s' % (name, val))
684 except self.error, val:
685 raise self.error('command: %s => %s' % (name, val))
686 self._check_bye()
687 if typ == 'BAD':
688 raise self.error('%s command error: %s %s' % (name, typ, data))
689 return typ, data
692 def _get_response(self):
694 # Read response and store.
696 # Returns None for continuation responses,
697 # otherwise first response line received.
699 resp = self._get_line()
701 # Command completion response?
703 if self._match(self.tagre, resp):
704 tag = self.mo.group('tag')
705 if not self.tagged_commands.has_key(tag):
706 raise self.abort('unexpected tagged response: %s' % resp)
708 typ = self.mo.group('type')
709 dat = self.mo.group('data')
710 self.tagged_commands[tag] = (typ, [dat])
711 else:
712 dat2 = None
714 # '*' (untagged) responses?
716 if not self._match(Untagged_response, resp):
717 if self._match(Untagged_status, resp):
718 dat2 = self.mo.group('data2')
720 if self.mo is None:
721 # Only other possibility is '+' (continuation) response...
723 if self._match(Continuation, resp):
724 self.continuation_response = self.mo.group('data')
725 return None # NB: indicates continuation
727 raise self.abort("unexpected response: '%s'" % resp)
729 typ = self.mo.group('type')
730 dat = self.mo.group('data')
731 if dat is None: dat = '' # Null untagged response
732 if dat2: dat = dat + ' ' + dat2
734 # Is there a literal to come?
736 while self._match(Literal, dat):
738 # Read literal direct from connection.
740 size = int(self.mo.group('size'))
741 if __debug__:
742 if self.debug >= 4:
743 _mesg('read literal size %s' % size)
744 data = self.file.read(size)
746 # Store response with literal as tuple
748 self._append_untagged(typ, (dat, data))
750 # Read trailer - possibly containing another literal
752 dat = self._get_line()
754 self._append_untagged(typ, dat)
756 # Bracketed response information?
758 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
759 self._append_untagged(self.mo.group('type'), self.mo.group('data'))
761 if __debug__:
762 if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
763 _mesg('%s response: %s' % (typ, dat))
765 return resp
768 def _get_tagged_response(self, tag):
770 while 1:
771 result = self.tagged_commands[tag]
772 if result is not None:
773 del self.tagged_commands[tag]
774 return result
776 # Some have reported "unexpected response" exceptions.
777 # Note that ignoring them here causes loops.
778 # Instead, send me details of the unexpected response and
779 # I'll update the code in `_get_response()'.
781 try:
782 self._get_response()
783 except self.abort, val:
784 if __debug__:
785 if self.debug >= 1:
786 print_log()
787 raise
790 def _get_line(self):
792 line = self.file.readline()
793 if not line:
794 raise self.abort('socket error: EOF')
796 # Protocol mandates all lines terminated by CRLF
798 line = line[:-2]
799 if __debug__:
800 if self.debug >= 4:
801 _mesg('< %s' % line)
802 else:
803 _log('< %s' % line)
804 return line
807 def _match(self, cre, s):
809 # Run compiled regular expression match method on 's'.
810 # Save result, return success.
812 self.mo = cre.match(s)
813 if __debug__:
814 if self.mo is not None and self.debug >= 5:
815 _mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`))
816 return self.mo is not None
819 def _new_tag(self):
821 tag = '%s%s' % (self.tagpre, self.tagnum)
822 self.tagnum = self.tagnum + 1
823 self.tagged_commands[tag] = None
824 return tag
827 def _checkquote(self, arg):
829 # Must quote command args if non-alphanumeric chars present,
830 # and not already quoted.
832 if type(arg) is not type(''):
833 return arg
834 if (arg[0],arg[-1]) in (('(',')'),('"','"')):
835 return arg
836 if self.mustquote.search(arg) is None:
837 return arg
838 return self._quote(arg)
841 def _quote(self, arg):
843 arg = arg.replace('\\', '\\\\')
844 arg = arg.replace('"', '\\"')
846 return '"%s"' % arg
849 def _simple_command(self, name, *args):
851 return self._command_complete(name, apply(self._command, (name,) + args))
854 def _untagged_response(self, typ, dat, name):
856 if typ == 'NO':
857 return typ, dat
858 if not self.untagged_responses.has_key(name):
859 return typ, [None]
860 data = self.untagged_responses[name]
861 if __debug__:
862 if self.debug >= 5:
863 _mesg('untagged_responses[%s] => %s' % (name, data))
864 del self.untagged_responses[name]
865 return typ, data
869 class _Authenticator:
871 """Private class to provide en/decoding
872 for base64-based authentication conversation.
875 def __init__(self, mechinst):
876 self.mech = mechinst # Callable object to provide/process data
878 def process(self, data):
879 ret = self.mech(self.decode(data))
880 if ret is None:
881 return '*' # Abort conversation
882 return self.encode(ret)
884 def encode(self, inp):
886 # Invoke binascii.b2a_base64 iteratively with
887 # short even length buffers, strip the trailing
888 # line feed from the result and append. "Even"
889 # means a number that factors to both 6 and 8,
890 # so when it gets to the end of the 8-bit input
891 # there's no partial 6-bit output.
893 oup = ''
894 while inp:
895 if len(inp) > 48:
896 t = inp[:48]
897 inp = inp[48:]
898 else:
899 t = inp
900 inp = ''
901 e = binascii.b2a_base64(t)
902 if e:
903 oup = oup + e[:-1]
904 return oup
906 def decode(self, inp):
907 if not inp:
908 return ''
909 return binascii.a2b_base64(inp)
913 Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
914 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
916 def Internaldate2tuple(resp):
917 """Convert IMAP4 INTERNALDATE to UT.
919 Returns Python time module tuple.
922 mo = InternalDate.match(resp)
923 if not mo:
924 return None
926 mon = Mon2num[mo.group('mon')]
927 zonen = mo.group('zonen')
929 day = int(mo.group('day'))
930 year = int(mo.group('year'))
931 hour = int(mo.group('hour'))
932 min = int(mo.group('min'))
933 sec = int(mo.group('sec'))
934 zoneh = int(mo.group('zoneh'))
935 zonem = int(mo.group('zonem'))
937 # INTERNALDATE timezone must be subtracted to get UT
939 zone = (zoneh*60 + zonem)*60
940 if zonen == '-':
941 zone = -zone
943 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
945 utc = time.mktime(tt)
947 # Following is necessary because the time module has no 'mkgmtime'.
948 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
950 lt = time.localtime(utc)
951 if time.daylight and lt[-1]:
952 zone = zone + time.altzone
953 else:
954 zone = zone + time.timezone
956 return time.localtime(utc - zone)
960 def Int2AP(num):
962 """Convert integer to A-P string representation."""
964 val = ''; AP = 'ABCDEFGHIJKLMNOP'
965 num = int(abs(num))
966 while num:
967 num, mod = divmod(num, 16)
968 val = AP[mod] + val
969 return val
973 def ParseFlags(resp):
975 """Convert IMAP4 flags response to python tuple."""
977 mo = Flags.match(resp)
978 if not mo:
979 return ()
981 return tuple(mo.group('flags').split())
984 def Time2Internaldate(date_time):
986 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
988 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
991 dttype = type(date_time)
992 if dttype is type(1) or dttype is type(1.1):
993 tt = time.localtime(date_time)
994 elif dttype is type(()):
995 tt = date_time
996 elif dttype is type(""):
997 return date_time # Assume in correct format
998 else: raise ValueError
1000 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
1001 if dt[0] == '0':
1002 dt = ' ' + dt[1:]
1003 if time.daylight and tt[-1]:
1004 zone = -time.altzone
1005 else:
1006 zone = -time.timezone
1007 return '"' + dt + " %+02d%02d" % divmod(zone/60, 60) + '"'
1011 if __debug__:
1013 def _mesg(s, secs=None):
1014 if secs is None:
1015 secs = time.time()
1016 tm = time.strftime('%M:%S', time.localtime(secs))
1017 sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
1018 sys.stderr.flush()
1020 def _dump_ur(dict):
1021 # Dump untagged responses (in `dict').
1022 l = dict.items()
1023 if not l: return
1024 t = '\n\t\t'
1025 l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l)
1026 _mesg('untagged responses dump:%s%s' % (t, j(l, t)))
1028 _cmd_log = [] # Last `_cmd_log_len' interactions
1029 _cmd_log_len = 10
1031 def _log(line):
1032 # Keep log of last `_cmd_log_len' interactions for debugging.
1033 if len(_cmd_log) == _cmd_log_len:
1034 del _cmd_log[0]
1035 _cmd_log.append((time.time(), line))
1037 def print_log():
1038 _mesg('last %d IMAP4 interactions:' % len(_cmd_log))
1039 for secs,line in _cmd_log:
1040 _mesg(line, secs)
1044 if __name__ == '__main__':
1046 import getopt, getpass, sys
1048 try:
1049 optlist, args = getopt.getopt(sys.argv[1:], 'd:')
1050 except getopt.error, val:
1051 pass
1053 for opt,val in optlist:
1054 if opt == '-d':
1055 Debug = int(val)
1057 if not args: args = ('',)
1059 host = args[0]
1061 USER = getpass.getuser()
1062 PASSWD = getpass.getpass("IMAP password for %s on %s:" % (USER, host or "localhost"))
1064 test_mesg = 'From: %s@localhost\nSubject: IMAP4 test\n\ndata...\n' % USER
1065 test_seq1 = (
1066 ('login', (USER, PASSWD)),
1067 ('create', ('/tmp/xxx 1',)),
1068 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1069 ('CREATE', ('/tmp/yyz 2',)),
1070 ('append', ('/tmp/yyz 2', None, None, test_mesg)),
1071 ('list', ('/tmp', 'yy*')),
1072 ('select', ('/tmp/yyz 2',)),
1073 ('search', (None, 'SUBJECT', 'test')),
1074 ('partial', ('1', 'RFC822', 1, 1024)),
1075 ('store', ('1', 'FLAGS', '(\Deleted)')),
1076 ('expunge', ()),
1077 ('recent', ()),
1078 ('close', ()),
1081 test_seq2 = (
1082 ('select', ()),
1083 ('response',('UIDVALIDITY',)),
1084 ('uid', ('SEARCH', 'ALL')),
1085 ('response', ('EXISTS',)),
1086 ('append', (None, None, None, test_mesg)),
1087 ('recent', ()),
1088 ('logout', ()),
1091 def run(cmd, args):
1092 _mesg('%s %s' % (cmd, args))
1093 typ, dat = apply(eval('M.%s' % cmd), args)
1094 _mesg('%s => %s %s' % (cmd, typ, dat))
1095 return dat
1097 try:
1098 M = IMAP4(host)
1099 _mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
1101 for cmd,args in test_seq1:
1102 run(cmd, args)
1104 for ml in run('list', ('/tmp/', 'yy%')):
1105 mo = re.match(r'.*"([^"]+)"$', ml)
1106 if mo: path = mo.group(1)
1107 else: path = ml.split()[-1]
1108 run('delete', (path,))
1110 for cmd,args in test_seq2:
1111 dat = run(cmd, args)
1113 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
1114 continue
1116 uid = dat[-1].split()
1117 if not uid: continue
1118 run('uid', ('FETCH', '%s' % uid[-1],
1119 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1121 print '\nAll tests OK.'
1123 except:
1124 print '\nTests failed.'
1126 if not Debug:
1127 print '''
1128 If you would like to see debugging output,
1129 try: %s -d5
1130 ''' % sys.argv[0]
1132 raise