From eda960a1dde94e0c3a172889d22588f839e4b2de Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Thu, 18 Jun 1998 14:24:28 +0000 Subject: Piers' latest version -- authentication added by Donn Cave. --- Lib/imaplib.py | 216 ++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 175 insertions(+), 41 deletions(-) diff --git a/Lib/imaplib.py b/Lib/imaplib.py index caea5bf..8bab8d8 100644 --- a/Lib/imaplib.py +++ b/Lib/imaplib.py @@ -4,6 +4,8 @@ Based on RFC 2060. Author: Piers Lauder December 1997. +Authentication code contributed by Donn Cave June 1998. + Public class: IMAP4 Public variable: Debug Public functions: Internaldate2tuple @@ -11,8 +13,12 @@ Public functions: Internaldate2tuple ParseFlags Time2Internaldate """ +# +# $Header$ +# +__version__ = "$Revision$" -import re, socket, string, time, random +import binascii, re, socket, string, time, random # Globals @@ -41,6 +47,7 @@ Commands = { 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'), 'LSUB': ('AUTH', 'SELECTED'), 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'), + 'PARTIAL': ('SELECTED',), 'RENAME': ('AUTH', 'SELECTED'), 'SEARCH': ('SELECTED',), 'SELECT': ('AUTH', 'SELECTED'), @@ -53,7 +60,7 @@ Commands = { # Patterns to match server responses -Continuation = re.compile(r'\+ (?P.*)') +Continuation = re.compile(r'\+( (?P.*))?') Flags = re.compile(r'.*FLAGS \((?P[^\)]*)\)') InternalDate = re.compile(r'.*INTERNALDATE "' r'(?P[ 123][0-9])-(?P[A-Z][a-z][a-z])-(?P[0-9][0-9][0-9][0-9])' @@ -62,7 +69,7 @@ InternalDate = re.compile(r'.*INTERNALDATE "' r'"') Literal = re.compile(r'(?P.*) {(?P\d+)}$') Response_code = re.compile(r'\[(?P[A-Z-]+)( (?P[^\]]*))?\]') -Untagged_response = re.compile(r'\* (?P[A-Z-]+) (?P.*)') +Untagged_response = re.compile(r'\* (?P[A-Z-]+)( (?P.*))?') Untagged_status = re.compile(r'\* (?P\d+) (?P[A-Z-]+)( (?P.*))?') @@ -81,8 +88,9 @@ class IMAP4: All arguments to commands are converted to strings, except for the last argument to APPEND which is passed as an IMAP4 - literal. If necessary (the string isn't enclosed with either - parentheses or double quotes) each converted string is quoted. + literal. If necessary (the string contains white-space and + isn't enclosed with either parentheses or double quotes) each + string is quoted. Each command returns a tuple: (type, [data, ...]) where 'type' is usually 'OK' or 'NO', and 'data' is either the text from the @@ -91,6 +99,11 @@ class IMAP4: Errors raise the exception class .error(""). IMAP4 server errors raise .abort(""), which is a sub-class of 'error'. + + Note: to use this module, you must read the RFCs pertaining + to the IMAP4 protocol, as the semantics of the arguments to + each IMAP4 command are left to the invoker, not to mention + the results. """ class error(Exception): pass # Logical errors - debug required @@ -110,9 +123,7 @@ class IMAP4: # Open socket to server. - self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.sock.connect(self.host, self.port) - self.file = self.sock.makefile('r') + self.open(host, port) # Create unique tag for this session, # and compile tagged response matcher. @@ -156,6 +167,13 @@ class IMAP4: raise self.error('server not IMAP4 compliant') + def open(self, host, port): + """Setup 'self.sock' and 'self.file'.""" + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.connect(self.host, self.port) + self.file = self.sock.makefile('r') + + def __getattr__(self, attr): """Allow UPPERCASE variants of all following IMAP4 commands.""" if Commands.has_key(attr): @@ -173,7 +191,8 @@ class IMAP4: """ name = 'APPEND' if flags: - flags = '(%s)' % flags + if (flags[0],flags[-1]) != ('(',')'): + flags = '(%s)' % flags else: flags = None if date_time: @@ -184,12 +203,32 @@ class IMAP4: return self._simple_command(name, mailbox, flags, date_time) - def authenticate(self, func): + def authenticate(self, mechanism, authobject): """Authenticate command - requires response processing. - UNIMPLEMENTED + 'mechanism' specifies which authentication mechanism is to + be used - it must appear in .capabilities in the + form AUTH=. + + 'authobject' must be a callable object: + + data = authobject(response) + + It will be called to process server continuation responses. + It should return data that will be encoded and sent to server. + It should return None if the client abort response '*' should + be sent instead. """ - raise self.error('UNIMPLEMENTED') + mech = string.upper(mechanism) + cap = 'AUTH=%s' % mech + if not cap in self.capabilities: + raise self.error("Server doesn't allow %s authentication." % mech) + self.literal = _Authenticator(authobject).process + typ, dat = self._simple_command('AUTHENTICATE', mech) + if typ != 'OK': + raise self.error(dat) + self.state = 'AUTH' + return typ, dat def check(self): @@ -324,18 +363,32 @@ class IMAP4: (typ, data) = .noop() """ + if __debug__ and self.debug >= 3: + print '\tuntagged responses: %s' % `self.untagged_responses` return self._simple_command('NOOP') + def partial(self, message_num, message_part, start, length): + """Fetch truncated part of a message. + + (typ, [data, ...]) = .partial(message_num, message_part, start, length) + + 'data' is tuple of message part envelope and data. + """ + name = 'PARTIAL' + typ, dat = self._simple_command(name, message_num, message_part, start, length) + return self._untagged_response(typ, 'FETCH') + + def recent(self): - """Return most recent 'RECENT' response if it exists, + """Return most recent 'RECENT' responses if any exist, else prompt server for an update using the 'NOOP' command, and flush all untagged responses. (typ, [data]) = .recent() 'data' is None if no new messages, - else value of RECENT response. + else list of RECENT responses, most recent last. """ name = 'RECENT' typ, dat = self._untagged_response('OK', name) @@ -361,7 +414,7 @@ class IMAP4: (code, [data]) = .response(code) """ - return self._untagged_response(code, code) + return self._untagged_response(code, string.upper(code)) def search(self, charset, criteria): @@ -403,6 +456,14 @@ class IMAP4: return typ, self.untagged_responses.get('EXISTS', [None]) + def socket(self): + """Return socket instance used to connect to IMAP4 server. + + socket = .socket() + """ + return self.sock + + def status(self, mailbox, names): """Request named status conditions for mailbox. @@ -440,8 +501,14 @@ class IMAP4: Returns response appropriate to 'command'. """ + command = string.upper(command) + if not Commands.has_key(command): + raise self.error("Unknown IMAP4 UID command: %s" % command) + if self.state not in Commands[command]: + raise self.error('command %s illegal in state %s' + % (command, self.state)) name = 'UID' - typ, dat = apply(self._simple_command, ('UID', command) + args) + typ, dat = apply(self._simple_command, (name, command) + args) if command == 'SEARCH': name = 'SEARCH' else: @@ -476,13 +543,13 @@ class IMAP4: def _append_untagged(self, typ, dat): - if self.untagged_responses.has_key(typ): - self.untagged_responses[typ].append(dat) + ur = self.untagged_responses + if ur.has_key(typ): + ur[typ].append(dat) else: - self.untagged_responses[typ] = [dat] - + ur[typ] = [dat] if __debug__ and self.debug >= 5: - print '\tuntagged_responses[%s] += %.20s..' % (typ, `dat`) + print '\tuntagged_responses[%s] %s += %s' % (typ, len(`ur[typ]`), _trunc(20, `dat`)) def _command(self, name, *args): @@ -492,6 +559,9 @@ class IMAP4: raise self.error( 'command %s illegal in state %s' % (name, self.state)) + if self.untagged_responses.has_key('OK'): + del self.untagged_responses['OK'] + tag = self._new_tag() data = '%s %s' % (tag, name) for d in args: @@ -508,7 +578,11 @@ class IMAP4: literal = self.literal if literal is not None: self.literal = None - data = '%s {%s}' % (data, len(literal)) + if type(literal) is type(self._command): + literator = literal + else: + literator = None + data = '%s {%s}' % (data, len(literal)) try: self.sock.send('%s%s' % (data, CRLF)) @@ -521,22 +595,29 @@ class IMAP4: if literal is None: return tag - # Wait for continuation response + while 1: + # Wait for continuation response - while self._get_response(): - if self.tagged_commands[tag]: # BAD/NO? - return tag + while self._get_response(): + if self.tagged_commands[tag]: # BAD/NO? + return tag - # Send literal + # Send literal - if __debug__ and self.debug >= 4: - print '\twrite literal size %s' % len(literal) + if literator: + literal = literator(self.continuation_response) - try: - self.sock.send(literal) - self.sock.send(CRLF) - except socket.error, val: - raise self.abort('socket error: %s' % val) + if __debug__ and self.debug >= 4: + print '\twrite literal size %s' % len(literal) + + try: + self.sock.send(literal) + self.sock.send(CRLF) + except socket.error, val: + raise self.abort('socket error: %s' % val) + + if not literator: + break return tag @@ -590,10 +671,11 @@ class IMAP4: self.continuation_response = self.mo.group('data') return None # NB: indicates continuation - raise self.abort('unexpected response: %s' % resp) + raise self.abort("unexpected response: '%s'" % resp) typ = self.mo.group('type') dat = self.mo.group('data') + if dat is None: dat = '' # Null untagged response if dat2: dat = dat + ' ' + dat2 # Is there a literal to come? @@ -679,12 +761,56 @@ class IMAP4: return typ, [None] data = self.untagged_responses[name] if __debug__ and self.debug >= 5: - print '\tuntagged_responses[%s] => %.20s..' % (name, `data`) + print '\tuntagged_responses[%s] => %s' % (name, _trunc(20, `data`)) del self.untagged_responses[name] return typ, data +class _Authenticator: + + """Private class to provide en/decoding + for base64-based authentication conversation. + """ + + def __init__(self, mechinst): + self.mech = mechinst # Callable object to provide/process data + + def process(self, data): + ret = self.mech(self.decode(data)) + if ret is None: + return '*' # Abort conversation + return self.encode(ret) + + def encode(self, inp): + # + # Invoke binascii.b2a_base64 iteratively with + # short even length buffers, strip the trailing + # line feed from the result and append. "Even" + # means a number that factors to both 6 and 8, + # so when it gets to the end of the 8-bit input + # there's no partial 6-bit output. + # + oup = '' + while inp: + if len(inp) > 48: + t = inp[:48] + inp = inp[48:] + else: + t = inp + inp = '' + e = binascii.b2a_base64(t) + if e: + oup = oup + e[:-1] + return oup + + def decode(self, inp): + if not inp: + return '' + return binascii.a2b_base64(inp) + + + Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6, 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12} @@ -779,6 +905,14 @@ def Time2Internaldate(date_time): +if __debug__: + + def _trunc(m, s): + if len(s) <= m: return s + return '%.*s..' % (m, s) + + + if __debug__ and __name__ == '__main__': host = '' @@ -798,8 +932,8 @@ if __debug__ and __name__ == '__main__': ('CREATE', ('/tmp/yyz 2',)), ('append', ('/tmp/yyz 2', None, None, 'From: anon@x.y.z\n\ndata...')), ('select', ('/tmp/yyz 2',)), - ('uid', ('SEARCH', 'ALL')), - ('fetch', ('1', '(INTERNALDATE RFC822)')), + ('search', (None, '(TO zork)')), + ('partial', ('1', 'RFC822', 1, 1024)), ('store', ('1', 'FLAGS', '(\Deleted)')), ('expunge', ()), ('recent', ()), @@ -820,7 +954,7 @@ if __debug__ and __name__ == '__main__': print ' %s %s\n => %s %s' % (cmd, args, typ, dat) return dat - Debug = 4 + Debug = 5 M = IMAP4(host) print 'PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION @@ -839,6 +973,6 @@ if __debug__ and __name__ == '__main__': if (cmd,args) != ('uid', ('SEARCH', 'ALL')): continue - uid = string.split(dat[0])[-1] + uid = string.split(dat[-1])[-1] run('uid', ('FETCH', '%s' % uid, - '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822)')) + '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)')) -- cgit v0.12