diff options
Diffstat (limited to 'Lib/imaplib.py')
| -rw-r--r-- | Lib/imaplib.py | 277 |
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 |
