summaryrefslogtreecommitdiffstats
path: root/Lib/imaplib.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/imaplib.py')
-rw-r--r--Lib/imaplib.py277
1 files changed, 134 insertions, 143 deletions
diff --git a/Lib/imaplib.py b/Lib/imaplib.py
index c576927..c0334d8 100644
--- a/Lib/imaplib.py
+++ b/Lib/imaplib.py
@@ -22,14 +22,20 @@ Public functions: Internaldate2tuple
__version__ = "2.58"
-import binascii, errno, random, re, socket, subprocess, sys, time
+import binascii, errno, random, re, socket, subprocess, sys, time, calendar
+
+try:
+ import ssl
+ HAVE_SSL = True
+except ImportError:
+ HAVE_SSL = False
__all__ = ["IMAP4", "IMAP4_stream", "Internaldate2tuple",
"Int2AP", "ParseFlags", "Time2Internaldate"]
# Globals
-CRLF = '\r\n'
+CRLF = b'\r\n'
Debug = 0
IMAP4_PORT = 143
IMAP4_SSL_PORT = 993
@@ -71,6 +77,7 @@ Commands = {
'SETANNOTATION':('AUTH', 'SELECTED'),
'SETQUOTA': ('AUTH', 'SELECTED'),
'SORT': ('SELECTED',),
+ 'STARTTLS': ('NONAUTH',),
'STATUS': ('AUTH', 'SELECTED'),
'STORE': ('SELECTED',),
'SUBSCRIBE': ('AUTH', 'SELECTED'),
@@ -81,18 +88,19 @@ Commands = {
# Patterns to match server responses
-Continuation = re.compile(r'\+( (?P<data>.*))?')
-Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
-InternalDate = re.compile(r'.*INTERNALDATE "'
- r'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
- r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
- r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
- r'"')
-Literal = re.compile(r'.*{(?P<size>\d+)}$')
-MapCRLF = re.compile(r'\r\n|\r|\n')
-Response_code = 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>.*))?')
+Continuation = re.compile(br'\+( (?P<data>.*))?')
+Flags = re.compile(br'.*FLAGS \((?P<flags>[^\)]*)\)')
+InternalDate = re.compile(br'.*INTERNALDATE "'
+ br'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
+ br' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
+ br' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
+ br'"')
+Literal = re.compile(br'.*{(?P<size>\d+)}$', re.ASCII)
+MapCRLF = re.compile(br'\r\n|\r|\n')
+Response_code = re.compile(br'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
+Untagged_response = re.compile(br'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
+Untagged_status = re.compile(
+ br'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?', re.ASCII)
@@ -146,8 +154,6 @@ class IMAP4:
class abort(error): pass # Service errors - close and retry
class readonly(abort): pass # Mailbox status changed to READ-ONLY
- mustquote = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]")
-
def __init__(self, host = '', port = IMAP4_PORT):
self.debug = Debug
self.state = 'LOGOUT'
@@ -157,18 +163,30 @@ class IMAP4:
self.continuation_response = '' # Last continuation response
self.is_readonly = False # READ-ONLY desired state
self.tagnum = 0
+ self._tls_established = False
# Open socket to server.
self.open(host, port)
+ try:
+ self._connect()
+ except Exception:
+ try:
+ self.shutdown()
+ except socket.error:
+ pass
+ raise
+
+
+ def _connect(self):
# Create unique tag for this session,
# and compile tagged response matcher.
self.tagpre = Int2AP(random.randint(4096, 65535))
- self.tagre = re.compile(r'(?P<tag>'
+ self.tagre = re.compile(br'(?P<tag>'
+ self.tagpre
- + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
+ + br'\d+) (?P<type>[A-Z]+) (?P<data>.*)', re.ASCII)
# Get server welcome message,
# request and store CAPABILITY response.
@@ -189,11 +207,7 @@ class IMAP4:
else:
raise self.error(self.welcome)
- typ, dat = self.capability()
- if dat == [None]:
- raise self.error('no CAPABILITY response from server')
- self.capabilities = tuple(dat[-1].upper().split())
-
+ self._get_capabilities()
if __debug__:
if self.debug >= 3:
self._mesg('CAPABILITIES: %r' % (self.capabilities,))
@@ -218,6 +232,9 @@ class IMAP4:
# Overridable methods
+ def _create_socket(self):
+ return socket.create_connection((self.host, self.port))
+
def open(self, host = '', port = IMAP4_PORT):
"""Setup connection to remote server on "host:port"
(default: localhost:standard IMAP4 port).
@@ -226,13 +243,21 @@ class IMAP4:
"""
self.host = host
self.port = port
- self.sock = socket.create_connection((host, port))
+ self.sock = self._create_socket()
self.file = self.sock.makefile('rb')
def read(self, size):
"""Read 'size' bytes from remote."""
- return self.file.read(size)
+ chunks = []
+ read = 0
+ while read < size:
+ data = self.file.read(min(size-read, 4096))
+ if not data:
+ break
+ read += len(data)
+ chunks.append(data)
+ return b''.join(chunks)
def readline(self):
@@ -699,6 +724,30 @@ class IMAP4:
return self._untagged_response(typ, dat, name)
+ def starttls(self, ssl_context=None):
+ name = 'STARTTLS'
+ if not HAVE_SSL:
+ raise self.error('SSL support missing')
+ if self._tls_established:
+ raise self.abort('TLS session already established')
+ if name not in self.capabilities:
+ raise self.abort('TLS not supported by server')
+ # Generate a default SSL context if none was passed.
+ if ssl_context is None:
+ ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
+ # SSLv2 considered harmful.
+ ssl_context.options |= ssl.OP_NO_SSLv2
+ typ, dat = self._simple_command(name)
+ if typ == 'OK':
+ self.sock = ssl_context.wrap_socket(self.sock)
+ self.file = self.sock.makefile('rb')
+ self._tls_established = True
+ self._get_capabilities()
+ else:
+ raise self.error("Couldn't establish TLS session")
+ return self._untagged_response(typ, dat, name)
+
+
def status(self, mailbox, names):
"""Request named status conditions for mailbox.
@@ -796,12 +845,12 @@ class IMAP4:
def _append_untagged(self, typ, dat):
-
- if dat is None: dat = ''
+ if dat is None:
+ dat = b''
ur = self.untagged_responses
if __debug__:
if self.debug >= 5:
- self._mesg('untagged_responses[%s] %s += ["%s"]' %
+ self._mesg('untagged_responses[%s] %s += ["%r"]' %
(typ, len(ur.get(typ,'')), dat))
if typ in ur:
ur[typ].append(dat)
@@ -812,7 +861,7 @@ class IMAP4:
def _check_bye(self):
bye = self.untagged_responses.get('BYE')
if bye:
- raise self.abort(bye[-1])
+ raise self.abort(bye[-1].decode('ascii', 'replace'))
def _command(self, name, *args):
@@ -833,10 +882,13 @@ class IMAP4:
raise self.readonly('mailbox status changed to READ-ONLY')
tag = self._new_tag()
- data = '%s %s' % (tag, name)
+ name = bytes(name, 'ASCII')
+ data = tag + b' ' + name
for arg in args:
if arg is None: continue
- data = '%s %s' % (data, self._checkquote(arg))
+ if isinstance(arg, str):
+ arg = bytes(arg, "ASCII")
+ data = data + b' ' + arg
literal = self.literal
if literal is not None:
@@ -845,17 +897,17 @@ class IMAP4:
literator = literal
else:
literator = None
- data = '%s {%s}' % (data, len(literal))
+ data = data + bytes(' {%s}' % len(literal), 'ASCII')
if __debug__:
if self.debug >= 4:
- self._mesg('> %s' % data)
+ self._mesg('> %r' % data)
else:
- self._log('> %s' % data)
+ self._log('> %r' % data)
try:
- self.send('%s%s' % (data, CRLF))
- except (socket.error, OSError), val:
+ self.send(data + CRLF)
+ except (socket.error, OSError) as val:
raise self.abort('socket error: %s' % val)
if literal is None:
@@ -880,7 +932,7 @@ class IMAP4:
try:
self.send(literal)
self.send(CRLF)
- except (socket.error, OSError), val:
+ except (socket.error, OSError) as val:
raise self.abort('socket error: %s' % val)
if not literator:
@@ -895,9 +947,9 @@ class IMAP4:
self._check_bye()
try:
typ, data = self._get_tagged_response(tag)
- except self.abort, val:
+ except self.abort as val:
raise self.abort('command: %s => %s' % (name, val))
- except self.error, val:
+ except self.error as val:
raise self.error('command: %s => %s' % (name, val))
if name != 'LOGOUT':
self._check_bye()
@@ -906,6 +958,15 @@ class IMAP4:
return typ, data
+ def _get_capabilities(self):
+ typ, dat = self.capability()
+ if dat == [None]:
+ raise self.error('no CAPABILITY response from server')
+ dat = str(dat[-1], "ASCII")
+ dat = dat.upper()
+ self.capabilities = tuple(dat.split())
+
+
def _get_response(self):
# Read response and store.
@@ -923,6 +984,7 @@ class IMAP4:
raise self.abort('unexpected tagged response: %s' % resp)
typ = self.mo.group('type')
+ typ = str(typ, 'ASCII')
dat = self.mo.group('data')
self.tagged_commands[tag] = (typ, [dat])
else:
@@ -944,9 +1006,10 @@ class IMAP4:
raise self.abort("unexpected response: '%s'" % resp)
typ = self.mo.group('type')
+ typ = str(typ, 'ascii')
dat = self.mo.group('data')
- if dat is None: dat = '' # Null untagged response
- if dat2: dat = dat + ' ' + dat2
+ if dat is None: dat = b'' # Null untagged response
+ if dat2: dat = dat + b' ' + dat2
# Is there a literal to come?
@@ -973,11 +1036,13 @@ class IMAP4:
# Bracketed response information?
if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
- self._append_untagged(self.mo.group('type'), self.mo.group('data'))
+ typ = self.mo.group('type')
+ typ = str(typ, "ASCII")
+ self._append_untagged(typ, self.mo.group('data'))
if __debug__:
if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
- self._mesg('%s response: %s' % (typ, dat))
+ self._mesg('%s response: %r' % (typ, dat))
return resp
@@ -997,7 +1062,7 @@ class IMAP4:
try:
self._get_response()
- except self.abort, val:
+ except self.abort as val:
if __debug__:
if self.debug >= 1:
self.print_log()
@@ -1011,15 +1076,15 @@ class IMAP4:
raise self.abort('socket error: EOF')
# Protocol mandates all lines terminated by CRLF
- if not line.endswith('\r\n'):
+ if not line.endswith(b'\r\n'):
raise self.abort('socket error: unterminated line')
line = line[:-2]
if __debug__:
if self.debug >= 4:
- self._mesg('< %s' % line)
+ self._mesg('< %r' % line)
else:
- self._log('< %s' % line)
+ self._log('< %r' % line)
return line
@@ -1031,38 +1096,24 @@ class IMAP4:
self.mo = cre.match(s)
if __debug__:
if self.mo is not None and self.debug >= 5:
- self._mesg("\tmatched r'%s' => %r" % (cre.pattern, self.mo.groups()))
+ self._mesg("\tmatched r'%r' => %r" % (cre.pattern, self.mo.groups()))
return self.mo is not None
def _new_tag(self):
- tag = '%s%s' % (self.tagpre, self.tagnum)
+ tag = self.tagpre + bytes(str(self.tagnum), 'ASCII')
self.tagnum = self.tagnum + 1
self.tagged_commands[tag] = None
return tag
- def _checkquote(self, arg):
-
- # Must quote command args if non-alphanumeric chars present,
- # and not already quoted.
-
- if type(arg) is not type(''):
- return arg
- if len(arg) >= 2 and (arg[0],arg[-1]) in (('(',')'),('"','"')):
- return arg
- if arg and self.mustquote.search(arg) is None:
- return arg
- return self._quote(arg)
-
-
def _quote(self, arg):
arg = arg.replace('\\', '\\\\')
arg = arg.replace('"', '\\"')
- return '"%s"' % arg
+ return '"' + arg + '"'
def _simple_command(self, name, *args):
@@ -1071,7 +1122,6 @@ class IMAP4:
def _untagged_response(self, typ, dat, name):
-
if typ == 'NO':
return typ, dat
if not name in self.untagged_responses:
@@ -1121,12 +1171,8 @@ class IMAP4:
n -= 1
+if HAVE_SSL:
-try:
- import ssl
-except ImportError:
- pass
-else:
class IMAP4_SSL(IMAP4):
"""IMAP4 client class over SSL connection
@@ -1147,61 +1193,17 @@ else:
self.certfile = certfile
IMAP4.__init__(self, host, port)
+ def _create_socket(self):
+ sock = IMAP4._create_socket(self)
+ return ssl.wrap_socket(sock, self.keyfile, self.certfile)
- def open(self, host = '', port = IMAP4_SSL_PORT):
+ def open(self, host='', port=IMAP4_SSL_PORT):
"""Setup connection to remote server on "host:port".
(default: localhost:standard IMAP4 SSL port).
This connection will be used by the routines:
read, readline, send, shutdown.
"""
- self.host = host
- self.port = port
- self.sock = socket.create_connection((host, port))
- self.sslobj = ssl.wrap_socket(self.sock, self.keyfile, self.certfile)
- self.file = self.sslobj.makefile('rb')
-
-
- def read(self, size):
- """Read 'size' bytes from remote."""
- return self.file.read(size)
-
-
- def readline(self):
- """Read line from remote."""
- return self.file.readline()
-
-
- def send(self, data):
- """Send data to remote."""
- bytes = len(data)
- while bytes > 0:
- sent = self.sslobj.write(data)
- if sent == bytes:
- break # avoid copy
- data = data[sent:]
- bytes = bytes - sent
-
-
- def shutdown(self):
- """Close I/O established in "open"."""
- self.file.close()
- self.sock.close()
-
-
- def socket(self):
- """Return socket instance used to connect to IMAP4 server.
-
- socket = <instance>.socket()
- """
- return self.sock
-
-
- def ssl(self):
- """Return SSLObject instance used to communicate with the IMAP4 server.
-
- ssl = ssl.wrap_socket(<instance>.socket)
- """
- return self.sslobj
+ IMAP4.open(self, host, port)
__all__.append("IMAP4_SSL")
@@ -1238,7 +1240,6 @@ class IMAP4_stream(IMAP4):
self.writefile = self.process.stdin
self.readfile = self.process.stdout
-
def read(self, size):
"""Read 'size' bytes from remote."""
return self.readfile.read(size)
@@ -1307,14 +1308,14 @@ class _Authenticator:
-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}
+Mon2num = {b'Jan': 1, b'Feb': 2, b'Mar': 3, b'Apr': 4, b'May': 5, b'Jun': 6,
+ b'Jul': 7, b'Aug': 8, b'Sep': 9, b'Oct': 10, b'Nov': 11, b'Dec': 12}
def Internaldate2tuple(resp):
"""Parse an IMAP4 INTERNALDATE string.
Return corresponding local time. The return value is a
- time.struct_time instance or None if the string has wrong format.
+ time.struct_time tuple or None if the string has wrong format.
"""
mo = InternalDate.match(resp)
@@ -1335,23 +1336,13 @@ def Internaldate2tuple(resp):
# INTERNALDATE timezone must be subtracted to get UT
zone = (zoneh*60 + zonem)*60
- if zonen == '-':
+ if zonen == b'-':
zone = -zone
tt = (year, mon, day, hour, min, sec, -1, -1, -1)
+ utc = calendar.timegm(tt) - zone
- utc = time.mktime(tt)
-
- # Following is necessary because the time module has no 'mkgmtime'.
- # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
-
- lt = time.localtime(utc)
- if time.daylight and lt[-1]:
- zone = zone + time.altzone
- else:
- zone = zone + time.timezone
-
- return time.localtime(utc - zone)
+ return time.localtime(utc)
@@ -1359,11 +1350,11 @@ def Int2AP(num):
"""Convert integer to A-P string representation."""
- val = ''; AP = 'ABCDEFGHIJKLMNOP'
+ val = b''; AP = b'ABCDEFGHIJKLMNOP'
num = int(abs(num))
while num:
num, mod = divmod(num, 16)
- val = AP[mod] + val
+ val = AP[mod:mod+1] + val
return val
@@ -1421,7 +1412,7 @@ if __name__ == '__main__':
try:
optlist, args = getopt.getopt(sys.argv[1:], 'd:s:')
- except getopt.error, val:
+ except getopt.error as val:
optlist, args = (), ()
stream_command = None
@@ -1504,15 +1495,15 @@ if __name__ == '__main__':
run('uid', ('FETCH', '%s' % uid[-1],
'(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
- print '\nAll tests OK.'
+ print('\nAll tests OK.')
except:
- print '\nTests failed.'
+ print('\nTests failed.')
if not Debug:
- print '''
+ print('''
If you would like to see debugging output,
try: %s -d5
-''' % sys.argv[0]
+''' % sys.argv[0])
raise