Merge branch 'ss/imaplib2-2.28' into next

Conflicts:
	Changelog.draft.rst
This commit is contained in:
Nicolas Sebrecht 2011-08-15 13:57:37 +02:00
commit 2749374a93
4 changed files with 195 additions and 255 deletions

View File

@ -22,9 +22,9 @@ Changes
* Refactor our IMAPServer class. Background work without user-visible * Refactor our IMAPServer class. Background work without user-visible
changes. changes.
* Remove the configurability of the Blinkenlights statuschar. It * Remove the configurability of the Blinkenlights statuschar. It
cluttered the main configuration file for little gain. cluttered the main configuration file for little gain.
* Updated bundled imaplib2 to version 2.28.
Bug Fixes Bug Fixes
--------- ---------

View File

@ -2,7 +2,7 @@
"""Threaded IMAP4 client. """Threaded IMAP4 client.
Based on RFC 2060 and original imaplib module. Based on RFC 3501 and original imaplib module.
Public classes: IMAP4 Public classes: IMAP4
IMAP4_SSL IMAP4_SSL
@ -17,9 +17,9 @@ Public functions: Internaldate2Time
__all__ = ("IMAP4", "IMAP4_SSL", "IMAP4_stream", __all__ = ("IMAP4", "IMAP4_SSL", "IMAP4_stream",
"Internaldate2Time", "ParseFlags", "Time2Internaldate") "Internaldate2Time", "ParseFlags", "Time2Internaldate")
__version__ = "2.24" __version__ = "2.28"
__release__ = "2" __release__ = "2"
__revision__ = "24" __revision__ = "28"
__credits__ = """ __credits__ = """
Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998. Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
String method conversion by ESR, February 2001. String method conversion by ESR, February 2001.
@ -38,7 +38,8 @@ Improved timeout handling contributed by Ivan Vovnenko <ivovnenko@gmail.com> Oct
Timeout handling further improved by Ethan Glasser-Camp <glasse@cs.rpi.edu> December 2010. Timeout handling further improved by Ethan Glasser-Camp <glasse@cs.rpi.edu> December 2010.
Time2Internaldate() patch to match RFC2060 specification of English month names from bugs.python.org/issue11024 March 2011. Time2Internaldate() patch to match RFC2060 specification of English month names from bugs.python.org/issue11024 March 2011.
starttls() bug fixed with the help of Sebastian Spaeth <sebastian@sspaeth.de> April 2011. starttls() bug fixed with the help of Sebastian Spaeth <sebastian@sspaeth.de> April 2011.
Threads now set the "daemon" flag (suggested by offlineimap-project).""" Threads now set the "daemon" flag (suggested by offlineimap-project) April 2011.
Single quoting introduced with the help of Vladimir Marek <vladimir.marek@oracle.com> August 2011."""
__author__ = "Piers Lauder <piers@janeelix.com>" __author__ = "Piers Lauder <piers@janeelix.com>"
__URL__ = "http://imaplib2.sourceforge.net" __URL__ = "http://imaplib2.sourceforge.net"
__license__ = "Python License" __license__ = "Python License"
@ -88,7 +89,7 @@ Commands = {
'GETANNOTATION':((AUTH, SELECTED), True), 'GETANNOTATION':((AUTH, SELECTED), True),
'GETQUOTA': ((AUTH, SELECTED), True), 'GETQUOTA': ((AUTH, SELECTED), True),
'GETQUOTAROOT': ((AUTH, SELECTED), True), 'GETQUOTAROOT': ((AUTH, SELECTED), True),
'ID': ((NONAUTH, AUTH, SELECTED), True), 'ID': ((NONAUTH, AUTH, LOGOUT, SELECTED), True),
'IDLE': ((SELECTED,), False), 'IDLE': ((SELECTED,), False),
'LIST': ((AUTH, SELECTED), True), 'LIST': ((AUTH, SELECTED), True),
'LOGIN': ((NONAUTH,), False), 'LOGIN': ((NONAUTH,), False),
@ -137,11 +138,14 @@ class Request(object):
"""Private class to represent a request awaiting response.""" """Private class to represent a request awaiting response."""
def __init__(self, parent, name=None, callback=None, cb_arg=None): def __init__(self, parent, name=None, callback=None, cb_arg=None, cb_self=False):
self.parent = parent self.parent = parent
self.name = name self.name = name
self.callback = callback # Function called to process result self.callback = callback # Function called to process result
if not cb_self:
self.callback_arg = cb_arg # Optional arg passed to "callback" self.callback_arg = cb_arg # Optional arg passed to "callback"
else:
self.callback_arg = (self, cb_arg) # Self reference required in callback arg
self.tag = '%s%s' % (parent.tagpre, parent.tagnum) self.tag = '%s%s' % (parent.tagpre, parent.tagnum)
parent.tagnum += 1 parent.tagnum += 1
@ -153,9 +157,6 @@ class Request(object):
def abort(self, typ, val): def abort(self, typ, val):
"""Called whenever we abort a command
Sets self.aborted reason, and deliver()s nothing"""
self.aborted = (typ, val) self.aborted = (typ, val)
self.deliver(None) self.deliver(None)
@ -238,12 +239,17 @@ class IMAP4(object):
All (non-callback) arguments to commands are converted to strings, All (non-callback) arguments to commands are converted to strings,
except for AUTHENTICATE, and the last argument to APPEND which is except for AUTHENTICATE, and the last argument to APPEND which is
passed as an IMAP4 literal. If necessary (the string contains any passed as an IMAP4 literal. If necessary (the string contains any
non-printing characters or white-space and isn't enclosed with either non-printing characters or white-space and isn't enclosed with
parentheses or double quotes) each string is quoted. However, the either parentheses or double or single quotes) each string is
'password' argument to the LOGIN command is always quoted. If you quoted. However, the 'password' argument to the LOGIN command is
want to avoid having an argument string quoted (eg: the 'flags' always quoted. If you want to avoid having an argument string
argument to STORE) then enclose the string in parentheses (eg: quoted (eg: the 'flags' argument to STORE) then enclose the string
"(\Deleted)"). in parentheses (eg: "(\Deleted)"). If you are using "sequence sets"
containing the wildcard character '*', then enclose the argument
in single quotes: the quotes will be removed and the resulting
string passed unquoted. Note also that you can pass in an argument
with a type that doesn't evaluate to 'basestring' (eg: 'bytearray')
and it will be converted to a string without quoting.
There is one instance variable, 'state', that is useful for tracking There is one instance variable, 'state', that is useful for tracking
whether the client needs to login to the server. If it has the whether the client needs to login to the server. If it has the
@ -275,6 +281,7 @@ class IMAP4(object):
# so match not the inverse set # so match not the inverse set
mustquote_cre = re.compile(r"[^!#$&'+,./0-9:;<=>?@A-Z\[^_`a-z|}~-]") mustquote_cre = re.compile(r"[^!#$&'+,./0-9:;<=>?@A-Z\[^_`a-z|}~-]")
response_code_cre = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]') response_code_cre = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
# sequence_set_cre = re.compile(r"^[0-9]+(:([0-9]+|\*))?(,[0-9]+(:([0-9]+|\*))?)*$")
untagged_response_cre = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?') untagged_response_cre = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
untagged_status_cre = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?') untagged_status_cre = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
@ -339,8 +346,6 @@ class IMAP4(object):
self.state_change_free = threading.Event() self.state_change_free = threading.Event()
self.state_change_pending = threading.Lock() self.state_change_pending = threading.Lock()
self.commands_lock = threading.Lock() self.commands_lock = threading.Lock()
"""commands_lock prevents self.untagged_responses to be
manipulated concurrently"""
self.idle_lock = threading.Lock() self.idle_lock = threading.Lock()
self.ouq = Queue.Queue(10) self.ouq = Queue.Queue(10)
@ -368,7 +373,7 @@ class IMAP4(object):
elif self._get_untagged_response('OK'): elif self._get_untagged_response('OK'):
if __debug__: self._log(1, 'state => NONAUTH') if __debug__: self._log(1, 'state => NONAUTH')
else: else:
raise self.error(self.welcome) raise self.error('unrecognised server welcome message: %s' % `self.welcome`)
typ, dat = self.capability() typ, dat = self.capability()
if dat == [None]: if dat == [None]:
@ -443,6 +448,35 @@ class IMAP4(object):
return s return s
def ssl_wrap_socket(self):
# Allow sending of keep-alive messages - seems to prevent some servers
# from closing SSL, leading to deadlocks.
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
try:
import ssl
if self.ca_certs is not None:
cert_reqs = ssl.CERT_REQUIRED
else:
cert_reqs = ssl.CERT_NONE
self.sock = ssl.wrap_socket(self.sock, self.keyfile, self.certfile, ca_certs=self.ca_certs, cert_reqs=cert_reqs)
ssl_exc = ssl.SSLError
except ImportError:
# No ssl module, and socket.ssl does not allow certificate verification
if self.ca_certs is not None:
raise socket.sslerror("SSL CA certificates cannot be checked without ssl module")
self.sock = socket.ssl(self.sock, self.keyfile, self.certfile)
ssl_exc = socket.sslerror
if self.cert_verify_cb is not None:
cert_err = self.cert_verify_cb(self.sock.getpeercert(), self.host)
if cert_err:
raise ssl_exc(cert_err)
self.read_fd = self.sock.fileno()
def start_compressing(self): def start_compressing(self):
"""start_compressing() """start_compressing()
Enable deflate compression on the socket (RFC 4978).""" Enable deflate compression on the socket (RFC 4978)."""
@ -671,7 +705,7 @@ class IMAP4(object):
def examine(self, mailbox='INBOX', **kw): def examine(self, mailbox='INBOX', **kw):
"""(typ, [data]) = examine(mailbox='INBOX', readonly=False) """(typ, [data]) = examine(mailbox='INBOX')
Select a mailbox for READ-ONLY access. (Flushes all untagged responses.) Select a mailbox for READ-ONLY access. (Flushes all untagged responses.)
'data' is count of messages in mailbox ('EXISTS' response). 'data' is count of messages in mailbox ('EXISTS' response).
Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
@ -745,13 +779,23 @@ class IMAP4(object):
def id(self, *kv_pairs, **kw): def id(self, *kv_pairs, **kw):
"""(typ, [data]) = <instance>.id(kv_pairs) """(typ, [data]) = <instance>.id(kv_pairs)
'data' is list of ID key value pairs. 'kv_pairs' is a possibly empty list of keys and values.
Request information for problem analysis and determination. 'data' is a list of ID key value pairs or NIL.
NB: a single argument is assumed to be correctly formatted and is passed through unchanged
(for backward compatibility with earlier version).
Exchange information for problem analysis and determination.
The ID extension is defined in RFC 2971. """ The ID extension is defined in RFC 2971. """
name = 'ID' name = 'ID'
kw['untagged_response'] = name kw['untagged_response'] = name
return self._simple_command(name, *kv_pairs, **kw)
if not kv_pairs:
data = 'NIL'
elif len(kv_pairs) == 1:
data = kv_pairs[0] # Assume invoker passing correctly formatted string (back-compat)
else:
data = '(%s)' % ' '.join([(arg and self._quote(arg) or 'NIL') for arg in kv_pairs])
return self._simple_command(name, (data,), **kw)
def idle(self, timeout=None, **kw): def idle(self, timeout=None, **kw):
@ -999,8 +1043,8 @@ class IMAP4(object):
return self._simple_command(name, sort_criteria, charset, *search_criteria, **kw) return self._simple_command(name, sort_criteria, charset, *search_criteria, **kw)
def starttls(self, keyfile=None, certfile=None, **kw): def starttls(self, keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None, **kw):
"""(typ, [data]) = starttls(keyfile=None, certfile=None) """(typ, [data]) = starttls(keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None)
Start TLS negotiation as per RFC 2595.""" Start TLS negotiation as per RFC 2595."""
name = 'STARTTLS' name = 'STARTTLS'
@ -1031,14 +1075,13 @@ class IMAP4(object):
self.rdth.start() self.rdth.start()
raise self.error("Couldn't establish TLS session: %s" % dat) raise self.error("Couldn't establish TLS session: %s" % dat)
try: self.keyfile = keyfile
try: self.certfile = certfile
import ssl self.ca_certs = ca_certs
self.sock = ssl.wrap_socket(self.sock, keyfile, certfile) self.cert_verify_cb = cert_verify_cb
except ImportError:
self.sock = socket.ssl(self.sock, keyfile, certfile)
self.read_fd = self.sock.fileno() try:
self.ssl_wrap_socket()
finally: finally:
# Restart reader thread # Restart reader thread
self.rdth = threading.Thread(target=self._reader) self.rdth = threading.Thread(target=self._reader)
@ -1140,29 +1183,34 @@ class IMAP4(object):
def _append_untagged(self, typ, dat): def _append_untagged(self, typ, dat):
"""Append new untagged response
Append new 'dat' to end of last untagged response if same 'typ', # Append new 'dat' to end of last untagged response if same 'typ',
else append new response.""" # else append new response.
if dat is None: dat = '' if dat is None: dat = ''
ur_data = []
self.commands_lock.acquire() # protect untagged_responses self.commands_lock.acquire()
if self.untagged_responses and self.untagged_responses[-1][0] == typ: if self.untagged_responses:
# last respons is of type 'typ', get ur_data for appending urn, urd = self.untagged_responses[-1]
ur_data = self.untagged_responses[-1][1] if urn != typ:
urd = None
else: else:
# need to create new untagged response of this type urd = None
self.untagged_responses.append([typ, ur_data])
if urd is None:
urd = []
self.untagged_responses.append([typ, urd])
urd.append(dat)
ur_data.append(dat)
self.commands_lock.release() self.commands_lock.release()
if __debug__: self._log(5, 'untagged_responses[%s] %s += ["%s"]' % (typ, len(ur_data)-1, dat))
if __debug__: self._log(5, 'untagged_responses[%s] %s += ["%s"]' % (typ, len(urd)-1, dat))
def _check_bye(self): def _check_bye(self):
"""raise Exception if untagged responses contains a 'BYE'"""
bye = self._get_untagged_response('BYE', leave=True) bye = self._get_untagged_response('BYE', leave=True)
if bye: if bye:
raise self.abort(bye[-1]) raise self.abort(bye[-1])
@ -1171,12 +1219,14 @@ class IMAP4(object):
def _checkquote(self, arg): def _checkquote(self, arg):
# Must quote command args if "atom-specials" present, # Must quote command args if "atom-specials" present,
# and not already quoted. # and not already quoted. NB: single quotes are removed.
if not isinstance(arg, basestring): if not isinstance(arg, basestring):
return arg return arg
if len(arg) >= 2 and (arg[0],arg[-1]) in (('(',')'),('"','"')): if len(arg) >= 2 and (arg[0],arg[-1]) in (('(',')'),('"','"')):
return arg return arg
if len(arg) >= 2 and (arg[0],arg[-1]) in (("'","'"),):
return arg[1:-1]
if arg and self.mustquote_cre.search(arg) is None: if arg and self.mustquote_cre.search(arg) is None:
return arg return arg
return self._quote(arg) return self._quote(arg)
@ -1372,11 +1422,7 @@ class IMAP4(object):
def _get_untagged_response(self, name, leave=False): def _get_untagged_response(self, name, leave=False):
"""Return an untagged response of type 'name'
:param leave: If leave (default: False) is True, we keep the
fetched responsem; otherwise it will be deleted. Returns
None if no such response found."""
self.commands_lock.acquire() self.commands_lock.acquire()
for i, (typ, dat) in enumerate(self.untagged_responses): for i, (typ, dat) in enumerate(self.untagged_responses):
@ -1543,24 +1589,13 @@ class IMAP4(object):
def _simple_command(self, name, *args, **kw): def _simple_command(self, name, *args, **kw):
if 'callback' in kw: if 'callback' in kw:
rqb = self._command(name, callback=self._command_completer, *args) self._command(name, *args, callback=self._command_completer, cb_arg=kw, cb_self=True)
rqb.callback_arg = (rqb, kw)
return (None, None) return (None, None)
return self._command_complete(self._command(name, *args), kw) return self._command_complete(self._command(name, *args), kw)
def _untagged_response(self, typ, dat, name): def _untagged_response(self, typ, dat, name):
"""Returns an untagged response for 'name' of type 'typ'
:param typ: 'OK, 'NO', etc... which will be used for the type of
the response.
:param dat: The fallback data to be used in case `typ` is
'NO'. Otherwise the data from the existing untagged
responses will be searched for data to be returned. If there
is no such response, we return `[None]` as data.
:param name: The name of the response.
:returns: (typ, data)
"""
if typ == 'NO': if typ == 'NO':
return typ, dat return typ, dat
data = self._get_untagged_response(name) data = self._get_untagged_response(name)
@ -1940,18 +1975,23 @@ class IMAP4_SSL(IMAP4):
port - port number (default: standard IMAP4 SSL port); port - port number (default: standard IMAP4 SSL port);
keyfile - PEM formatted file that contains your private key (default: None); keyfile - PEM formatted file that contains your private key (default: None);
certfile - PEM formatted certificate chain file (default: None); certfile - PEM formatted certificate chain file (default: None);
ca_certs - PEM formatted certificate chain file used to validate server certificates (default: None);
cert_verify_cb - function to verify authenticity of server certificates (default: None);
debug - debug level (default: 0 - no debug); debug - debug level (default: 0 - no debug);
debug_file - debug stream (default: sys.stderr); debug_file - debug stream (default: sys.stderr);
identifier - thread identifier prefix (default: host); identifier - thread identifier prefix (default: host);
timeout - timeout in seconds when expecting a command response. timeout - timeout in seconds when expecting a command response.
debug_buf_lvl - debug level at which buffering is turned off.
For more documentation see the docstring of the parent class IMAP4. For more documentation see the docstring of the parent class IMAP4.
""" """
def __init__(self, host=None, port=None, keyfile=None, certfile=None, debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None): def __init__(self, host=None, port=None, keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None, debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None):
self.keyfile = keyfile self.keyfile = keyfile
self.certfile = certfile self.certfile = certfile
self.ca_certs = ca_certs
self.cert_verify_cb = cert_verify_cb
IMAP4.__init__(self, host, port, debug, debug_file, identifier, timeout, debug_buf_lvl) IMAP4.__init__(self, host, port, debug, debug_file, identifier, timeout, debug_buf_lvl)
@ -1965,14 +2005,7 @@ class IMAP4_SSL(IMAP4):
self.host = self._choose_nonull_or_dflt('', host) self.host = self._choose_nonull_or_dflt('', host)
self.port = self._choose_nonull_or_dflt(IMAP4_SSL_PORT, port) self.port = self._choose_nonull_or_dflt(IMAP4_SSL_PORT, port)
self.sock = self.open_socket() self.sock = self.open_socket()
self.ssl_wrap_socket()
try:
import ssl
self.sslobj = ssl.wrap_socket(self.sock, self.keyfile, self.certfile)
except ImportError:
self.sslobj = socket.ssl(self.sock, self.keyfile, self.certfile)
self.read_fd = self.sock.fileno()
def read(self, size): def read(self, size):
@ -1980,12 +2013,12 @@ class IMAP4_SSL(IMAP4):
Read at most 'size' bytes from remote.""" Read at most 'size' bytes from remote."""
if self.decompressor is None: if self.decompressor is None:
return self.sslobj.read(size) return self.sock.read(size)
if self.decompressor.unconsumed_tail: if self.decompressor.unconsumed_tail:
data = self.decompressor.unconsumed_tail data = self.decompressor.unconsumed_tail
else: else:
data = self.sslobj.read(8192) data = self.sock.read(8192)
return self.decompressor.decompress(data, size) return self.decompressor.decompress(data, size)
@ -1998,10 +2031,12 @@ class IMAP4_SSL(IMAP4):
data = self.compressor.compress(data) data = self.compressor.compress(data)
data += self.compressor.flush(zlib.Z_SYNC_FLUSH) data += self.compressor.flush(zlib.Z_SYNC_FLUSH)
# NB: socket.ssl needs a "sendall" method to match socket objects. if hasattr(self.sock, "sendall"):
self.sock.sendall(data)
else:
bytes = len(data) bytes = len(data)
while bytes > 0: while bytes > 0:
sent = self.sslobj.write(data) sent = self.sock.write(data)
if sent == bytes: if sent == bytes:
break # avoid copy break # avoid copy
data = data[sent:] data = data[sent:]
@ -2012,7 +2047,7 @@ class IMAP4_SSL(IMAP4):
"""ssl = ssl() """ssl = ssl()
Return socket.ssl instance used to communicate with the IMAP4 server.""" Return socket.ssl instance used to communicate with the IMAP4 server."""
return self.sslobj return self.sock
@ -2021,13 +2056,14 @@ class IMAP4_stream(IMAP4):
"""IMAP4 client class over a stream """IMAP4 client class over a stream
Instantiate with: Instantiate with:
IMAP4_stream(command, debug=None, debug_file=None, identifier=None, timeout=None) IMAP4_stream(command, debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None)
command - string that can be passed to subprocess.Popen(); command - string that can be passed to subprocess.Popen();
debug - debug level (default: 0 - no debug); debug - debug level (default: 0 - no debug);
debug_file - debug stream (default: sys.stderr); debug_file - debug stream (default: sys.stderr);
identifier - thread identifier prefix (default: host); identifier - thread identifier prefix (default: host);
timeout - timeout in seconds when expecting a command response. timeout - timeout in seconds when expecting a command response.
debug_buf_lvl - debug level at which buffering is turned off.
For more documentation see the docstring of the parent class IMAP4. For more documentation see the docstring of the parent class IMAP4.
""" """
@ -2296,7 +2332,7 @@ if __name__ == '__main__':
('list', ('/tmp', 'imaplib2_test*')), ('list', ('/tmp', 'imaplib2_test*')),
('select', ('/tmp/imaplib2_test.2',)), ('select', ('/tmp/imaplib2_test.2',)),
('search', (None, 'SUBJECT', 'IMAP4 test')), ('search', (None, 'SUBJECT', 'IMAP4 test')),
('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')), ('fetch', ("'1:*'", '(FLAGS INTERNALDATE RFC822)')),
('store', ('1', 'FLAGS', '(\Deleted)')), ('store', ('1', 'FLAGS', '(\Deleted)')),
('namespace', ()), ('namespace', ()),
('expunge', ()), ('expunge', ()),
@ -2380,6 +2416,11 @@ if __name__ == '__main__':
else: path = ml.split()[-1] else: path = ml.split()[-1]
run('delete', (path,)) run('delete', (path,))
if 'ID' in M.capabilities:
run('id', ())
run('id', ('("name", "imaplib2")',))
run('id', ("version", __version__, "os", os.uname()[0]))
for cmd,args in test_seq2: for cmd,args in test_seq2:
if (cmd,args) != ('uid', ('SEARCH', 'SUBJECT', 'IMAP4 test')): if (cmd,args) != ('uid', ('SEARCH', 'SUBJECT', 'IMAP4 test')):
run(cmd, args) run(cmd, args)

View File

@ -127,162 +127,16 @@ def new_mesg(self, s, tn=None, secs=None):
tm = time.strftime('%M:%S', time.localtime(secs)) tm = time.strftime('%M:%S', time.localtime(secs))
getglobalui().debug('imap', ' %s.%02d %s %s' % (tm, (secs*100)%100, tn, s)) getglobalui().debug('imap', ' %s.%02d %s %s' % (tm, (secs*100)%100, tn, s))
class WrappedIMAP4_SSL(UsefulIMAPMixIn, IMAP4_SSL): class WrappedIMAP4_SSL(UsefulIMAPMixIn, IMAP4_SSL):
"""Provides an improved version of the standard IMAP4_SSL """Improved version of imaplib.IMAP4_SSL overriding select()"""
pass
It provides a better readline() implementation as impaplib's
readline() is extremly inefficient. It can also connect to IPv6
addresses."""
def __init__(self, *args, **kwargs):
self._readbuf = ''
self._cacertfile = kwargs.get('cacertfile', None)
if kwargs.has_key('cacertfile'):
del kwargs['cacertfile']
IMAP4_SSL.__init__(self, *args, **kwargs)
def open(self, host=None, port=None):
"""Do whatever IMAP4_SSL would do in open, but call sslwrap
with cert verification"""
#IMAP4_SSL.open(self, host, port) uses the below 2 lines:
self.host = host
self.port = port
#rather than just self.sock = socket.create_connection((host, port))
#we use the below part to be able to connect to ipv6 addresses too
#This connects to the first ip found ipv4/ipv6
#Added by Adriaan Peeters <apeeters@lashout.net> based on a socket
#example from the python documentation:
#http://www.python.org/doc/lib/socket-example.html
res = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
socket.SOCK_STREAM)
# Try all the addresses in turn until we connect()
last_error = 0
for remote in res:
af, socktype, proto, canonname, sa = remote
self.sock = socket.socket(af, socktype, proto)
last_error = self.sock.connect_ex(sa)
if last_error == 0:
break
else:
self.sock.close()
if last_error != 0:
raise Exception("can't open socket; error: %s"\
% socket.error(last_error))
# Allow sending of keep-alive message seems to prevent some servers
# from closing SSL on us leading to deadlocks
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
#connected to socket, now wrap it in SSL
try:
if self._cacertfile:
requirecert = ssl.CERT_REQUIRED
else:
requirecert = ssl.CERT_NONE
self.sslobj = ssl.wrap_socket(self.sock, self.keyfile,
self.certfile,
ca_certs = self._cacertfile,
cert_reqs = requirecert)
except NameError:
#Python 2.4/2.5 don't have the ssl module, we need to
#socket.ssl() here but that doesn't allow cert
#verification!!!
if self._cacertfile:
#user configured a CA certificate, but python 2.4/5 doesn't
#allow us to easily check it. So bail out here.
raise Exception("SSL CA Certificates cannot be checked with python <=2.6. Abort")
self.sslobj = socket.ssl(self.sock, self.keyfile,
self.certfile)
else:
#ssl.wrap_socket worked and cert is verified (if configured),
#now check that hostnames also match if we have a CA cert.
if self._cacertfile:
error = self._verifycert(self.sslobj.getpeercert(), host)
if error:
raise ssl.SSLError("SSL Certificate host name mismatch: %s" % error)
# imaplib2 uses this to poll()
self.read_fd = self.sock.fileno()
#TODO: Done for now. We should implement a mutt-like behavior
#that offers the users to accept a certificate (presenting a
#fingerprint of it) (get via self.sslobj.getpeercert()), and
#save that, and compare on future connects, rather than having
#to trust what the CA certs say.
def _verifycert(self, cert, hostname):
'''Verify that cert (in socket.getpeercert() format) matches hostname.
CRLs are not handled.
Returns error message if any problems are found and None on success.
'''
if not cert:
return ('no certificate received')
dnsname = hostname.lower()
certnames = []
# cert expired?
notafter = cert.get('notAfter')
if notafter:
if time.time() >= ssl.cert_time_to_seconds(notafter):
return ('server certificate error: certificate expired %s'
) % notafter
# First read commonName
for s in cert.get('subject', []):
key, value = s[0]
if key == 'commonName':
certnames.append(value.lower())
if len(certnames) == 0:
return ('no commonName found in certificate')
# Then read subjectAltName
for key, value in cert.get('subjectAltName', []):
if key == 'DNS':
certnames.append(value.lower())
# And finally try to match hostname with one of these names
for certname in certnames:
if (certname == dnsname or
'.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1]):
return None
return ('no matching domain name found in certificate')
class WrappedIMAP4(UsefulIMAPMixIn, IMAP4): class WrappedIMAP4(UsefulIMAPMixIn, IMAP4):
"""Improved version of imaplib.IMAP4 that can also connect to IPv6""" """Improved version of imaplib.IMAP4 overriding select()"""
pass
def open(self, host = '', port = IMAP4_PORT):
"""Setup connection to remote server on "host:port"
(default: localhost:standard IMAP4 port).
"""
#self.host and self.port are needed by the parent IMAP4 class
self.host = host
self.port = port
res = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
socket.SOCK_STREAM)
# Try each address returned by getaddrinfo in turn until we
# manage to connect to one.
# Try all the addresses in turn until we connect()
last_error = 0
for remote in res:
af, socktype, proto, canonname, sa = remote
self.sock = socket.socket(af, socktype, proto)
last_error = self.sock.connect_ex(sa)
if last_error == 0:
break
else:
self.sock.close()
if last_error != 0:
raise Exception("can't open socket; error: %s"\
% socket.error(last_error))
self.file = self.sock.makefile('rb')
# imaplib2 uses this to poll()
self.read_fd = self.sock.fileno()
def Internaldate2epoch(resp): def Internaldate2epoch(resp):
"""Convert IMAP4 INTERNALDATE to UT. """Convert IMAP4 INTERNALDATE to UT.

View File

@ -24,10 +24,11 @@ import offlineimap.accounts
import hmac import hmac
import socket import socket
import base64 import base64
import time
from socket import gaierror from socket import gaierror
try: try:
from ssl import SSLError from ssl import SSLError, cert_time_to_seconds
except ImportError: except ImportError:
# Protect against python<2.6, use dummy and won't get SSL errors. # Protect against python<2.6, use dummy and won't get SSL errors.
SSLError = None SSLError = None
@ -204,9 +205,12 @@ class IMAPServer:
self.ui.connecting(self.hostname, self.port) self.ui.connecting(self.hostname, self.port)
imapobj = imaplibutil.WrappedIMAP4_SSL(self.hostname, imapobj = imaplibutil.WrappedIMAP4_SSL(self.hostname,
self.port, self.port,
self.sslclientkey, self.sslclientcert, self.sslclientkey,
self.sslclientcert,
self.sslcacertfile,
self.verifycert,
timeout=socket.getdefaulttimeout(), timeout=socket.getdefaulttimeout(),
cacertfile = self.sslcacertfile) )
else: else:
self.ui.connecting(self.hostname, self.port) self.ui.connecting(self.hostname, self.port)
imapobj = imaplibutil.WrappedIMAP4(self.hostname, self.port, imapobj = imaplibutil.WrappedIMAP4(self.hostname, self.port,
@ -403,6 +407,47 @@ class IMAPServer:
self.ui.debug('imap', 'keepalive: bottom of loop') self.ui.debug('imap', 'keepalive: bottom of loop')
def verifycert(self, cert, hostname):
'''Verify that cert (in socket.getpeercert() format) matches hostname.
CRLs are not handled.
Returns error message if any problems are found and None on success.
'''
errstr = "CA Cert verifying failed: "
if not cert:
return ('%s no certificate received' % errstr)
dnsname = hostname.lower()
certnames = []
# cert expired?
notafter = cert.get('notAfter')
if notafter:
if time.time() >= cert_time_to_seconds(notafter):
return '%s certificate expired %s' % (errstr, notafter)
# First read commonName
for s in cert.get('subject', []):
key, value = s[0]
if key == 'commonName':
certnames.append(value.lower())
if len(certnames) == 0:
return ('%s no commonName found in certificate' % errstr)
# Then read subjectAltName
for key, value in cert.get('subjectAltName', []):
if key == 'DNS':
certnames.append(value.lower())
# And finally try to match hostname with one of these names
for certname in certnames:
if (certname == dnsname or
'.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1]):
return None
return ('%s no matching domain name found in certificate' % errstr)
class IdleThread(object): class IdleThread(object):
def __init__(self, parent, folder=None): def __init__(self, parent, folder=None):
self.parent = parent self.parent = parent