From 8c0622114bc95f9f5b85a8a7e4ed8619bf3ff024 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Mon, 13 Dec 1999 23:27:45 +0000 Subject: [PATCH] V 2.16 from Piers: I've changed the login command to force proper quoting of the password argument. I've also added some extra debugging code, which is removed when __debug__ is false. --- Lib/imaplib.py | 173 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 123 insertions(+), 50 deletions(-) diff --git a/Lib/imaplib.py b/Lib/imaplib.py index cf79449c020..02755713233 100644 --- a/Lib/imaplib.py +++ b/Lib/imaplib.py @@ -15,7 +15,7 @@ Time2Internaldate """ -__version__ = "2.15" +__version__ = "2.16" import binascii, re, socket, string, time, random, sys @@ -89,7 +89,8 @@ class IMAP4: AUTHENTICATE, and the last argument to APPEND which is passed as an IMAP4 literal. If necessary (the string contains white-space and isn't enclosed with either parentheses or - double quotes) each string is quoted. + double quotes) each string is quoted. However, the 'password' + argument to the LOGIN command is always quoted. Each command returns a tuple: (type, [data, ...]) where 'type' is usually 'OK' or 'NO', and 'data' is either the text from the @@ -101,6 +102,11 @@ class IMAP4: from READ-WRITE to READ-ONLY raise the exception class .readonly(""), which is a sub-class of 'abort'. + "error" exceptions imply a program error. + "abort" exceptions imply the connection should be reset, and + the command re-tried. + "readonly" exceptions imply the command should be re-tried. + 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 @@ -111,6 +117,7 @@ class error(Exception): pass # Logical errors - debug required class abort(error): pass # Service errors - close and retry class readonly(abort): pass # Mailbox status changed to READ-ONLY + mustquote = re.compile(r'\W') # Match any non-alphanumeric character def __init__(self, host = '', port = IMAP4_PORT): self.host = host @@ -138,15 +145,15 @@ def __init__(self, host = '', port = IMAP4_PORT): # Get server welcome message, # request and store CAPABILITY response. - if __debug__ and self.debug >= 1: - _mesg('new IMAP4 connection, tag=%s' % self.tagpre) + if __debug__: + if self.debug >= 1: + _mesg('new IMAP4 connection, tag=%s' % self.tagpre) self.welcome = self._get_response() if self.untagged_responses.has_key('PREAUTH'): self.state = 'AUTH' elif self.untagged_responses.has_key('OK'): self.state = 'NONAUTH' -# elif self.untagged_responses.has_key('BYE'): else: raise self.error(self.welcome) @@ -156,8 +163,9 @@ def __init__(self, host = '', port = IMAP4_PORT): raise self.error('no CAPABILITY response from server') self.capabilities = tuple(string.split(string.upper(self.untagged_responses[cap][-1]))) - if __debug__ and self.debug >= 3: - _mesg('CAPABILITIES: %s' % `self.capabilities`) + if __debug__: + if self.debug >= 3: + _mesg('CAPABILITIES: %s' % `self.capabilities`) for version in AllowedVersions: if not version in self.capabilities: @@ -229,8 +237,12 @@ def append(self, mailbox, flags, date_time, message): """Append message to named mailbox. (typ, [data]) = .append(mailbox, flags, date_time, message) + + All args except `message' can be None. """ name = 'APPEND' + if not mailbox: + mailbox = 'INBOX' if flags: if (flags[0],flags[-1]) != ('(',')'): flags = '(%s)' % flags @@ -360,9 +372,13 @@ def list(self, directory='""', pattern='*'): def login(self, user, password): """Identify client using plaintext password. - (typ, [data]) = .list(user, password) + (typ, [data]) = .login(user, password) + + NB: 'password' will be quoted. """ - typ, dat = self._simple_command('LOGIN', user, password) + #if not 'AUTH=LOGIN' in self.capabilities: + # raise self.error("Server doesn't allow LOGIN authentication." % mech) + typ, dat = self._simple_command('LOGIN', user, self._quote(password)) if typ != 'OK': raise self.error(dat[-1]) self.state = 'AUTH' @@ -403,8 +419,9 @@ def noop(self): (typ, data) = .noop() """ - if __debug__ and self.debug >= 3: - _dump_ur(self.untagged_responses) + if __debug__: + if self.debug >= 3: + _dump_ur(self.untagged_responses) return self._simple_command('NOOP') @@ -464,7 +481,9 @@ def select(self, mailbox='INBOX', readonly=None): self.state = 'SELECTED' if not self.untagged_responses.has_key('READ-WRITE') \ and not readonly: - if __debug__ and self.debug >= 1: _dump_ur(self.untagged_responses) + if __debug__: + if self.debug >= 1: + _dump_ur(self.untagged_responses) raise self.readonly('%s is not writable' % mailbox) return typ, self.untagged_responses.get('EXISTS', [None]) @@ -546,16 +565,24 @@ def xatom(self, name, *args): def _append_untagged(self, typ, dat): + if dat is None: dat = '' ur = self.untagged_responses - if __debug__ and self.debug >= 5: - _mesg('untagged_responses[%s] %s += %s' % - (typ, len(ur.get(typ,'')), dat)) + if __debug__: + if self.debug >= 5: + _mesg('untagged_responses[%s] %s += ["%s"]' % + (typ, len(ur.get(typ,'')), dat)) if ur.has_key(typ): ur[typ].append(dat) else: ur[typ] = [dat] + def _check_bye(self): + bye = self.untagged_responses.get('BYE') + if bye: + raise self.abort(bye[-1]) + + def _command(self, name, *args): if self.state not in Commands[name]: @@ -574,16 +601,9 @@ def _command(self, name, *args): tag = self._new_tag() data = '%s %s' % (tag, name) - for d in args: - if d is None: continue - if type(d) is type(''): - l = len(string.split(d)) - else: - l = 1 - if l == 0 or l > 1 and (d[0],d[-1]) not in (('(',')'),('"','"')): - data = '%s "%s"' % (data, d) - else: - data = '%s %s' % (data, d) + for arg in args: + if arg is None: continue + data = '%s %s' % (data, self._checkquote(arg)) literal = self.literal if literal is not None: @@ -594,14 +614,17 @@ def _command(self, name, *args): literator = None data = '%s {%s}' % (data, len(literal)) + if __debug__: + if self.debug >= 4: + _mesg('> %s' % data) + else: + _log('> %s' % data) + try: self.sock.send('%s%s' % (data, CRLF)) except socket.error, val: raise self.abort('socket error: %s' % val) - if __debug__ and self.debug >= 4: - _mesg('> %s' % data) - if literal is None: return tag @@ -617,8 +640,9 @@ def _command(self, name, *args): if literator: literal = literator(self.continuation_response) - if __debug__ and self.debug >= 4: - _mesg('write literal size %s' % len(literal)) + if __debug__: + if self.debug >= 4: + _mesg('write literal size %s' % len(literal)) try: self.sock.send(literal) @@ -633,14 +657,14 @@ def _command(self, name, *args): def _command_complete(self, name, tag): + self._check_bye() try: typ, data = self._get_tagged_response(tag) except self.abort, val: raise self.abort('command: %s => %s' % (name, val)) except self.error, val: raise self.error('command: %s => %s' % (name, val)) - if self.untagged_responses.has_key('BYE') and name != 'LOGOUT': - raise self.abort(self.untagged_responses['BYE'][-1]) + self._check_bye() if typ == 'BAD': raise self.error('%s command error: %s %s' % (name, typ, data)) return typ, data @@ -695,8 +719,9 @@ def _get_response(self): # Read literal direct from connection. size = string.atoi(self.mo.group('size')) - if __debug__ and self.debug >= 4: - _mesg('read literal size %s' % size) + if __debug__: + if self.debug >= 4: + _mesg('read literal size %s' % size) data = self.file.read(size) # Store response with literal as tuple @@ -714,8 +739,9 @@ def _get_response(self): if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat): self._append_untagged(self.mo.group('type'), self.mo.group('data')) - if __debug__ and self.debug >= 1 and typ in ('NO', 'BAD'): - _mesg('%s response: %s' % (typ, dat)) + if __debug__: + if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'): + _mesg('%s response: %s' % (typ, dat)) return resp @@ -739,8 +765,11 @@ def _get_line(self): # Protocol mandates all lines terminated by CRLF line = line[:-2] - if __debug__ and self.debug >= 4: - _mesg('< %s' % line) + if __debug__: + if self.debug >= 4: + _mesg('< %s' % line) + else: + _log('< %s' % line) return line @@ -750,8 +779,9 @@ def _match(self, cre, s): # Save result, return success. self.mo = cre.match(s) - if __debug__ and self.mo is not None and self.debug >= 5: - _mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`)) + if __debug__: + if self.mo is not None and self.debug >= 5: + _mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`)) return self.mo is not None @@ -763,6 +793,28 @@ def _new_tag(self): return tag + def _checkquote(self, arg): + + # Must quote command args if non-alphanumeric chars present, + # and not already quoted. + + if type(arg) is not type(''): + return arg + if (arg[0],arg[-1]) in (('(',')'),('"','"')): + return arg + if self.mustquote.search(arg) is None: + return arg + return self._quote(arg) + + + def _quote(self, arg): + + arg = string.replace(arg, '\\', '\\\\') + arg = string.replace(arg, '"', '\\"') + + return '"%s"' % arg + + def _simple_command(self, name, *args): return self._command_complete(name, apply(self._command, (name,) + args)) @@ -775,8 +827,9 @@ def _untagged_response(self, typ, dat, name): if not self.untagged_responses.has_key(name): return typ, [None] data = self.untagged_responses[name] - if __debug__ and self.debug >= 5: - _mesg('untagged_responses[%s] => %s' % (name, data)) + if __debug__: + if self.debug >= 5: + _mesg('untagged_responses[%s] => %s' % (name, data)) del self.untagged_responses[name] return typ, data @@ -901,7 +954,7 @@ def Time2Internaldate(date_time): """ dttype = type(date_time) - if dttype is type(1): + if dttype is type(1) or dttype is type(1.1): tt = time.localtime(date_time) elif dttype is type(()): tt = date_time @@ -922,9 +975,11 @@ def Time2Internaldate(date_time): if __debug__: - def _mesg(s): -# if len(s) > 70: s = '%.70s..' % s - sys.stderr.write('\t'+s+'\n') + def _mesg(s, secs=None): + if secs is None: + secs = time.time() + tm = time.strftime('%M:%S', time.localtime(secs)) + sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s)) sys.stderr.flush() def _dump_ur(dict): @@ -936,9 +991,23 @@ def _dump_ur(dict): l = map(lambda x,j=j:'%s: "%s"' % (x[0], x[1][0] and j(x[1], '" "') or ''), l) _mesg('untagged responses dump:%s%s' % (t, j(l, t))) + _cmd_log = [] # Last `_cmd_log_len' interactions + _cmd_log_len = 10 + + def _log(line): + # Keep log of last `_cmd_log_len' interactions for debugging. + if len(_cmd_log) == _cmd_log_len: + del _cmd_log[0] + _cmd_log.append((time.time(), line)) + + def print_log(): + _mesg('last %d IMAP4 interactions:' % len(_cmd_log)) + for secs,line in _cmd_log: + _mesg(line, secs) -if __debug__ and __name__ == '__main__': + +if __name__ == '__main__': import getpass, sys @@ -954,6 +1023,7 @@ def _dump_ur(dict): ('rename', ('/tmp/xxx 1', '/tmp/yyy')), ('CREATE', ('/tmp/yyz 2',)), ('append', ('/tmp/yyz 2', None, None, 'From: anon@x.y.z\n\ndata...')), + ('list', ('/tmp', 'yy*')), ('select', ('/tmp/yyz 2',)), ('search', (None, '(TO zork)')), ('partial', ('1', 'RFC822', 1, 1024)), @@ -968,13 +1038,15 @@ def _dump_ur(dict): ('response',('UIDVALIDITY',)), ('uid', ('SEARCH', 'ALL')), ('response', ('EXISTS',)), + ('append', (None, None, None, 'From: anon@x.y.z\n\ndata...')), ('recent', ()), ('logout', ()), ) def run(cmd, args): + _mesg('%s %s' % (cmd, args)) typ, dat = apply(eval('M.%s' % cmd), args) - _mesg(' %s %s\n => %s %s' % (cmd, args, typ, dat)) + _mesg('%s => %s %s' % (cmd, typ, dat)) return dat Debug = 5 @@ -996,6 +1068,7 @@ def run(cmd, args): if (cmd,args) != ('uid', ('SEARCH', 'ALL')): continue - uid = string.split(dat[-1])[-1] - run('uid', ('FETCH', '%s' % uid, + uid = string.split(dat[-1]) + if not uid: continue + run('uid', ('FETCH', '%s' % uid[-1], '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))