Commit eda960a1 authored by Guido van Rossum's avatar Guido van Rossum

Piers' latest version -- authentication added by Donn Cave.

parent faac0136
...@@ -4,6 +4,8 @@ Based on RFC 2060. ...@@ -4,6 +4,8 @@ Based on RFC 2060.
Author: Piers Lauder <piers@cs.su.oz.au> December 1997. Author: Piers Lauder <piers@cs.su.oz.au> December 1997.
Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
Public class: IMAP4 Public class: IMAP4
Public variable: Debug Public variable: Debug
Public functions: Internaldate2tuple Public functions: Internaldate2tuple
...@@ -11,8 +13,12 @@ Public functions: Internaldate2tuple ...@@ -11,8 +13,12 @@ Public functions: Internaldate2tuple
ParseFlags ParseFlags
Time2Internaldate Time2Internaldate
""" """
#
# $Header$
#
__version__ = "$Revision$"
import re, socket, string, time, random import binascii, re, socket, string, time, random
# Globals # Globals
...@@ -41,6 +47,7 @@ Commands = { ...@@ -41,6 +47,7 @@ Commands = {
'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'), 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
'LSUB': ('AUTH', 'SELECTED'), 'LSUB': ('AUTH', 'SELECTED'),
'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'), 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
'PARTIAL': ('SELECTED',),
'RENAME': ('AUTH', 'SELECTED'), 'RENAME': ('AUTH', 'SELECTED'),
'SEARCH': ('SELECTED',), 'SEARCH': ('SELECTED',),
'SELECT': ('AUTH', 'SELECTED'), 'SELECT': ('AUTH', 'SELECTED'),
...@@ -53,7 +60,7 @@ Commands = { ...@@ -53,7 +60,7 @@ Commands = {
# Patterns to match server responses # Patterns to match server responses
Continuation = re.compile(r'\+ (?P<data>.*)') Continuation = re.compile(r'\+( (?P<data>.*))?')
Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)') Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
InternalDate = re.compile(r'.*INTERNALDATE "' InternalDate = re.compile(r'.*INTERNALDATE "'
r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])' r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
...@@ -62,7 +69,7 @@ InternalDate = re.compile(r'.*INTERNALDATE "' ...@@ -62,7 +69,7 @@ InternalDate = re.compile(r'.*INTERNALDATE "'
r'"') r'"')
Literal = re.compile(r'(?P<data>.*) {(?P<size>\d+)}$') Literal = re.compile(r'(?P<data>.*) {(?P<size>\d+)}$')
Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]') Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+) (?P<data>.*)') Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?') Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
...@@ -81,8 +88,9 @@ class IMAP4: ...@@ -81,8 +88,9 @@ class IMAP4:
All arguments to commands are converted to strings, except for All arguments to commands are converted to strings, except for
the last argument to APPEND which is passed as an IMAP4 the last argument to APPEND which is passed as an IMAP4
literal. If necessary (the string isn't enclosed with either literal. If necessary (the string contains white-space and
parentheses or double quotes) each converted string is quoted. isn't enclosed with either parentheses or double quotes) each
string is quoted.
Each command returns a tuple: (type, [data, ...]) where 'type' Each command returns a tuple: (type, [data, ...]) where 'type'
is usually 'OK' or 'NO', and 'data' is either the text from the is usually 'OK' or 'NO', and 'data' is either the text from the
...@@ -91,6 +99,11 @@ class IMAP4: ...@@ -91,6 +99,11 @@ 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'.
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 class error(Exception): pass # Logical errors - debug required
...@@ -110,9 +123,7 @@ class IMAP4: ...@@ -110,9 +123,7 @@ class IMAP4:
# Open socket to server. # Open socket to server.
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.open(host, port)
self.sock.connect(self.host, self.port)
self.file = self.sock.makefile('r')
# Create unique tag for this session, # Create unique tag for this session,
# and compile tagged response matcher. # and compile tagged response matcher.
...@@ -156,6 +167,13 @@ class IMAP4: ...@@ -156,6 +167,13 @@ class IMAP4:
raise self.error('server not IMAP4 compliant') 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): def __getattr__(self, attr):
"""Allow UPPERCASE variants of all following IMAP4 commands.""" """Allow UPPERCASE variants of all following IMAP4 commands."""
if Commands.has_key(attr): if Commands.has_key(attr):
...@@ -173,6 +191,7 @@ class IMAP4: ...@@ -173,6 +191,7 @@ class IMAP4:
""" """
name = 'APPEND' name = 'APPEND'
if flags: if flags:
if (flags[0],flags[-1]) != ('(',')'):
flags = '(%s)' % flags flags = '(%s)' % flags
else: else:
flags = None flags = None
...@@ -184,12 +203,32 @@ class IMAP4: ...@@ -184,12 +203,32 @@ class IMAP4:
return self._simple_command(name, mailbox, flags, date_time) return self._simple_command(name, mailbox, flags, date_time)
def authenticate(self, func): def authenticate(self, mechanism, authobject):
"""Authenticate command - requires response processing. """Authenticate command - requires response processing.
UNIMPLEMENTED 'mechanism' specifies which authentication mechanism is to
be used - it must appear in <instance>.capabilities in the
form AUTH=<mechanism>.
'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): def check(self):
...@@ -324,18 +363,32 @@ class IMAP4: ...@@ -324,18 +363,32 @@ class IMAP4:
(typ, data) = <instance>.noop() (typ, data) = <instance>.noop()
""" """
if __debug__ and self.debug >= 3:
print '\tuntagged responses: %s' % `self.untagged_responses`
return self._simple_command('NOOP') return self._simple_command('NOOP')
def partial(self, message_num, message_part, start, length):
"""Fetch truncated part of a message.
(typ, [data, ...]) = <instance>.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): 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, else prompt server for an update using the 'NOOP' command,
and flush all untagged responses. and flush all untagged responses.
(typ, [data]) = <instance>.recent() (typ, [data]) = <instance>.recent()
'data' is None if no new messages, 'data' is None if no new messages,
else value of RECENT response. else list of RECENT responses, most recent last.
""" """
name = 'RECENT' name = 'RECENT'
typ, dat = self._untagged_response('OK', name) typ, dat = self._untagged_response('OK', name)
...@@ -361,7 +414,7 @@ class IMAP4: ...@@ -361,7 +414,7 @@ class IMAP4:
(code, [data]) = <instance>.response(code) (code, [data]) = <instance>.response(code)
""" """
return self._untagged_response(code, code) return self._untagged_response(code, string.upper(code))
def search(self, charset, criteria): def search(self, charset, criteria):
...@@ -403,6 +456,14 @@ class IMAP4: ...@@ -403,6 +456,14 @@ class IMAP4:
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.
...@@ -440,8 +501,14 @@ class IMAP4: ...@@ -440,8 +501,14 @@ class IMAP4:
Returns response appropriate to 'command'. 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' name = 'UID'
typ, dat = apply(self._simple_command, ('UID', command) + args) typ, dat = apply(self._simple_command, (name, command) + args)
if command == 'SEARCH': if command == 'SEARCH':
name = 'SEARCH' name = 'SEARCH'
else: else:
...@@ -476,13 +543,13 @@ class IMAP4: ...@@ -476,13 +543,13 @@ class IMAP4:
def _append_untagged(self, typ, dat): def _append_untagged(self, typ, dat):
if self.untagged_responses.has_key(typ): ur = self.untagged_responses
self.untagged_responses[typ].append(dat) if ur.has_key(typ):
ur[typ].append(dat)
else: else:
self.untagged_responses[typ] = [dat] ur[typ] = [dat]
if __debug__ and self.debug >= 5: 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): def _command(self, name, *args):
...@@ -492,6 +559,9 @@ class IMAP4: ...@@ -492,6 +559,9 @@ 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'):
del self.untagged_responses['OK']
tag = self._new_tag() tag = self._new_tag()
data = '%s %s' % (tag, name) data = '%s %s' % (tag, name)
for d in args: for d in args:
...@@ -508,6 +578,10 @@ class IMAP4: ...@@ -508,6 +578,10 @@ class IMAP4:
literal = self.literal literal = self.literal
if literal is not None: if literal is not None:
self.literal = None self.literal = None
if type(literal) is type(self._command):
literator = literal
else:
literator = None
data = '%s {%s}' % (data, len(literal)) data = '%s {%s}' % (data, len(literal))
try: try:
...@@ -521,6 +595,7 @@ class IMAP4: ...@@ -521,6 +595,7 @@ class IMAP4:
if literal is None: if literal is None:
return tag return tag
while 1:
# Wait for continuation response # Wait for continuation response
while self._get_response(): while self._get_response():
...@@ -529,6 +604,9 @@ class IMAP4: ...@@ -529,6 +604,9 @@ class IMAP4:
# Send literal # Send literal
if literator:
literal = literator(self.continuation_response)
if __debug__ and self.debug >= 4: if __debug__ and self.debug >= 4:
print '\twrite literal size %s' % len(literal) print '\twrite literal size %s' % len(literal)
...@@ -538,6 +616,9 @@ class IMAP4: ...@@ -538,6 +616,9 @@ class IMAP4:
except socket.error, val: except socket.error, val:
raise self.abort('socket error: %s' % val) raise self.abort('socket error: %s' % val)
if not literator:
break
return tag return tag
...@@ -590,10 +671,11 @@ class IMAP4: ...@@ -590,10 +671,11 @@ class IMAP4:
self.continuation_response = self.mo.group('data') self.continuation_response = self.mo.group('data')
return None # NB: indicates continuation return None # NB: indicates continuation
raise self.abort('unexpected response: %s' % resp) raise self.abort("unexpected response: '%s'" % resp)
typ = self.mo.group('type') typ = self.mo.group('type')
dat = self.mo.group('data') dat = self.mo.group('data')
if dat is None: dat = '' # Null untagged response
if dat2: dat = dat + ' ' + dat2 if dat2: dat = dat + ' ' + dat2
# Is there a literal to come? # Is there a literal to come?
...@@ -679,12 +761,56 @@ class IMAP4: ...@@ -679,12 +761,56 @@ class IMAP4:
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] => %.20s..' % (name, `data`) print '\tuntagged_responses[%s] => %s' % (name, _trunc(20, `data`))
del self.untagged_responses[name] del self.untagged_responses[name]
return typ, data 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, 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} 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
...@@ -779,6 +905,14 @@ def Time2Internaldate(date_time): ...@@ -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__': if __debug__ and __name__ == '__main__':
host = '' host = ''
...@@ -798,8 +932,8 @@ if __debug__ and __name__ == '__main__': ...@@ -798,8 +932,8 @@ if __debug__ and __name__ == '__main__':
('CREATE', ('/tmp/yyz 2',)), ('CREATE', ('/tmp/yyz 2',)),
('append', ('/tmp/yyz 2', None, None, 'From: anon@x.y.z\n\ndata...')), ('append', ('/tmp/yyz 2', None, None, 'From: anon@x.y.z\n\ndata...')),
('select', ('/tmp/yyz 2',)), ('select', ('/tmp/yyz 2',)),
('uid', ('SEARCH', 'ALL')), ('search', (None, '(TO zork)')),
('fetch', ('1', '(INTERNALDATE RFC822)')), ('partial', ('1', 'RFC822', 1, 1024)),
('store', ('1', 'FLAGS', '(\Deleted)')), ('store', ('1', 'FLAGS', '(\Deleted)')),
('expunge', ()), ('expunge', ()),
('recent', ()), ('recent', ()),
...@@ -820,7 +954,7 @@ if __debug__ and __name__ == '__main__': ...@@ -820,7 +954,7 @@ if __debug__ and __name__ == '__main__':
print ' %s %s\n => %s %s' % (cmd, args, typ, dat) print ' %s %s\n => %s %s' % (cmd, args, typ, dat)
return dat return dat
Debug = 4 Debug = 5
M = IMAP4(host) M = IMAP4(host)
print 'PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION print 'PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION
...@@ -839,6 +973,6 @@ if __debug__ and __name__ == '__main__': ...@@ -839,6 +973,6 @@ if __debug__ and __name__ == '__main__':
if (cmd,args) != ('uid', ('SEARCH', 'ALL')): if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
continue continue
uid = string.split(dat[0])[-1] uid = string.split(dat[-1])[-1]
run('uid', ('FETCH', '%s' % uid, run('uid', ('FETCH', '%s' % uid,
'(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822)')) '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
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