Kaydet (Commit) 26367a00 authored tarafından Guido van Rossum's avatar Guido van Rossum

New version from Piers Lauder, who writes:

Added a debug function to replace 'print' statements.
Ensured that response attached to 'NO' replies is passed back.
added readonly exception.
Rearranged method order into types.
Ensure select returns a meaningful error on 'NO'.
'NO' returns from authenticate and login raise error with last message,
not list.
üst 75bb54c3
...@@ -15,9 +15,9 @@ Public functions: Internaldate2tuple ...@@ -15,9 +15,9 @@ Public functions: Internaldate2tuple
Time2Internaldate Time2Internaldate
""" """
__version__ = "2.11" __version__ = "2.15"
import binascii, re, socket, string, time, random import binascii, re, socket, string, time, random, sys
# Globals # Globals
...@@ -97,7 +97,9 @@ class IMAP4: ...@@ -97,7 +97,9 @@ class IMAP4:
Errors raise the exception class <instance>.error("<reason>"). Errors raise the exception class <instance>.error("<reason>").
IMAP4 server errors raise <instance>.abort("<reason>"), IMAP4 server errors raise <instance>.abort("<reason>"),
which is a sub-class of 'error'. which is a sub-class of 'error'. Mailbox status changes
from READ-WRITE to READ-ONLY raise the exception class
<instance>.readonly("<reason>"), which is a sub-class of 'abort'.
Note: to use this module, you must read the RFCs pertaining Note: to use this module, you must read the RFCs pertaining
to the IMAP4 protocol, as the semantics of the arguments to to the IMAP4 protocol, as the semantics of the arguments to
...@@ -107,6 +109,7 @@ class IMAP4: ...@@ -107,6 +109,7 @@ class IMAP4:
class error(Exception): pass # Logical errors - debug required class error(Exception): pass # Logical errors - debug required
class abort(error): pass # Service errors - close and retry class abort(error): pass # Service errors - close and retry
class readonly(abort): pass # Mailbox status changed to READ-ONLY
def __init__(self, host = '', port = IMAP4_PORT): def __init__(self, host = '', port = IMAP4_PORT):
...@@ -136,7 +139,7 @@ class IMAP4: ...@@ -136,7 +139,7 @@ class IMAP4:
# request and store CAPABILITY response. # request and store CAPABILITY response.
if __debug__ and self.debug >= 1: if __debug__ and self.debug >= 1:
print '\tnew IMAP4 connection, tag=%s' % self.tagpre _mesg('new IMAP4 connection, tag=%s' % self.tagpre)
self.welcome = self._get_response() self.welcome = self._get_response()
if self.untagged_responses.has_key('PREAUTH'): if self.untagged_responses.has_key('PREAUTH'):
...@@ -154,7 +157,7 @@ class IMAP4: ...@@ -154,7 +157,7 @@ class IMAP4:
self.capabilities = tuple(string.split(self.untagged_responses[cap][-1])) self.capabilities = tuple(string.split(self.untagged_responses[cap][-1]))
if __debug__ and self.debug >= 3: if __debug__ and self.debug >= 3:
print '\tCAPABILITIES: %s' % `self.capabilities` _mesg('CAPABILITIES: %s' % `self.capabilities`)
for version in AllowedVersions: for version in AllowedVersions:
if not version in self.capabilities: if not version in self.capabilities:
...@@ -165,6 +168,17 @@ class IMAP4: ...@@ -165,6 +168,17 @@ class IMAP4:
raise self.error('server not IMAP4 compliant') raise self.error('server not IMAP4 compliant')
def __getattr__(self, attr):
# Allow UPPERCASE variants of IMAP4 command methods.
if Commands.has_key(attr):
return eval("self.%s" % string.lower(attr))
raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
# Public methods
def open(self, host, port): def open(self, host, port):
"""Setup 'self.sock' and 'self.file'.""" """Setup 'self.sock' and 'self.file'."""
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
...@@ -172,14 +186,43 @@ class IMAP4: ...@@ -172,14 +186,43 @@ class IMAP4:
self.file = self.sock.makefile('r') self.file = self.sock.makefile('r')
def __getattr__(self, attr): def recent(self):
"""Allow UPPERCASE variants of all following IMAP4 commands.""" """Return most recent 'RECENT' responses if any exist,
if Commands.has_key(attr): else prompt server for an update using the 'NOOP' command.
return eval("self.%s" % string.lower(attr))
raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
(typ, [data]) = <instance>.recent()
'data' is None if no new messages,
else list of RECENT responses, most recent last.
"""
name = 'RECENT'
typ, dat = self._untagged_response('OK', [None], name)
if dat[-1]:
return typ, dat
typ, dat = self.noop() # Prod server for response
return self._untagged_response(typ, dat, name)
def response(self, code):
"""Return data for response 'code' if received, or None.
Old value for response 'code' is cleared.
(code, [data]) = <instance>.response(code)
"""
return self._untagged_response(code, [None], string.upper(code))
def socket(self):
"""Return socket instance used to connect to IMAP4 server.
socket = <instance>.socket()
"""
return self.sock
# Public methods
# IMAP4 commands
def append(self, mailbox, flags, date_time, message): def append(self, mailbox, flags, date_time, message):
...@@ -224,7 +267,7 @@ class IMAP4: ...@@ -224,7 +267,7 @@ class IMAP4:
self.literal = _Authenticator(authobject).process self.literal = _Authenticator(authobject).process
typ, dat = self._simple_command('AUTHENTICATE', mech) typ, dat = self._simple_command('AUTHENTICATE', mech)
if typ != 'OK': if typ != 'OK':
raise self.error(dat) raise self.error(dat[-1])
self.state = 'AUTH' self.state = 'AUTH'
return typ, dat return typ, dat
...@@ -246,8 +289,7 @@ class IMAP4: ...@@ -246,8 +289,7 @@ class IMAP4:
(typ, [data]) = <instance>.close() (typ, [data]) = <instance>.close()
""" """
try: try:
try: typ, dat = self._simple_command('CLOSE') typ, dat = self._simple_command('CLOSE')
except EOFError: typ, dat = None, [None]
finally: finally:
self.state = 'AUTH' self.state = 'AUTH'
return typ, dat return typ, dat
...@@ -288,7 +330,7 @@ class IMAP4: ...@@ -288,7 +330,7 @@ class IMAP4:
""" """
name = 'EXPUNGE' name = 'EXPUNGE'
typ, dat = self._simple_command(name) typ, dat = self._simple_command(name)
return self._untagged_response(typ, name) return self._untagged_response(typ, dat, name)
def fetch(self, message_set, message_parts): def fetch(self, message_set, message_parts):
...@@ -300,7 +342,7 @@ class IMAP4: ...@@ -300,7 +342,7 @@ class IMAP4:
""" """
name = 'FETCH' name = 'FETCH'
typ, dat = self._simple_command(name, message_set, message_parts) typ, dat = self._simple_command(name, message_set, message_parts)
return self._untagged_response(typ, name) return self._untagged_response(typ, dat, name)
def list(self, directory='""', pattern='*'): def list(self, directory='""', pattern='*'):
...@@ -312,7 +354,7 @@ class IMAP4: ...@@ -312,7 +354,7 @@ class IMAP4:
""" """
name = 'LIST' name = 'LIST'
typ, dat = self._simple_command(name, directory, pattern) typ, dat = self._simple_command(name, directory, pattern)
return self._untagged_response(typ, name) return self._untagged_response(typ, dat, name)
def login(self, user, password): def login(self, user, password):
...@@ -322,7 +364,7 @@ class IMAP4: ...@@ -322,7 +364,7 @@ class IMAP4:
""" """
typ, dat = self._simple_command('LOGIN', user, password) typ, dat = self._simple_command('LOGIN', user, password)
if typ != 'OK': if typ != 'OK':
raise self.error(dat) raise self.error(dat[-1])
self.state = 'AUTH' self.state = 'AUTH'
return typ, dat return typ, dat
...@@ -336,7 +378,7 @@ class IMAP4: ...@@ -336,7 +378,7 @@ class IMAP4:
""" """
self.state = 'LOGOUT' self.state = 'LOGOUT'
try: typ, dat = self._simple_command('LOGOUT') try: typ, dat = self._simple_command('LOGOUT')
except EOFError: typ, dat = None, [None] except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
self.file.close() self.file.close()
self.sock.close() self.sock.close()
if self.untagged_responses.has_key('BYE'): if self.untagged_responses.has_key('BYE'):
...@@ -353,7 +395,7 @@ class IMAP4: ...@@ -353,7 +395,7 @@ class IMAP4:
""" """
name = 'LSUB' name = 'LSUB'
typ, dat = self._simple_command(name, directory, pattern) typ, dat = self._simple_command(name, directory, pattern)
return self._untagged_response(typ, name) return self._untagged_response(typ, dat, name)
def noop(self): def noop(self):
...@@ -362,7 +404,7 @@ class IMAP4: ...@@ -362,7 +404,7 @@ class IMAP4:
(typ, data) = <instance>.noop() (typ, data) = <instance>.noop()
""" """
if __debug__ and self.debug >= 3: if __debug__ and self.debug >= 3:
print '\tuntagged responses: %s' % `self.untagged_responses` _dump_ur(self.untagged_responses)
return self._simple_command('NOOP') return self._simple_command('NOOP')
...@@ -375,26 +417,7 @@ class IMAP4: ...@@ -375,26 +417,7 @@ class IMAP4:
""" """
name = 'PARTIAL' name = 'PARTIAL'
typ, dat = self._simple_command(name, message_num, message_part, start, length) typ, dat = self._simple_command(name, message_num, message_part, start, length)
return self._untagged_response(typ, 'FETCH') return self._untagged_response(typ, dat, 'FETCH')
def recent(self):
"""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]) = <instance>.recent()
'data' is None if no new messages,
else list of RECENT responses, most recent last.
"""
name = 'RECENT'
typ, dat = self._untagged_response('OK', name)
if dat[-1]:
return typ, dat
self.untagged_responses = {}
typ, dat = self._simple_command('NOOP')
return self._untagged_response(typ, name)
def rename(self, oldmailbox, newmailbox): def rename(self, oldmailbox, newmailbox):
...@@ -405,16 +428,6 @@ class IMAP4: ...@@ -405,16 +428,6 @@ class IMAP4:
return self._simple_command('RENAME', oldmailbox, newmailbox) return self._simple_command('RENAME', oldmailbox, newmailbox)
def response(self, code):
"""Return data for response 'code' if received, or None.
Old value for response 'code' is cleared.
(code, [data]) = <instance>.response(code)
"""
return self._untagged_response(code, string.upper(code))
def search(self, charset, criteria): def search(self, charset, criteria):
"""Search mailbox for matching messages. """Search mailbox for matching messages.
...@@ -426,7 +439,7 @@ class IMAP4: ...@@ -426,7 +439,7 @@ class IMAP4:
if charset: if charset:
charset = 'CHARSET ' + charset charset = 'CHARSET ' + charset
typ, dat = self._simple_command(name, charset, criteria) typ, dat = self._simple_command(name, charset, criteria)
return self._untagged_response(typ, name) return self._untagged_response(typ, dat, name)
def select(self, mailbox='INBOX', readonly=None): def select(self, mailbox='INBOX', readonly=None):
...@@ -445,23 +458,17 @@ class IMAP4: ...@@ -445,23 +458,17 @@ class IMAP4:
else: else:
name = 'SELECT' name = 'SELECT'
typ, dat = self._simple_command(name, mailbox) typ, dat = self._simple_command(name, mailbox)
if typ == 'OK': if typ != 'OK':
self.state = 'SELECTED' self.state = 'AUTH' # Might have been 'SELECTED'
elif typ == 'NO': return typ, dat
self.state = 'AUTH' self.state = 'SELECTED'
if not readonly and not self.untagged_responses.has_key('READ-WRITE'): if not self.untagged_responses.has_key('READ-WRITE') \
raise self.error('%s is not writable' % mailbox) and not readonly:
if __debug__ and self.debug >= 1: _dump_ur(self.untagged_responses)
raise self.readonly('%s is not writable' % mailbox)
return typ, self.untagged_responses.get('EXISTS', [None]) return typ, self.untagged_responses.get('EXISTS', [None])
def socket(self):
"""Return socket instance used to connect to IMAP4 server.
socket = <instance>.socket()
"""
return self.sock
def status(self, mailbox, names): def status(self, mailbox, names):
"""Request named status conditions for mailbox. """Request named status conditions for mailbox.
...@@ -471,7 +478,7 @@ class IMAP4: ...@@ -471,7 +478,7 @@ class IMAP4:
if self.PROTOCOL_VERSION == 'IMAP4': if self.PROTOCOL_VERSION == 'IMAP4':
raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name) raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
typ, dat = self._simple_command(name, mailbox, names) typ, dat = self._simple_command(name, mailbox, names)
return self._untagged_response(typ, name) return self._untagged_response(typ, dat, name)
def store(self, message_set, command, flag_list): def store(self, message_set, command, flag_list):
...@@ -480,7 +487,7 @@ class IMAP4: ...@@ -480,7 +487,7 @@ class IMAP4:
(typ, [data]) = <instance>.store(message_set, command, flag_list) (typ, [data]) = <instance>.store(message_set, command, flag_list)
""" """
typ, dat = self._simple_command('STORE', message_set, command, flag_list) typ, dat = self._simple_command('STORE', message_set, command, flag_list)
return self._untagged_response(typ, 'FETCH') return self._untagged_response(typ, dat, 'FETCH')
def subscribe(self, mailbox): def subscribe(self, mailbox):
...@@ -511,9 +518,7 @@ class IMAP4: ...@@ -511,9 +518,7 @@ class IMAP4:
name = 'SEARCH' name = 'SEARCH'
else: else:
name = 'FETCH' name = 'FETCH'
typ, dat2 = self._untagged_response(typ, name) return self._untagged_response(typ, dat, name)
if dat2[-1]: dat = dat2
return typ, dat
def unsubscribe(self, mailbox): def unsubscribe(self, mailbox):
...@@ -542,12 +547,13 @@ class IMAP4: ...@@ -542,12 +547,13 @@ class IMAP4:
def _append_untagged(self, typ, dat): def _append_untagged(self, typ, dat):
ur = self.untagged_responses ur = self.untagged_responses
if __debug__ and self.debug >= 5:
_mesg('untagged_responses[%s] %s += %s' %
(typ, len(ur.get(typ,'')), dat))
if ur.has_key(typ): if ur.has_key(typ):
ur[typ].append(dat) ur[typ].append(dat)
else: else:
ur[typ] = [dat] ur[typ] = [dat]
if __debug__ and self.debug >= 5:
print '\tuntagged_responses[%s] %s += %s' % (typ, len(`ur[typ]`), _trunc(20, `dat`))
def _command(self, name, *args): def _command(self, name, *args):
...@@ -557,8 +563,14 @@ class IMAP4: ...@@ -557,8 +563,14 @@ class IMAP4:
raise self.error( raise self.error(
'command %s illegal in state %s' % (name, self.state)) 'command %s illegal in state %s' % (name, self.state))
if self.untagged_responses.has_key('OK'): for typ in ('OK', 'NO', 'BAD'):
del self.untagged_responses['OK'] if self.untagged_responses.has_key(typ):
del self.untagged_responses[typ]
if self.untagged_responses.has_key('READ-WRITE') \
and self.untagged_responses.has_key('READ-ONLY'):
del self.untagged_responses['READ-WRITE']
raise self.readonly('mailbox status changed to READ-ONLY')
tag = self._new_tag() tag = self._new_tag()
data = '%s %s' % (tag, name) data = '%s %s' % (tag, name)
...@@ -588,7 +600,7 @@ class IMAP4: ...@@ -588,7 +600,7 @@ class IMAP4:
raise self.abort('socket error: %s' % val) raise self.abort('socket error: %s' % val)
if __debug__ and self.debug >= 4: if __debug__ and self.debug >= 4:
print '\t> %s' % data _mesg('> %s' % data)
if literal is None: if literal is None:
return tag return tag
...@@ -606,7 +618,7 @@ class IMAP4: ...@@ -606,7 +618,7 @@ class IMAP4:
literal = literator(self.continuation_response) literal = literator(self.continuation_response)
if __debug__ and self.debug >= 4: if __debug__ and self.debug >= 4:
print '\twrite literal size %s' % len(literal) _mesg('write literal size %s' % len(literal))
try: try:
self.sock.send(literal) self.sock.send(literal)
...@@ -684,7 +696,7 @@ class IMAP4: ...@@ -684,7 +696,7 @@ class IMAP4:
size = string.atoi(self.mo.group('size')) size = string.atoi(self.mo.group('size'))
if __debug__ and self.debug >= 4: if __debug__ and self.debug >= 4:
print '\tread literal size %s' % size _mesg('read literal size %s' % size)
data = self.file.read(size) data = self.file.read(size)
# Store response with literal as tuple # Store response with literal as tuple
...@@ -702,6 +714,9 @@ class IMAP4: ...@@ -702,6 +714,9 @@ class IMAP4:
if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat): if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
self._append_untagged(self.mo.group('type'), self.mo.group('data')) 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))
return resp return resp
...@@ -719,13 +734,13 @@ class IMAP4: ...@@ -719,13 +734,13 @@ class IMAP4:
line = self.file.readline() line = self.file.readline()
if not line: if not line:
raise EOFError raise self.abort('socket error: EOF')
# Protocol mandates all lines terminated by CRLF # Protocol mandates all lines terminated by CRLF
line = line[:-2] line = line[:-2]
if __debug__ and self.debug >= 4: if __debug__ and self.debug >= 4:
print '\t< %s' % line _mesg('< %s' % line)
return line return line
...@@ -736,7 +751,7 @@ class IMAP4: ...@@ -736,7 +751,7 @@ class IMAP4:
self.mo = cre.match(s) self.mo = cre.match(s)
if __debug__ and self.mo is not None and self.debug >= 5: if __debug__ and self.mo is not None and self.debug >= 5:
print "\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`) _mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`))
return self.mo is not None return self.mo is not None
...@@ -753,13 +768,15 @@ class IMAP4: ...@@ -753,13 +768,15 @@ class IMAP4:
return self._command_complete(name, apply(self._command, (name,) + args)) return self._command_complete(name, apply(self._command, (name,) + args))
def _untagged_response(self, typ, name): def _untagged_response(self, typ, dat, name):
if typ == 'NO':
return typ, dat
if not self.untagged_responses.has_key(name): if not self.untagged_responses.has_key(name):
return typ, [None] return typ, [None]
data = self.untagged_responses[name] data = self.untagged_responses[name]
if __debug__ and self.debug >= 5: if __debug__ and self.debug >= 5:
print '\tuntagged_responses[%s] => %s' % (name, _trunc(20, `data`)) _mesg('untagged_responses[%s] => %s' % (name, data))
del self.untagged_responses[name] del self.untagged_responses[name]
return typ, data return typ, data
...@@ -905,9 +922,19 @@ def Time2Internaldate(date_time): ...@@ -905,9 +922,19 @@ def Time2Internaldate(date_time):
if __debug__: if __debug__:
def _trunc(m, s): def _mesg(s):
if len(s) <= m: return s # if len(s) > 70: s = '%.70s..' % s
return '%.*s..' % (m, s) sys.stderr.write('\t'+s+'\n')
sys.stderr.flush()
def _dump_ur(dict):
# Dump untagged responses (in `dict').
l = dict.items()
if not l: return
t = '\n\t\t'
j = string.join
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)))
...@@ -947,12 +974,12 @@ if __debug__ and __name__ == '__main__': ...@@ -947,12 +974,12 @@ if __debug__ and __name__ == '__main__':
def run(cmd, args): def run(cmd, args):
typ, dat = apply(eval('M.%s' % cmd), args) typ, dat = apply(eval('M.%s' % cmd), args)
print ' %s %s\n => %s %s' % (cmd, args, typ, dat) _mesg(' %s %s\n => %s %s' % (cmd, args, typ, dat))
return dat return dat
Debug = 5 Debug = 5
M = IMAP4(host) M = IMAP4(host)
print 'PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION _mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
for cmd,args in test_seq1: for cmd,args in test_seq1:
run(cmd, args) run(cmd, args)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment