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.
This commit is contained in:
parent
75bb54c3d8
commit
26367a001d
203
Lib/imaplib.py
203
Lib/imaplib.py
|
@ -15,9 +15,9 @@ Public functions: Internaldate2tuple
|
|||
Time2Internaldate
|
||||
"""
|
||||
|
||||
__version__ = "2.11"
|
||||
__version__ = "2.15"
|
||||
|
||||
import binascii, re, socket, string, time, random
|
||||
import binascii, re, socket, string, time, random, sys
|
||||
|
||||
# Globals
|
||||
|
||||
|
@ -97,7 +97,9 @@ class IMAP4:
|
|||
|
||||
Errors raise the exception class <instance>.error("<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
|
||||
to the IMAP4 protocol, as the semantics of the arguments to
|
||||
|
@ -107,6 +109,7 @@ class IMAP4:
|
|||
|
||||
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
|
||||
|
||||
|
||||
def __init__(self, host = '', port = IMAP4_PORT):
|
||||
|
@ -136,7 +139,7 @@ class IMAP4:
|
|||
# request and store CAPABILITY response.
|
||||
|
||||
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()
|
||||
if self.untagged_responses.has_key('PREAUTH'):
|
||||
|
@ -154,7 +157,7 @@ class IMAP4:
|
|||
self.capabilities = tuple(string.split(self.untagged_responses[cap][-1]))
|
||||
|
||||
if __debug__ and self.debug >= 3:
|
||||
print '\tCAPABILITIES: %s' % `self.capabilities`
|
||||
_mesg('CAPABILITIES: %s' % `self.capabilities`)
|
||||
|
||||
for version in AllowedVersions:
|
||||
if not version in self.capabilities:
|
||||
|
@ -165,6 +168,17 @@ class IMAP4:
|
|||
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):
|
||||
"""Setup 'self.sock' and 'self.file'."""
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
|
@ -172,14 +186,43 @@ class IMAP4:
|
|||
self.file = self.sock.makefile('r')
|
||||
|
||||
|
||||
def __getattr__(self, attr):
|
||||
"""Allow UPPERCASE variants of all following IMAP4 commands."""
|
||||
if Commands.has_key(attr):
|
||||
return eval("self.%s" % string.lower(attr))
|
||||
raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
|
||||
def recent(self):
|
||||
"""Return most recent 'RECENT' responses if any exist,
|
||||
else prompt server for an update using the 'NOOP' command.
|
||||
|
||||
(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)
|
||||
|
||||
|
||||
# Public methods
|
||||
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
|
||||
|
||||
|
||||
|
||||
# IMAP4 commands
|
||||
|
||||
|
||||
def append(self, mailbox, flags, date_time, message):
|
||||
|
@ -224,7 +267,7 @@ class IMAP4:
|
|||
self.literal = _Authenticator(authobject).process
|
||||
typ, dat = self._simple_command('AUTHENTICATE', mech)
|
||||
if typ != 'OK':
|
||||
raise self.error(dat)
|
||||
raise self.error(dat[-1])
|
||||
self.state = 'AUTH'
|
||||
return typ, dat
|
||||
|
||||
|
@ -246,8 +289,7 @@ class IMAP4:
|
|||
(typ, [data]) = <instance>.close()
|
||||
"""
|
||||
try:
|
||||
try: typ, dat = self._simple_command('CLOSE')
|
||||
except EOFError: typ, dat = None, [None]
|
||||
typ, dat = self._simple_command('CLOSE')
|
||||
finally:
|
||||
self.state = 'AUTH'
|
||||
return typ, dat
|
||||
|
@ -288,7 +330,7 @@ class IMAP4:
|
|||
"""
|
||||
name = 'EXPUNGE'
|
||||
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):
|
||||
|
@ -300,7 +342,7 @@ class IMAP4:
|
|||
"""
|
||||
name = 'FETCH'
|
||||
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='*'):
|
||||
|
@ -312,7 +354,7 @@ class IMAP4:
|
|||
"""
|
||||
name = 'LIST'
|
||||
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):
|
||||
|
@ -322,7 +364,7 @@ class IMAP4:
|
|||
"""
|
||||
typ, dat = self._simple_command('LOGIN', user, password)
|
||||
if typ != 'OK':
|
||||
raise self.error(dat)
|
||||
raise self.error(dat[-1])
|
||||
self.state = 'AUTH'
|
||||
return typ, dat
|
||||
|
||||
|
@ -336,7 +378,7 @@ class IMAP4:
|
|||
"""
|
||||
self.state = '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.sock.close()
|
||||
if self.untagged_responses.has_key('BYE'):
|
||||
|
@ -353,7 +395,7 @@ class IMAP4:
|
|||
"""
|
||||
name = 'LSUB'
|
||||
typ, dat = self._simple_command(name, directory, pattern)
|
||||
return self._untagged_response(typ, name)
|
||||
return self._untagged_response(typ, dat, name)
|
||||
|
||||
|
||||
def noop(self):
|
||||
|
@ -362,7 +404,7 @@ class IMAP4:
|
|||
(typ, data) = <instance>.noop()
|
||||
"""
|
||||
if __debug__ and self.debug >= 3:
|
||||
print '\tuntagged responses: %s' % `self.untagged_responses`
|
||||
_dump_ur(self.untagged_responses)
|
||||
return self._simple_command('NOOP')
|
||||
|
||||
|
||||
|
@ -375,26 +417,7 @@ class IMAP4:
|
|||
"""
|
||||
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' 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)
|
||||
return self._untagged_response(typ, dat, 'FETCH')
|
||||
|
||||
|
||||
def rename(self, oldmailbox, newmailbox):
|
||||
|
@ -405,16 +428,6 @@ class IMAP4:
|
|||
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):
|
||||
"""Search mailbox for matching messages.
|
||||
|
||||
|
@ -426,7 +439,7 @@ class IMAP4:
|
|||
if charset:
|
||||
charset = 'CHARSET ' + charset
|
||||
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):
|
||||
|
@ -445,23 +458,17 @@ class IMAP4:
|
|||
else:
|
||||
name = 'SELECT'
|
||||
typ, dat = self._simple_command(name, mailbox)
|
||||
if typ == 'OK':
|
||||
self.state = 'SELECTED'
|
||||
elif typ == 'NO':
|
||||
self.state = 'AUTH'
|
||||
if not readonly and not self.untagged_responses.has_key('READ-WRITE'):
|
||||
raise self.error('%s is not writable' % mailbox)
|
||||
if typ != 'OK':
|
||||
self.state = 'AUTH' # Might have been 'SELECTED'
|
||||
return typ, dat
|
||||
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)
|
||||
raise self.readonly('%s is not writable' % mailbox)
|
||||
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):
|
||||
"""Request named status conditions for mailbox.
|
||||
|
||||
|
@ -471,7 +478,7 @@ class IMAP4:
|
|||
if self.PROTOCOL_VERSION == 'IMAP4':
|
||||
raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
|
||||
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):
|
||||
|
@ -480,7 +487,7 @@ class IMAP4:
|
|||
(typ, [data]) = <instance>.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):
|
||||
|
@ -511,9 +518,7 @@ class IMAP4:
|
|||
name = 'SEARCH'
|
||||
else:
|
||||
name = 'FETCH'
|
||||
typ, dat2 = self._untagged_response(typ, name)
|
||||
if dat2[-1]: dat = dat2
|
||||
return typ, dat
|
||||
return self._untagged_response(typ, dat, name)
|
||||
|
||||
|
||||
def unsubscribe(self, mailbox):
|
||||
|
@ -542,12 +547,13 @@ class IMAP4:
|
|||
def _append_untagged(self, typ, dat):
|
||||
|
||||
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):
|
||||
ur[typ].append(dat)
|
||||
else:
|
||||
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):
|
||||
|
@ -557,8 +563,14 @@ 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']
|
||||
for typ in ('OK', 'NO', 'BAD'):
|
||||
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()
|
||||
data = '%s %s' % (tag, name)
|
||||
|
@ -588,7 +600,7 @@ class IMAP4:
|
|||
raise self.abort('socket error: %s' % val)
|
||||
|
||||
if __debug__ and self.debug >= 4:
|
||||
print '\t> %s' % data
|
||||
_mesg('> %s' % data)
|
||||
|
||||
if literal is None:
|
||||
return tag
|
||||
|
@ -606,7 +618,7 @@ class IMAP4:
|
|||
literal = literator(self.continuation_response)
|
||||
|
||||
if __debug__ and self.debug >= 4:
|
||||
print '\twrite literal size %s' % len(literal)
|
||||
_mesg('write literal size %s' % len(literal))
|
||||
|
||||
try:
|
||||
self.sock.send(literal)
|
||||
|
@ -684,7 +696,7 @@ class IMAP4:
|
|||
|
||||
size = string.atoi(self.mo.group('size'))
|
||||
if __debug__ and self.debug >= 4:
|
||||
print '\tread literal size %s' % size
|
||||
_mesg('read literal size %s' % size)
|
||||
data = self.file.read(size)
|
||||
|
||||
# Store response with literal as tuple
|
||||
|
@ -702,6 +714,9 @@ class IMAP4:
|
|||
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))
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
|
@ -719,13 +734,13 @@ class IMAP4:
|
|||
|
||||
line = self.file.readline()
|
||||
if not line:
|
||||
raise EOFError
|
||||
raise self.abort('socket error: EOF')
|
||||
|
||||
# Protocol mandates all lines terminated by CRLF
|
||||
|
||||
line = line[:-2]
|
||||
if __debug__ and self.debug >= 4:
|
||||
print '\t< %s' % line
|
||||
_mesg('< %s' % line)
|
||||
return line
|
||||
|
||||
|
||||
|
@ -736,7 +751,7 @@ class IMAP4:
|
|||
|
||||
self.mo = cre.match(s)
|
||||
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
|
||||
|
||||
|
||||
|
@ -753,13 +768,15 @@ class IMAP4:
|
|||
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):
|
||||
return typ, [None]
|
||||
data = self.untagged_responses[name]
|
||||
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]
|
||||
return typ, data
|
||||
|
||||
|
@ -905,9 +922,19 @@ def Time2Internaldate(date_time):
|
|||
|
||||
if __debug__:
|
||||
|
||||
def _trunc(m, s):
|
||||
if len(s) <= m: return s
|
||||
return '%.*s..' % (m, s)
|
||||
def _mesg(s):
|
||||
# if len(s) > 70: s = '%.70s..' % 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__':
|
|||
|
||||
def run(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
|
||||
|
||||
Debug = 5
|
||||
M = IMAP4(host)
|
||||
print 'PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION
|
||||
_mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
|
||||
|
||||
for cmd,args in test_seq1:
|
||||
run(cmd, args)
|
||||
|
|
Loading…
Reference in New Issue