Updated for 2.1b2 distribution.
[python/dscho.git] / Lib / imaplib.py
blob2a0eeb6ff46448050633bc942e91ae622b9f1819
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'),
63 # Patterns to match server responses
65 Continuation = re.compile(r'\+( (?P<data>.*))?')
66 Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
67 InternalDate = re.compile(r'.*INTERNALDATE "'
68 r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
69 r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
70 r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
71 r'"')
72 Literal = re.compile(r'.*{(?P<size>\d+)}$')
73 Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
74 Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
75 Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
79 class IMAP4:
81 """IMAP4 client class.
83 Instantiate with: IMAP4([host[, port]])
85 host - host's name (default: localhost);
86 port - port number (default: standard IMAP4 port).
88 All IMAP4rev1 commands are supported by methods of the same
89 name (in lower-case).
91 All arguments to commands are converted to strings, except for
92 AUTHENTICATE, and the last argument to APPEND which is passed as
93 an IMAP4 literal. If necessary (the string contains any
94 non-printing characters or white-space and isn't enclosed with
95 either parentheses or double quotes) each string is quoted.
96 However, the 'password' argument to the LOGIN command is always
97 quoted. If you want to avoid having an argument string quoted
98 (eg: the 'flags' argument to STORE) then enclose the string in
99 parentheses (eg: "(\Deleted)").
101 Each command returns a tuple: (type, [data, ...]) where 'type'
102 is usually 'OK' or 'NO', and 'data' is either the text from the
103 tagged response, or untagged results from command.
105 Errors raise the exception class <instance>.error("<reason>").
106 IMAP4 server errors raise <instance>.abort("<reason>"),
107 which is a sub-class of 'error'. Mailbox status changes
108 from READ-WRITE to READ-ONLY raise the exception class
109 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
111 "error" exceptions imply a program error.
112 "abort" exceptions imply the connection should be reset, and
113 the command re-tried.
114 "readonly" exceptions imply the command should be re-tried.
116 Note: to use this module, you must read the RFCs pertaining
117 to the IMAP4 protocol, as the semantics of the arguments to
118 each IMAP4 command are left to the invoker, not to mention
119 the results.
122 class error(Exception): pass # Logical errors - debug required
123 class abort(error): pass # Service errors - close and retry
124 class readonly(abort): pass # Mailbox status changed to READ-ONLY
126 mustquote = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]")
128 def __init__(self, host = '', port = IMAP4_PORT):
129 self.host = host
130 self.port = port
131 self.debug = Debug
132 self.state = 'LOGOUT'
133 self.literal = None # A literal argument to a command
134 self.tagged_commands = {} # Tagged commands awaiting response
135 self.untagged_responses = {} # {typ: [data, ...], ...}
136 self.continuation_response = '' # Last continuation response
137 self.is_readonly = None # READ-ONLY desired state
138 self.tagnum = 0
140 # Open socket to server.
142 self.open(host, port)
144 # Create unique tag for this session,
145 # and compile tagged response matcher.
147 self.tagpre = Int2AP(random.randint(0, 31999))
148 self.tagre = re.compile(r'(?P<tag>'
149 + self.tagpre
150 + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
152 # Get server welcome message,
153 # request and store CAPABILITY response.
155 if __debug__:
156 if self.debug >= 1:
157 _mesg('new IMAP4 connection, tag=%s' % self.tagpre)
159 self.welcome = self._get_response()
160 if self.untagged_responses.has_key('PREAUTH'):
161 self.state = 'AUTH'
162 elif self.untagged_responses.has_key('OK'):
163 self.state = 'NONAUTH'
164 else:
165 raise self.error(self.welcome)
167 cap = 'CAPABILITY'
168 self._simple_command(cap)
169 if not self.untagged_responses.has_key(cap):
170 raise self.error('no CAPABILITY response from server')
171 self.capabilities = tuple(self.untagged_responses[cap][-1].upper().split())
173 if __debug__:
174 if self.debug >= 3:
175 _mesg('CAPABILITIES: %s' % `self.capabilities`)
177 for version in AllowedVersions:
178 if not version in self.capabilities:
179 continue
180 self.PROTOCOL_VERSION = version
181 return
183 raise self.error('server not IMAP4 compliant')
186 def __getattr__(self, attr):
187 # Allow UPPERCASE variants of IMAP4 command methods.
188 if Commands.has_key(attr):
189 return eval("self.%s" % attr.lower())
190 raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
194 # Public methods
197 def open(self, host, port):
198 """Setup 'self.sock' and 'self.file'."""
199 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
200 self.sock.connect((self.host, self.port))
201 self.file = self.sock.makefile('r')
204 def recent(self):
205 """Return most recent 'RECENT' responses if any exist,
206 else prompt server for an update using the 'NOOP' command.
208 (typ, [data]) = <instance>.recent()
210 'data' is None if no new messages,
211 else list of RECENT responses, most recent last.
213 name = 'RECENT'
214 typ, dat = self._untagged_response('OK', [None], name)
215 if dat[-1]:
216 return typ, dat
217 typ, dat = self.noop() # Prod server for response
218 return self._untagged_response(typ, dat, name)
221 def response(self, code):
222 """Return data for response 'code' if received, or None.
224 Old value for response 'code' is cleared.
226 (code, [data]) = <instance>.response(code)
228 return self._untagged_response(code, [None], code.upper())
231 def socket(self):
232 """Return socket instance used to connect to IMAP4 server.
234 socket = <instance>.socket()
236 return self.sock
240 # IMAP4 commands
243 def append(self, mailbox, flags, date_time, message):
244 """Append message to named mailbox.
246 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
248 All args except `message' can be None.
250 name = 'APPEND'
251 if not mailbox:
252 mailbox = 'INBOX'
253 if flags:
254 if (flags[0],flags[-1]) != ('(',')'):
255 flags = '(%s)' % flags
256 else:
257 flags = None
258 if date_time:
259 date_time = Time2Internaldate(date_time)
260 else:
261 date_time = None
262 self.literal = message
263 return self._simple_command(name, mailbox, flags, date_time)
266 def authenticate(self, mechanism, authobject):
267 """Authenticate command - requires response processing.
269 'mechanism' specifies which authentication mechanism is to
270 be used - it must appear in <instance>.capabilities in the
271 form AUTH=<mechanism>.
273 'authobject' must be a callable object:
275 data = authobject(response)
277 It will be called to process server continuation responses.
278 It should return data that will be encoded and sent to server.
279 It should return None if the client abort response '*' should
280 be sent instead.
282 mech = mechanism.upper()
283 cap = 'AUTH=%s' % mech
284 if not cap in self.capabilities:
285 raise self.error("Server doesn't allow %s authentication." % mech)
286 self.literal = _Authenticator(authobject).process
287 typ, dat = self._simple_command('AUTHENTICATE', mech)
288 if typ != 'OK':
289 raise self.error(dat[-1])
290 self.state = 'AUTH'
291 return typ, dat
294 def check(self):
295 """Checkpoint mailbox on server.
297 (typ, [data]) = <instance>.check()
299 return self._simple_command('CHECK')
302 def close(self):
303 """Close currently selected mailbox.
305 Deleted messages are removed from writable mailbox.
306 This is the recommended command before 'LOGOUT'.
308 (typ, [data]) = <instance>.close()
310 try:
311 typ, dat = self._simple_command('CLOSE')
312 finally:
313 self.state = 'AUTH'
314 return typ, dat
317 def copy(self, message_set, new_mailbox):
318 """Copy 'message_set' messages onto end of 'new_mailbox'.
320 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
322 return self._simple_command('COPY', message_set, new_mailbox)
325 def create(self, mailbox):
326 """Create new mailbox.
328 (typ, [data]) = <instance>.create(mailbox)
330 return self._simple_command('CREATE', mailbox)
333 def delete(self, mailbox):
334 """Delete old mailbox.
336 (typ, [data]) = <instance>.delete(mailbox)
338 return self._simple_command('DELETE', mailbox)
341 def expunge(self):
342 """Permanently remove deleted items from selected mailbox.
344 Generates 'EXPUNGE' response for each deleted message.
346 (typ, [data]) = <instance>.expunge()
348 'data' is list of 'EXPUNGE'd message numbers in order received.
350 name = 'EXPUNGE'
351 typ, dat = self._simple_command(name)
352 return self._untagged_response(typ, dat, name)
355 def fetch(self, message_set, message_parts):
356 """Fetch (parts of) messages.
358 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
360 'message_parts' should be a string of selected parts
361 enclosed in parentheses, eg: "(UID BODY[TEXT])".
363 'data' are tuples of message part envelope and data.
365 name = 'FETCH'
366 typ, dat = self._simple_command(name, message_set, message_parts)
367 return self._untagged_response(typ, dat, name)
370 def list(self, directory='""', pattern='*'):
371 """List mailbox names in directory matching pattern.
373 (typ, [data]) = <instance>.list(directory='""', pattern='*')
375 'data' is list of LIST responses.
377 name = 'LIST'
378 typ, dat = self._simple_command(name, directory, pattern)
379 return self._untagged_response(typ, dat, name)
382 def login(self, user, password):
383 """Identify client using plaintext password.
385 (typ, [data]) = <instance>.login(user, password)
387 NB: 'password' will be quoted.
389 #if not 'AUTH=LOGIN' in self.capabilities:
390 # raise self.error("Server doesn't allow LOGIN authentication." % mech)
391 typ, dat = self._simple_command('LOGIN', user, self._quote(password))
392 if typ != 'OK':
393 raise self.error(dat[-1])
394 self.state = 'AUTH'
395 return typ, dat
398 def logout(self):
399 """Shutdown connection to server.
401 (typ, [data]) = <instance>.logout()
403 Returns server 'BYE' response.
405 self.state = 'LOGOUT'
406 try: typ, dat = self._simple_command('LOGOUT')
407 except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
408 self.file.close()
409 self.sock.close()
410 if self.untagged_responses.has_key('BYE'):
411 return 'BYE', self.untagged_responses['BYE']
412 return typ, dat
415 def lsub(self, directory='""', pattern='*'):
416 """List 'subscribed' mailbox names in directory matching pattern.
418 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
420 'data' are tuples of message part envelope and data.
422 name = 'LSUB'
423 typ, dat = self._simple_command(name, directory, pattern)
424 return self._untagged_response(typ, dat, name)
427 def noop(self):
428 """Send NOOP command.
430 (typ, data) = <instance>.noop()
432 if __debug__:
433 if self.debug >= 3:
434 _dump_ur(self.untagged_responses)
435 return self._simple_command('NOOP')
438 def partial(self, message_num, message_part, start, length):
439 """Fetch truncated part of a message.
441 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
443 'data' is tuple of message part envelope and data.
445 name = 'PARTIAL'
446 typ, dat = self._simple_command(name, message_num, message_part, start, length)
447 return self._untagged_response(typ, dat, 'FETCH')
450 def rename(self, oldmailbox, newmailbox):
451 """Rename old mailbox name to new.
453 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
455 return self._simple_command('RENAME', oldmailbox, newmailbox)
458 def search(self, charset, *criteria):
459 """Search mailbox for matching messages.
461 (typ, [data]) = <instance>.search(charset, criterium, ...)
463 'data' is space separated list of matching message numbers.
465 name = 'SEARCH'
466 if charset:
467 charset = 'CHARSET ' + charset
468 typ, dat = apply(self._simple_command, (name, charset) + criteria)
469 return self._untagged_response(typ, dat, name)
472 def select(self, mailbox='INBOX', readonly=None):
473 """Select a mailbox.
475 Flush all untagged responses.
477 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
479 'data' is count of messages in mailbox ('EXISTS' response).
481 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
482 self.untagged_responses = {} # Flush old responses.
483 self.is_readonly = readonly
484 if readonly:
485 name = 'EXAMINE'
486 else:
487 name = 'SELECT'
488 typ, dat = self._simple_command(name, mailbox)
489 if typ != 'OK':
490 self.state = 'AUTH' # Might have been 'SELECTED'
491 return typ, dat
492 self.state = 'SELECTED'
493 if self.untagged_responses.has_key('READ-ONLY') \
494 and not readonly:
495 if __debug__:
496 if self.debug >= 1:
497 _dump_ur(self.untagged_responses)
498 raise self.readonly('%s is not writable' % mailbox)
499 return typ, self.untagged_responses.get('EXISTS', [None])
502 def status(self, mailbox, names):
503 """Request named status conditions for mailbox.
505 (typ, [data]) = <instance>.status(mailbox, names)
507 name = 'STATUS'
508 if self.PROTOCOL_VERSION == 'IMAP4':
509 raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
510 typ, dat = self._simple_command(name, mailbox, names)
511 return self._untagged_response(typ, dat, name)
514 def store(self, message_set, command, flags):
515 """Alters flag dispositions for messages in mailbox.
517 (typ, [data]) = <instance>.store(message_set, command, flags)
519 if (flags[0],flags[-1]) != ('(',')'):
520 flags = '(%s)' % flags # Avoid quoting the flags
521 typ, dat = self._simple_command('STORE', message_set, command, flags)
522 return self._untagged_response(typ, dat, 'FETCH')
525 def subscribe(self, mailbox):
526 """Subscribe to new mailbox.
528 (typ, [data]) = <instance>.subscribe(mailbox)
530 return self._simple_command('SUBSCRIBE', mailbox)
533 def uid(self, command, *args):
534 """Execute "command arg ..." with messages identified by UID,
535 rather than message number.
537 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
539 Returns response appropriate to 'command'.
541 command = command.upper()
542 if not Commands.has_key(command):
543 raise self.error("Unknown IMAP4 UID command: %s" % command)
544 if self.state not in Commands[command]:
545 raise self.error('command %s illegal in state %s'
546 % (command, self.state))
547 name = 'UID'
548 typ, dat = apply(self._simple_command, (name, command) + args)
549 if command == 'SEARCH':
550 name = 'SEARCH'
551 else:
552 name = 'FETCH'
553 return self._untagged_response(typ, dat, name)
556 def unsubscribe(self, mailbox):
557 """Unsubscribe from old mailbox.
559 (typ, [data]) = <instance>.unsubscribe(mailbox)
561 return self._simple_command('UNSUBSCRIBE', mailbox)
564 def xatom(self, name, *args):
565 """Allow simple extension commands
566 notified by server in CAPABILITY response.
568 (typ, [data]) = <instance>.xatom(name, arg, ...)
570 if name[0] != 'X' or not name in self.capabilities:
571 raise self.error('unknown extension command: %s' % name)
572 return apply(self._simple_command, (name,) + args)
576 # Private methods
579 def _append_untagged(self, typ, dat):
581 if dat is None: dat = ''
582 ur = self.untagged_responses
583 if __debug__:
584 if self.debug >= 5:
585 _mesg('untagged_responses[%s] %s += ["%s"]' %
586 (typ, len(ur.get(typ,'')), dat))
587 if ur.has_key(typ):
588 ur[typ].append(dat)
589 else:
590 ur[typ] = [dat]
593 def _check_bye(self):
594 bye = self.untagged_responses.get('BYE')
595 if bye:
596 raise self.abort(bye[-1])
599 def _command(self, name, *args):
601 if self.state not in Commands[name]:
602 self.literal = None
603 raise self.error(
604 'command %s illegal in state %s' % (name, self.state))
606 for typ in ('OK', 'NO', 'BAD'):
607 if self.untagged_responses.has_key(typ):
608 del self.untagged_responses[typ]
610 if self.untagged_responses.has_key('READ-ONLY') \
611 and not self.is_readonly:
612 raise self.readonly('mailbox status changed to READ-ONLY')
614 tag = self._new_tag()
615 data = '%s %s' % (tag, name)
616 for arg in args:
617 if arg is None: continue
618 data = '%s %s' % (data, self._checkquote(arg))
620 literal = self.literal
621 if literal is not None:
622 self.literal = None
623 if type(literal) is type(self._command):
624 literator = literal
625 else:
626 literator = None
627 data = '%s {%s}' % (data, len(literal))
629 if __debug__:
630 if self.debug >= 4:
631 _mesg('> %s' % data)
632 else:
633 _log('> %s' % data)
635 try:
636 self.sock.send('%s%s' % (data, CRLF))
637 except socket.error, val:
638 raise self.abort('socket error: %s' % val)
640 if literal is None:
641 return tag
643 while 1:
644 # Wait for continuation response
646 while self._get_response():
647 if self.tagged_commands[tag]: # BAD/NO?
648 return tag
650 # Send literal
652 if literator:
653 literal = literator(self.continuation_response)
655 if __debug__:
656 if self.debug >= 4:
657 _mesg('write literal size %s' % len(literal))
659 try:
660 self.sock.send(literal)
661 self.sock.send(CRLF)
662 except socket.error, val:
663 raise self.abort('socket error: %s' % val)
665 if not literator:
666 break
668 return tag
671 def _command_complete(self, name, tag):
672 self._check_bye()
673 try:
674 typ, data = self._get_tagged_response(tag)
675 except self.abort, val:
676 raise self.abort('command: %s => %s' % (name, val))
677 except self.error, val:
678 raise self.error('command: %s => %s' % (name, val))
679 self._check_bye()
680 if typ == 'BAD':
681 raise self.error('%s command error: %s %s' % (name, typ, data))
682 return typ, data
685 def _get_response(self):
687 # Read response and store.
689 # Returns None for continuation responses,
690 # otherwise first response line received.
692 resp = self._get_line()
694 # Command completion response?
696 if self._match(self.tagre, resp):
697 tag = self.mo.group('tag')
698 if not self.tagged_commands.has_key(tag):
699 raise self.abort('unexpected tagged response: %s' % resp)
701 typ = self.mo.group('type')
702 dat = self.mo.group('data')
703 self.tagged_commands[tag] = (typ, [dat])
704 else:
705 dat2 = None
707 # '*' (untagged) responses?
709 if not self._match(Untagged_response, resp):
710 if self._match(Untagged_status, resp):
711 dat2 = self.mo.group('data2')
713 if self.mo is None:
714 # Only other possibility is '+' (continuation) response...
716 if self._match(Continuation, resp):
717 self.continuation_response = self.mo.group('data')
718 return None # NB: indicates continuation
720 raise self.abort("unexpected response: '%s'" % resp)
722 typ = self.mo.group('type')
723 dat = self.mo.group('data')
724 if dat is None: dat = '' # Null untagged response
725 if dat2: dat = dat + ' ' + dat2
727 # Is there a literal to come?
729 while self._match(Literal, dat):
731 # Read literal direct from connection.
733 size = int(self.mo.group('size'))
734 if __debug__:
735 if self.debug >= 4:
736 _mesg('read literal size %s' % size)
737 data = self.file.read(size)
739 # Store response with literal as tuple
741 self._append_untagged(typ, (dat, data))
743 # Read trailer - possibly containing another literal
745 dat = self._get_line()
747 self._append_untagged(typ, dat)
749 # Bracketed response information?
751 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
752 self._append_untagged(self.mo.group('type'), self.mo.group('data'))
754 if __debug__:
755 if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
756 _mesg('%s response: %s' % (typ, dat))
758 return resp
761 def _get_tagged_response(self, tag):
763 while 1:
764 result = self.tagged_commands[tag]
765 if result is not None:
766 del self.tagged_commands[tag]
767 return result
769 # Some have reported "unexpected response" exceptions.
770 # Note that ignoring them here causes loops.
771 # Instead, send me details of the unexpected response and
772 # I'll update the code in `_get_response()'.
774 try:
775 self._get_response()
776 except self.abort, val:
777 if __debug__:
778 if self.debug >= 1:
779 print_log()
780 raise
783 def _get_line(self):
785 line = self.file.readline()
786 if not line:
787 raise self.abort('socket error: EOF')
789 # Protocol mandates all lines terminated by CRLF
791 line = line[:-2]
792 if __debug__:
793 if self.debug >= 4:
794 _mesg('< %s' % line)
795 else:
796 _log('< %s' % line)
797 return line
800 def _match(self, cre, s):
802 # Run compiled regular expression match method on 's'.
803 # Save result, return success.
805 self.mo = cre.match(s)
806 if __debug__:
807 if self.mo is not None and self.debug >= 5:
808 _mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`))
809 return self.mo is not None
812 def _new_tag(self):
814 tag = '%s%s' % (self.tagpre, self.tagnum)
815 self.tagnum = self.tagnum + 1
816 self.tagged_commands[tag] = None
817 return tag
820 def _checkquote(self, arg):
822 # Must quote command args if non-alphanumeric chars present,
823 # and not already quoted.
825 if type(arg) is not type(''):
826 return arg
827 if (arg[0],arg[-1]) in (('(',')'),('"','"')):
828 return arg
829 if self.mustquote.search(arg) is None:
830 return arg
831 return self._quote(arg)
834 def _quote(self, arg):
836 arg = arg.replace('\\', '\\\\')
837 arg = arg.replace('"', '\\"')
839 return '"%s"' % arg
842 def _simple_command(self, name, *args):
844 return self._command_complete(name, apply(self._command, (name,) + args))
847 def _untagged_response(self, typ, dat, name):
849 if typ == 'NO':
850 return typ, dat
851 if not self.untagged_responses.has_key(name):
852 return typ, [None]
853 data = self.untagged_responses[name]
854 if __debug__:
855 if self.debug >= 5:
856 _mesg('untagged_responses[%s] => %s' % (name, data))
857 del self.untagged_responses[name]
858 return typ, data
862 class _Authenticator:
864 """Private class to provide en/decoding
865 for base64-based authentication conversation.
868 def __init__(self, mechinst):
869 self.mech = mechinst # Callable object to provide/process data
871 def process(self, data):
872 ret = self.mech(self.decode(data))
873 if ret is None:
874 return '*' # Abort conversation
875 return self.encode(ret)
877 def encode(self, inp):
879 # Invoke binascii.b2a_base64 iteratively with
880 # short even length buffers, strip the trailing
881 # line feed from the result and append. "Even"
882 # means a number that factors to both 6 and 8,
883 # so when it gets to the end of the 8-bit input
884 # there's no partial 6-bit output.
886 oup = ''
887 while inp:
888 if len(inp) > 48:
889 t = inp[:48]
890 inp = inp[48:]
891 else:
892 t = inp
893 inp = ''
894 e = binascii.b2a_base64(t)
895 if e:
896 oup = oup + e[:-1]
897 return oup
899 def decode(self, inp):
900 if not inp:
901 return ''
902 return binascii.a2b_base64(inp)
906 Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
907 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
909 def Internaldate2tuple(resp):
910 """Convert IMAP4 INTERNALDATE to UT.
912 Returns Python time module tuple.
915 mo = InternalDate.match(resp)
916 if not mo:
917 return None
919 mon = Mon2num[mo.group('mon')]
920 zonen = mo.group('zonen')
922 day = int(mo.group('day'))
923 year = int(mo.group('year'))
924 hour = int(mo.group('hour'))
925 min = int(mo.group('min'))
926 sec = int(mo.group('sec'))
927 zoneh = int(mo.group('zoneh'))
928 zonem = int(mo.group('zonem'))
930 # INTERNALDATE timezone must be subtracted to get UT
932 zone = (zoneh*60 + zonem)*60
933 if zonen == '-':
934 zone = -zone
936 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
938 utc = time.mktime(tt)
940 # Following is necessary because the time module has no 'mkgmtime'.
941 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
943 lt = time.localtime(utc)
944 if time.daylight and lt[-1]:
945 zone = zone + time.altzone
946 else:
947 zone = zone + time.timezone
949 return time.localtime(utc - zone)
953 def Int2AP(num):
955 """Convert integer to A-P string representation."""
957 val = ''; AP = 'ABCDEFGHIJKLMNOP'
958 num = int(abs(num))
959 while num:
960 num, mod = divmod(num, 16)
961 val = AP[mod] + val
962 return val
966 def ParseFlags(resp):
968 """Convert IMAP4 flags response to python tuple."""
970 mo = Flags.match(resp)
971 if not mo:
972 return ()
974 return tuple(mo.group('flags').split())
977 def Time2Internaldate(date_time):
979 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
981 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
984 dttype = type(date_time)
985 if dttype is type(1) or dttype is type(1.1):
986 tt = time.localtime(date_time)
987 elif dttype is type(()):
988 tt = date_time
989 elif dttype is type(""):
990 return date_time # Assume in correct format
991 else: raise ValueError
993 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
994 if dt[0] == '0':
995 dt = ' ' + dt[1:]
996 if time.daylight and tt[-1]:
997 zone = -time.altzone
998 else:
999 zone = -time.timezone
1000 return '"' + dt + " %+02d%02d" % divmod(zone/60, 60) + '"'
1004 if __debug__:
1006 def _mesg(s, secs=None):
1007 if secs is None:
1008 secs = time.time()
1009 tm = time.strftime('%M:%S', time.localtime(secs))
1010 sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
1011 sys.stderr.flush()
1013 def _dump_ur(dict):
1014 # Dump untagged responses (in `dict').
1015 l = dict.items()
1016 if not l: return
1017 t = '\n\t\t'
1018 l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l)
1019 _mesg('untagged responses dump:%s%s' % (t, j(l, t)))
1021 _cmd_log = [] # Last `_cmd_log_len' interactions
1022 _cmd_log_len = 10
1024 def _log(line):
1025 # Keep log of last `_cmd_log_len' interactions for debugging.
1026 if len(_cmd_log) == _cmd_log_len:
1027 del _cmd_log[0]
1028 _cmd_log.append((time.time(), line))
1030 def print_log():
1031 _mesg('last %d IMAP4 interactions:' % len(_cmd_log))
1032 for secs,line in _cmd_log:
1033 _mesg(line, secs)
1037 if __name__ == '__main__':
1039 import getopt, getpass, sys
1041 try:
1042 optlist, args = getopt.getopt(sys.argv[1:], 'd:')
1043 except getopt.error, val:
1044 pass
1046 for opt,val in optlist:
1047 if opt == '-d':
1048 Debug = int(val)
1050 if not args: args = ('',)
1052 host = args[0]
1054 USER = getpass.getuser()
1055 PASSWD = getpass.getpass("IMAP password for %s on %s:" % (USER, host or "localhost"))
1057 test_mesg = 'From: %s@localhost\nSubject: IMAP4 test\n\ndata...\n' % USER
1058 test_seq1 = (
1059 ('login', (USER, PASSWD)),
1060 ('create', ('/tmp/xxx 1',)),
1061 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1062 ('CREATE', ('/tmp/yyz 2',)),
1063 ('append', ('/tmp/yyz 2', None, None, test_mesg)),
1064 ('list', ('/tmp', 'yy*')),
1065 ('select', ('/tmp/yyz 2',)),
1066 ('search', (None, 'SUBJECT', 'test')),
1067 ('partial', ('1', 'RFC822', 1, 1024)),
1068 ('store', ('1', 'FLAGS', '(\Deleted)')),
1069 ('expunge', ()),
1070 ('recent', ()),
1071 ('close', ()),
1074 test_seq2 = (
1075 ('select', ()),
1076 ('response',('UIDVALIDITY',)),
1077 ('uid', ('SEARCH', 'ALL')),
1078 ('response', ('EXISTS',)),
1079 ('append', (None, None, None, test_mesg)),
1080 ('recent', ()),
1081 ('logout', ()),
1084 def run(cmd, args):
1085 _mesg('%s %s' % (cmd, args))
1086 typ, dat = apply(eval('M.%s' % cmd), args)
1087 _mesg('%s => %s %s' % (cmd, typ, dat))
1088 return dat
1090 try:
1091 M = IMAP4(host)
1092 _mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
1094 for cmd,args in test_seq1:
1095 run(cmd, args)
1097 for ml in run('list', ('/tmp/', 'yy%')):
1098 mo = re.match(r'.*"([^"]+)"$', ml)
1099 if mo: path = mo.group(1)
1100 else: path = ml.split()[-1]
1101 run('delete', (path,))
1103 for cmd,args in test_seq2:
1104 dat = run(cmd, args)
1106 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
1107 continue
1109 uid = dat[-1].split()
1110 if not uid: continue
1111 run('uid', ('FETCH', '%s' % uid[-1],
1112 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1114 print '\nAll tests OK.'
1116 except:
1117 print '\nTests failed.'
1119 if not Debug:
1120 print '''
1121 If you would like to see debugging output,
1122 try: %s -d5
1123 ''' % sys.argv[0]
1125 raise