Fix an amazing number of typos & malformed sentences reported by Detlef
[python/dscho.git] / Lib / imaplib.py
blobcf79449c02047e2b5f46ff7a00dbd8e08fd7c143
2 """IMAP4 client.
4 Based on RFC 2060.
6 Author: Piers Lauder <piers@cs.su.oz.au> December 1997.
8 Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
10 Public class: IMAP4
11 Public variable: Debug
12 Public functions: Internaldate2tuple
13 Int2AP
14 ParseFlags
15 Time2Internaldate
16 """
18 __version__ = "2.15"
20 import binascii, re, socket, string, time, random, sys
22 # Globals
24 CRLF = '\r\n'
25 Debug = 0
26 IMAP4_PORT = 143
27 AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first
29 # Commands
31 Commands = {
32 # name valid states
33 'APPEND': ('AUTH', 'SELECTED'),
34 'AUTHENTICATE': ('NONAUTH',),
35 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
36 'CHECK': ('SELECTED',),
37 'CLOSE': ('SELECTED',),
38 'COPY': ('SELECTED',),
39 'CREATE': ('AUTH', 'SELECTED'),
40 'DELETE': ('AUTH', 'SELECTED'),
41 'EXAMINE': ('AUTH', 'SELECTED'),
42 'EXPUNGE': ('SELECTED',),
43 'FETCH': ('SELECTED',),
44 'LIST': ('AUTH', 'SELECTED'),
45 'LOGIN': ('NONAUTH',),
46 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
47 'LSUB': ('AUTH', 'SELECTED'),
48 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
49 'PARTIAL': ('SELECTED',),
50 'RENAME': ('AUTH', 'SELECTED'),
51 'SEARCH': ('SELECTED',),
52 'SELECT': ('AUTH', 'SELECTED'),
53 'STATUS': ('AUTH', 'SELECTED'),
54 'STORE': ('SELECTED',),
55 'SUBSCRIBE': ('AUTH', 'SELECTED'),
56 'UID': ('SELECTED',),
57 'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
60 # Patterns to match server responses
62 Continuation = re.compile(r'\+( (?P<data>.*))?')
63 Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
64 InternalDate = re.compile(r'.*INTERNALDATE "'
65 r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
66 r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
67 r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
68 r'"')
69 Literal = re.compile(r'(?P<data>.*) {(?P<size>\d+)}$')
70 Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
71 Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
72 Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
76 class IMAP4:
78 """IMAP4 client class.
80 Instantiate with: IMAP4([host[, port]])
82 host - host's name (default: localhost);
83 port - port number (default: standard IMAP4 port).
85 All IMAP4rev1 commands are supported by methods of the same
86 name (in lower-case).
88 All arguments to commands are converted to strings, except for
89 AUTHENTICATE, and the last argument to APPEND which is passed as
90 an IMAP4 literal. If necessary (the string contains
91 white-space and isn't enclosed with either parentheses or
92 double quotes) each string is quoted.
94 Each command returns a tuple: (type, [data, ...]) where 'type'
95 is usually 'OK' or 'NO', and 'data' is either the text from the
96 tagged response, or untagged results from command.
98 Errors raise the exception class <instance>.error("<reason>").
99 IMAP4 server errors raise <instance>.abort("<reason>"),
100 which is a sub-class of 'error'. Mailbox status changes
101 from READ-WRITE to READ-ONLY raise the exception class
102 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
104 Note: to use this module, you must read the RFCs pertaining
105 to the IMAP4 protocol, as the semantics of the arguments to
106 each IMAP4 command are left to the invoker, not to mention
107 the results.
110 class error(Exception): pass # Logical errors - debug required
111 class abort(error): pass # Service errors - close and retry
112 class readonly(abort): pass # Mailbox status changed to READ-ONLY
115 def __init__(self, host = '', port = IMAP4_PORT):
116 self.host = host
117 self.port = port
118 self.debug = Debug
119 self.state = 'LOGOUT'
120 self.literal = None # A literal argument to a command
121 self.tagged_commands = {} # Tagged commands awaiting response
122 self.untagged_responses = {} # {typ: [data, ...], ...}
123 self.continuation_response = '' # Last continuation response
124 self.tagnum = 0
126 # Open socket to server.
128 self.open(host, port)
130 # Create unique tag for this session,
131 # and compile tagged response matcher.
133 self.tagpre = Int2AP(random.randint(0, 31999))
134 self.tagre = re.compile(r'(?P<tag>'
135 + self.tagpre
136 + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
138 # Get server welcome message,
139 # request and store CAPABILITY response.
141 if __debug__ and self.debug >= 1:
142 _mesg('new IMAP4 connection, tag=%s' % self.tagpre)
144 self.welcome = self._get_response()
145 if self.untagged_responses.has_key('PREAUTH'):
146 self.state = 'AUTH'
147 elif self.untagged_responses.has_key('OK'):
148 self.state = 'NONAUTH'
149 # elif self.untagged_responses.has_key('BYE'):
150 else:
151 raise self.error(self.welcome)
153 cap = 'CAPABILITY'
154 self._simple_command(cap)
155 if not self.untagged_responses.has_key(cap):
156 raise self.error('no CAPABILITY response from server')
157 self.capabilities = tuple(string.split(string.upper(self.untagged_responses[cap][-1])))
159 if __debug__ and self.debug >= 3:
160 _mesg('CAPABILITIES: %s' % `self.capabilities`)
162 for version in AllowedVersions:
163 if not version in self.capabilities:
164 continue
165 self.PROTOCOL_VERSION = version
166 return
168 raise self.error('server not IMAP4 compliant')
171 def __getattr__(self, attr):
172 # Allow UPPERCASE variants of IMAP4 command methods.
173 if Commands.has_key(attr):
174 return eval("self.%s" % string.lower(attr))
175 raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
179 # Public methods
182 def open(self, host, port):
183 """Setup 'self.sock' and 'self.file'."""
184 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
185 self.sock.connect(self.host, self.port)
186 self.file = self.sock.makefile('r')
189 def recent(self):
190 """Return most recent 'RECENT' responses if any exist,
191 else prompt server for an update using the 'NOOP' command.
193 (typ, [data]) = <instance>.recent()
195 'data' is None if no new messages,
196 else list of RECENT responses, most recent last.
198 name = 'RECENT'
199 typ, dat = self._untagged_response('OK', [None], name)
200 if dat[-1]:
201 return typ, dat
202 typ, dat = self.noop() # Prod server for response
203 return self._untagged_response(typ, dat, name)
206 def response(self, code):
207 """Return data for response 'code' if received, or None.
209 Old value for response 'code' is cleared.
211 (code, [data]) = <instance>.response(code)
213 return self._untagged_response(code, [None], string.upper(code))
216 def socket(self):
217 """Return socket instance used to connect to IMAP4 server.
219 socket = <instance>.socket()
221 return self.sock
225 # IMAP4 commands
228 def append(self, mailbox, flags, date_time, message):
229 """Append message to named mailbox.
231 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
233 name = 'APPEND'
234 if flags:
235 if (flags[0],flags[-1]) != ('(',')'):
236 flags = '(%s)' % flags
237 else:
238 flags = None
239 if date_time:
240 date_time = Time2Internaldate(date_time)
241 else:
242 date_time = None
243 self.literal = message
244 return self._simple_command(name, mailbox, flags, date_time)
247 def authenticate(self, mechanism, authobject):
248 """Authenticate command - requires response processing.
250 'mechanism' specifies which authentication mechanism is to
251 be used - it must appear in <instance>.capabilities in the
252 form AUTH=<mechanism>.
254 'authobject' must be a callable object:
256 data = authobject(response)
258 It will be called to process server continuation responses.
259 It should return data that will be encoded and sent to server.
260 It should return None if the client abort response '*' should
261 be sent instead.
263 mech = string.upper(mechanism)
264 cap = 'AUTH=%s' % mech
265 if not cap in self.capabilities:
266 raise self.error("Server doesn't allow %s authentication." % mech)
267 self.literal = _Authenticator(authobject).process
268 typ, dat = self._simple_command('AUTHENTICATE', mech)
269 if typ != 'OK':
270 raise self.error(dat[-1])
271 self.state = 'AUTH'
272 return typ, dat
275 def check(self):
276 """Checkpoint mailbox on server.
278 (typ, [data]) = <instance>.check()
280 return self._simple_command('CHECK')
283 def close(self):
284 """Close currently selected mailbox.
286 Deleted messages are removed from writable mailbox.
287 This is the recommended command before 'LOGOUT'.
289 (typ, [data]) = <instance>.close()
291 try:
292 typ, dat = self._simple_command('CLOSE')
293 finally:
294 self.state = 'AUTH'
295 return typ, dat
298 def copy(self, message_set, new_mailbox):
299 """Copy 'message_set' messages onto end of 'new_mailbox'.
301 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
303 return self._simple_command('COPY', message_set, new_mailbox)
306 def create(self, mailbox):
307 """Create new mailbox.
309 (typ, [data]) = <instance>.create(mailbox)
311 return self._simple_command('CREATE', mailbox)
314 def delete(self, mailbox):
315 """Delete old mailbox.
317 (typ, [data]) = <instance>.delete(mailbox)
319 return self._simple_command('DELETE', mailbox)
322 def expunge(self):
323 """Permanently remove deleted items from selected mailbox.
325 Generates 'EXPUNGE' response for each deleted message.
327 (typ, [data]) = <instance>.expunge()
329 'data' is list of 'EXPUNGE'd message numbers in order received.
331 name = 'EXPUNGE'
332 typ, dat = self._simple_command(name)
333 return self._untagged_response(typ, dat, name)
336 def fetch(self, message_set, message_parts):
337 """Fetch (parts of) messages.
339 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
341 'data' are tuples of message part envelope and data.
343 name = 'FETCH'
344 typ, dat = self._simple_command(name, message_set, message_parts)
345 return self._untagged_response(typ, dat, name)
348 def list(self, directory='""', pattern='*'):
349 """List mailbox names in directory matching pattern.
351 (typ, [data]) = <instance>.list(directory='""', pattern='*')
353 'data' is list of LIST responses.
355 name = 'LIST'
356 typ, dat = self._simple_command(name, directory, pattern)
357 return self._untagged_response(typ, dat, name)
360 def login(self, user, password):
361 """Identify client using plaintext password.
363 (typ, [data]) = <instance>.list(user, password)
365 typ, dat = self._simple_command('LOGIN', user, password)
366 if typ != 'OK':
367 raise self.error(dat[-1])
368 self.state = 'AUTH'
369 return typ, dat
372 def logout(self):
373 """Shutdown connection to server.
375 (typ, [data]) = <instance>.logout()
377 Returns server 'BYE' response.
379 self.state = 'LOGOUT'
380 try: typ, dat = self._simple_command('LOGOUT')
381 except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
382 self.file.close()
383 self.sock.close()
384 if self.untagged_responses.has_key('BYE'):
385 return 'BYE', self.untagged_responses['BYE']
386 return typ, dat
389 def lsub(self, directory='""', pattern='*'):
390 """List 'subscribed' mailbox names in directory matching pattern.
392 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
394 'data' are tuples of message part envelope and data.
396 name = 'LSUB'
397 typ, dat = self._simple_command(name, directory, pattern)
398 return self._untagged_response(typ, dat, name)
401 def noop(self):
402 """Send NOOP command.
404 (typ, data) = <instance>.noop()
406 if __debug__ and self.debug >= 3:
407 _dump_ur(self.untagged_responses)
408 return self._simple_command('NOOP')
411 def partial(self, message_num, message_part, start, length):
412 """Fetch truncated part of a message.
414 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
416 'data' is tuple of message part envelope and data.
418 name = 'PARTIAL'
419 typ, dat = self._simple_command(name, message_num, message_part, start, length)
420 return self._untagged_response(typ, dat, 'FETCH')
423 def rename(self, oldmailbox, newmailbox):
424 """Rename old mailbox name to new.
426 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
428 return self._simple_command('RENAME', oldmailbox, newmailbox)
431 def search(self, charset, criteria):
432 """Search mailbox for matching messages.
434 (typ, [data]) = <instance>.search(charset, criteria)
436 'data' is space separated list of matching message numbers.
438 name = 'SEARCH'
439 if charset:
440 charset = 'CHARSET ' + charset
441 typ, dat = self._simple_command(name, charset, criteria)
442 return self._untagged_response(typ, dat, name)
445 def select(self, mailbox='INBOX', readonly=None):
446 """Select a mailbox.
448 Flush all untagged responses.
450 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
452 'data' is count of messages in mailbox ('EXISTS' response).
454 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
455 self.untagged_responses = {} # Flush old responses.
456 if readonly:
457 name = 'EXAMINE'
458 else:
459 name = 'SELECT'
460 typ, dat = self._simple_command(name, mailbox)
461 if typ != 'OK':
462 self.state = 'AUTH' # Might have been 'SELECTED'
463 return typ, dat
464 self.state = 'SELECTED'
465 if not self.untagged_responses.has_key('READ-WRITE') \
466 and not readonly:
467 if __debug__ and self.debug >= 1: _dump_ur(self.untagged_responses)
468 raise self.readonly('%s is not writable' % mailbox)
469 return typ, self.untagged_responses.get('EXISTS', [None])
472 def status(self, mailbox, names):
473 """Request named status conditions for mailbox.
475 (typ, [data]) = <instance>.status(mailbox, names)
477 name = 'STATUS'
478 if self.PROTOCOL_VERSION == 'IMAP4':
479 raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
480 typ, dat = self._simple_command(name, mailbox, names)
481 return self._untagged_response(typ, dat, name)
484 def store(self, message_set, command, flag_list):
485 """Alters flag dispositions for messages in mailbox.
487 (typ, [data]) = <instance>.store(message_set, command, flag_list)
489 typ, dat = self._simple_command('STORE', message_set, command, flag_list)
490 return self._untagged_response(typ, dat, 'FETCH')
493 def subscribe(self, mailbox):
494 """Subscribe to new mailbox.
496 (typ, [data]) = <instance>.subscribe(mailbox)
498 return self._simple_command('SUBSCRIBE', mailbox)
501 def uid(self, command, *args):
502 """Execute "command arg ..." with messages identified by UID,
503 rather than message number.
505 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
507 Returns response appropriate to 'command'.
509 command = string.upper(command)
510 if not Commands.has_key(command):
511 raise self.error("Unknown IMAP4 UID command: %s" % command)
512 if self.state not in Commands[command]:
513 raise self.error('command %s illegal in state %s'
514 % (command, self.state))
515 name = 'UID'
516 typ, dat = apply(self._simple_command, (name, command) + args)
517 if command == 'SEARCH':
518 name = 'SEARCH'
519 else:
520 name = 'FETCH'
521 return self._untagged_response(typ, dat, name)
524 def unsubscribe(self, mailbox):
525 """Unsubscribe from old mailbox.
527 (typ, [data]) = <instance>.unsubscribe(mailbox)
529 return self._simple_command('UNSUBSCRIBE', mailbox)
532 def xatom(self, name, *args):
533 """Allow simple extension commands
534 notified by server in CAPABILITY response.
536 (typ, [data]) = <instance>.xatom(name, arg, ...)
538 if name[0] != 'X' or not name in self.capabilities:
539 raise self.error('unknown extension command: %s' % name)
540 return apply(self._simple_command, (name,) + args)
544 # Private methods
547 def _append_untagged(self, typ, dat):
549 ur = self.untagged_responses
550 if __debug__ and self.debug >= 5:
551 _mesg('untagged_responses[%s] %s += %s' %
552 (typ, len(ur.get(typ,'')), dat))
553 if ur.has_key(typ):
554 ur[typ].append(dat)
555 else:
556 ur[typ] = [dat]
559 def _command(self, name, *args):
561 if self.state not in Commands[name]:
562 self.literal = None
563 raise self.error(
564 'command %s illegal in state %s' % (name, self.state))
566 for typ in ('OK', 'NO', 'BAD'):
567 if self.untagged_responses.has_key(typ):
568 del self.untagged_responses[typ]
570 if self.untagged_responses.has_key('READ-WRITE') \
571 and self.untagged_responses.has_key('READ-ONLY'):
572 del self.untagged_responses['READ-WRITE']
573 raise self.readonly('mailbox status changed to READ-ONLY')
575 tag = self._new_tag()
576 data = '%s %s' % (tag, name)
577 for d in args:
578 if d is None: continue
579 if type(d) is type(''):
580 l = len(string.split(d))
581 else:
582 l = 1
583 if l == 0 or l > 1 and (d[0],d[-1]) not in (('(',')'),('"','"')):
584 data = '%s "%s"' % (data, d)
585 else:
586 data = '%s %s' % (data, d)
588 literal = self.literal
589 if literal is not None:
590 self.literal = None
591 if type(literal) is type(self._command):
592 literator = literal
593 else:
594 literator = None
595 data = '%s {%s}' % (data, len(literal))
597 try:
598 self.sock.send('%s%s' % (data, CRLF))
599 except socket.error, val:
600 raise self.abort('socket error: %s' % val)
602 if __debug__ and self.debug >= 4:
603 _mesg('> %s' % data)
605 if literal is None:
606 return tag
608 while 1:
609 # Wait for continuation response
611 while self._get_response():
612 if self.tagged_commands[tag]: # BAD/NO?
613 return tag
615 # Send literal
617 if literator:
618 literal = literator(self.continuation_response)
620 if __debug__ and self.debug >= 4:
621 _mesg('write literal size %s' % len(literal))
623 try:
624 self.sock.send(literal)
625 self.sock.send(CRLF)
626 except socket.error, val:
627 raise self.abort('socket error: %s' % val)
629 if not literator:
630 break
632 return tag
635 def _command_complete(self, name, tag):
636 try:
637 typ, data = self._get_tagged_response(tag)
638 except self.abort, val:
639 raise self.abort('command: %s => %s' % (name, val))
640 except self.error, val:
641 raise self.error('command: %s => %s' % (name, val))
642 if self.untagged_responses.has_key('BYE') and name != 'LOGOUT':
643 raise self.abort(self.untagged_responses['BYE'][-1])
644 if typ == 'BAD':
645 raise self.error('%s command error: %s %s' % (name, typ, data))
646 return typ, data
649 def _get_response(self):
651 # Read response and store.
653 # Returns None for continuation responses,
654 # otherwise first response line received.
656 resp = self._get_line()
658 # Command completion response?
660 if self._match(self.tagre, resp):
661 tag = self.mo.group('tag')
662 if not self.tagged_commands.has_key(tag):
663 raise self.abort('unexpected tagged response: %s' % resp)
665 typ = self.mo.group('type')
666 dat = self.mo.group('data')
667 self.tagged_commands[tag] = (typ, [dat])
668 else:
669 dat2 = None
671 # '*' (untagged) responses?
673 if not self._match(Untagged_response, resp):
674 if self._match(Untagged_status, resp):
675 dat2 = self.mo.group('data2')
677 if self.mo is None:
678 # Only other possibility is '+' (continuation) rsponse...
680 if self._match(Continuation, resp):
681 self.continuation_response = self.mo.group('data')
682 return None # NB: indicates continuation
684 raise self.abort("unexpected response: '%s'" % resp)
686 typ = self.mo.group('type')
687 dat = self.mo.group('data')
688 if dat is None: dat = '' # Null untagged response
689 if dat2: dat = dat + ' ' + dat2
691 # Is there a literal to come?
693 while self._match(Literal, dat):
695 # Read literal direct from connection.
697 size = string.atoi(self.mo.group('size'))
698 if __debug__ and self.debug >= 4:
699 _mesg('read literal size %s' % size)
700 data = self.file.read(size)
702 # Store response with literal as tuple
704 self._append_untagged(typ, (dat, data))
706 # Read trailer - possibly containing another literal
708 dat = self._get_line()
710 self._append_untagged(typ, dat)
712 # Bracketed response information?
714 if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
715 self._append_untagged(self.mo.group('type'), self.mo.group('data'))
717 if __debug__ and self.debug >= 1 and typ in ('NO', 'BAD'):
718 _mesg('%s response: %s' % (typ, dat))
720 return resp
723 def _get_tagged_response(self, tag):
725 while 1:
726 result = self.tagged_commands[tag]
727 if result is not None:
728 del self.tagged_commands[tag]
729 return result
730 self._get_response()
733 def _get_line(self):
735 line = self.file.readline()
736 if not line:
737 raise self.abort('socket error: EOF')
739 # Protocol mandates all lines terminated by CRLF
741 line = line[:-2]
742 if __debug__ and self.debug >= 4:
743 _mesg('< %s' % line)
744 return line
747 def _match(self, cre, s):
749 # Run compiled regular expression match method on 's'.
750 # Save result, return success.
752 self.mo = cre.match(s)
753 if __debug__ and self.mo is not None and self.debug >= 5:
754 _mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`))
755 return self.mo is not None
758 def _new_tag(self):
760 tag = '%s%s' % (self.tagpre, self.tagnum)
761 self.tagnum = self.tagnum + 1
762 self.tagged_commands[tag] = None
763 return tag
766 def _simple_command(self, name, *args):
768 return self._command_complete(name, apply(self._command, (name,) + args))
771 def _untagged_response(self, typ, dat, name):
773 if typ == 'NO':
774 return typ, dat
775 if not self.untagged_responses.has_key(name):
776 return typ, [None]
777 data = self.untagged_responses[name]
778 if __debug__ and self.debug >= 5:
779 _mesg('untagged_responses[%s] => %s' % (name, data))
780 del self.untagged_responses[name]
781 return typ, data
785 class _Authenticator:
787 """Private class to provide en/decoding
788 for base64-based authentication conversation.
791 def __init__(self, mechinst):
792 self.mech = mechinst # Callable object to provide/process data
794 def process(self, data):
795 ret = self.mech(self.decode(data))
796 if ret is None:
797 return '*' # Abort conversation
798 return self.encode(ret)
800 def encode(self, inp):
802 # Invoke binascii.b2a_base64 iteratively with
803 # short even length buffers, strip the trailing
804 # line feed from the result and append. "Even"
805 # means a number that factors to both 6 and 8,
806 # so when it gets to the end of the 8-bit input
807 # there's no partial 6-bit output.
809 oup = ''
810 while inp:
811 if len(inp) > 48:
812 t = inp[:48]
813 inp = inp[48:]
814 else:
815 t = inp
816 inp = ''
817 e = binascii.b2a_base64(t)
818 if e:
819 oup = oup + e[:-1]
820 return oup
822 def decode(self, inp):
823 if not inp:
824 return ''
825 return binascii.a2b_base64(inp)
829 Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
830 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
832 def Internaldate2tuple(resp):
834 """Convert IMAP4 INTERNALDATE to UT.
836 Returns Python time module tuple.
839 mo = InternalDate.match(resp)
840 if not mo:
841 return None
843 mon = Mon2num[mo.group('mon')]
844 zonen = mo.group('zonen')
846 for name in ('day', 'year', 'hour', 'min', 'sec', 'zoneh', 'zonem'):
847 exec "%s = string.atoi(mo.group('%s'))" % (name, name)
849 # INTERNALDATE timezone must be subtracted to get UT
851 zone = (zoneh*60 + zonem)*60
852 if zonen == '-':
853 zone = -zone
855 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
857 utc = time.mktime(tt)
859 # Following is necessary because the time module has no 'mkgmtime'.
860 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
862 lt = time.localtime(utc)
863 if time.daylight and lt[-1]:
864 zone = zone + time.altzone
865 else:
866 zone = zone + time.timezone
868 return time.localtime(utc - zone)
872 def Int2AP(num):
874 """Convert integer to A-P string representation."""
876 val = ''; AP = 'ABCDEFGHIJKLMNOP'
877 num = int(abs(num))
878 while num:
879 num, mod = divmod(num, 16)
880 val = AP[mod] + val
881 return val
885 def ParseFlags(resp):
887 """Convert IMAP4 flags response to python tuple."""
889 mo = Flags.match(resp)
890 if not mo:
891 return ()
893 return tuple(string.split(mo.group('flags')))
896 def Time2Internaldate(date_time):
898 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
900 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
903 dttype = type(date_time)
904 if dttype is type(1):
905 tt = time.localtime(date_time)
906 elif dttype is type(()):
907 tt = date_time
908 elif dttype is type(""):
909 return date_time # Assume in correct format
910 else: raise ValueError
912 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
913 if dt[0] == '0':
914 dt = ' ' + dt[1:]
915 if time.daylight and tt[-1]:
916 zone = -time.altzone
917 else:
918 zone = -time.timezone
919 return '"' + dt + " %+02d%02d" % divmod(zone/60, 60) + '"'
923 if __debug__:
925 def _mesg(s):
926 # if len(s) > 70: s = '%.70s..' % s
927 sys.stderr.write('\t'+s+'\n')
928 sys.stderr.flush()
930 def _dump_ur(dict):
931 # Dump untagged responses (in `dict').
932 l = dict.items()
933 if not l: return
934 t = '\n\t\t'
935 j = string.join
936 l = map(lambda x,j=j:'%s: "%s"' % (x[0], x[1][0] and j(x[1], '" "') or ''), l)
937 _mesg('untagged responses dump:%s%s' % (t, j(l, t)))
941 if __debug__ and __name__ == '__main__':
943 import getpass, sys
945 host = ''
946 if sys.argv[1:]: host = sys.argv[1]
948 USER = getpass.getuser()
949 PASSWD = getpass.getpass("IMAP password for %s: " % (host or "localhost"))
951 test_seq1 = (
952 ('login', (USER, PASSWD)),
953 ('create', ('/tmp/xxx 1',)),
954 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
955 ('CREATE', ('/tmp/yyz 2',)),
956 ('append', ('/tmp/yyz 2', None, None, 'From: anon@x.y.z\n\ndata...')),
957 ('select', ('/tmp/yyz 2',)),
958 ('search', (None, '(TO zork)')),
959 ('partial', ('1', 'RFC822', 1, 1024)),
960 ('store', ('1', 'FLAGS', '(\Deleted)')),
961 ('expunge', ()),
962 ('recent', ()),
963 ('close', ()),
966 test_seq2 = (
967 ('select', ()),
968 ('response',('UIDVALIDITY',)),
969 ('uid', ('SEARCH', 'ALL')),
970 ('response', ('EXISTS',)),
971 ('recent', ()),
972 ('logout', ()),
975 def run(cmd, args):
976 typ, dat = apply(eval('M.%s' % cmd), args)
977 _mesg(' %s %s\n => %s %s' % (cmd, args, typ, dat))
978 return dat
980 Debug = 5
981 M = IMAP4(host)
982 _mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
984 for cmd,args in test_seq1:
985 run(cmd, args)
987 for ml in run('list', ('/tmp/', 'yy%')):
988 mo = re.match(r'.*"([^"]+)"$', ml)
989 if mo: path = mo.group(1)
990 else: path = string.split(ml)[-1]
991 run('delete', (path,))
993 for cmd,args in test_seq2:
994 dat = run(cmd, args)
996 if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
997 continue
999 uid = string.split(dat[-1])[-1]
1000 run('uid', ('FETCH', '%s' % uid,
1001 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))