Merge branch 'ss/imaplib2-2.28' into next
Conflicts: Changelog.draft.rst
This commit is contained in:
commit
2749374a93
@ -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
|
||||||
---------
|
---------
|
||||||
|
@ -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)
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user