diff --git a/offlineimap/imaplib2.py b/offlineimap/imaplib2.py index 6d2805a..eddc3c4 100644 --- a/offlineimap/imaplib2.py +++ b/offlineimap/imaplib2.py @@ -17,9 +17,9 @@ Public functions: Internaldate2Time __all__ = ("IMAP4", "IMAP4_SSL", "IMAP4_stream", "Internaldate2Time", "ParseFlags", "Time2Internaldate") -__version__ = "2.37" +__version__ = "2.41" __release__ = "2" -__revision__ = "37" +__revision__ = "41" __credits__ = """ Authentication code contributed by Donn Cave June 1998. String method conversion by ESR, February 2001. @@ -43,12 +43,22 @@ Single quoting introduced with the help of Vladimir Marek July 2013. Fix for gmail "read 0" error provided by Jim Greenleaf August 2013. Fix for offlineimap "indexerror: string index out of range" bug provided by Eygene Ryabinkin August 2013. -Fix for missing idle_lock in _handler() provided by Franklin Brook August 2014""" +Fix for missing idle_lock in _handler() provided by Franklin Brook August 2014. +Conversion to Python3 provided by F. Malina February 2015. +Fix for READ-ONLY error from multiple EXAMINE/SELECT calls for same mailbox by March 2015.""" __author__ = "Piers Lauder " __URL__ = "http://imaplib2.sourceforge.net" __license__ = "Python License" -import binascii, errno, os, Queue, random, re, select, socket, sys, time, threading, traceback, zlib +import binascii, errno, os, random, re, select, socket, sys, time, threading, zlib + +try: + import queue # py3 + string_types = str +except ImportError: + import Queue as queue # py2 + string_types = basestring + select_module = select @@ -160,13 +170,9 @@ class Request(object): self.data = None - def abort_tb(self, typ, val, tb): - self.aborted = (typ, val, tb) - self.deliver(None) - - def abort(self, typ, val): - self.abort_tb(typ, val, traceback.extract_stack()) + self.aborted = (typ, val) + self.deliver(None) def get_response(self, exc_fmt=None): @@ -175,10 +181,10 @@ class Request(object): self.ready.wait() if self.aborted is not None: - typ, val, tb = self.aborted + typ, val = self.aborted if exc_fmt is None: exc_fmt = '%s - %%s' % typ - raise typ, typ(exc_fmt % str(val)), tb + raise typ(exc_fmt % str(val)) return self.response @@ -256,7 +262,7 @@ class IMAP4(object): 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') + with a type that doesn't evaluate to 'string_types' (eg: 'bytearray') and it will be converted to a string without quoting. There is one instance variable, 'state', that is useful for tracking @@ -301,7 +307,6 @@ class IMAP4(object): self.tagged_commands = {} # Tagged commands awaiting response self.untagged_responses = [] # [[typ: [data, ...]], ...] self.mailbox = None # Current mailbox selected - self.mailboxes = {} # Untagged responses state per mailbox self.is_readonly = False # READ-ONLY desired state self.idle_rqb = None # Server IDLE Request - see _IdleCont self.idle_timeout = None # Must prod server occasionally @@ -357,8 +362,8 @@ class IMAP4(object): self.commands_lock = threading.Lock() self.idle_lock = threading.Lock() - self.ouq = Queue.Queue(10) - self.inq = Queue.Queue() + self.ouq = queue.Queue(10) + self.inq = queue.Queue() self.wrth = threading.Thread(target=self._writer) self.wrth.setDaemon(True) @@ -435,19 +440,19 @@ class IMAP4(object): af, socktype, proto, canonname, sa = res try: s = socket.socket(af, socktype, proto) - except socket.error, msg: + except socket.error as msg: continue try: for i in (0, 1): try: s.connect(sa) break - except socket.error, msg: + except socket.error as msg: if len(msg.args) < 2 or msg.args[0] != errno.EINTR: raise else: raise socket.error(msg) - except socket.error, msg: + except socket.error as msg: s.close() continue break @@ -527,7 +532,10 @@ class IMAP4(object): data = self.compressor.compress(data) data += self.compressor.flush(zlib.Z_SYNC_FLUSH) - self.sock.sendall(data) + if bytes != str: + self.sock.sendall(bytes(data, 'utf8')) + else: + self.sock.sendall(data) def shutdown(self): @@ -813,7 +821,8 @@ class IMAP4(object): 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) + + return self._simple_command(name, data, **kw) def idle(self, timeout=None, **kw): @@ -981,20 +990,14 @@ class IMAP4(object): def select(self, mailbox='INBOX', readonly=False, **kw): """(typ, [data]) = select(mailbox='INBOX', readonly=False) - Select a mailbox. (Restores any previous untagged responses.) + Select a mailbox. (Flushes all untagged responses.) 'data' is count of messages in mailbox ('EXISTS' response). Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so other responses should be obtained via "response('FLAGS')" etc.""" - self.commands_lock.acquire() - # Save state of old mailbox, restore state for new... - self.mailboxes[self.mailbox] = self.untagged_responses - self.untagged_responses = self.mailboxes.setdefault(mailbox, []) - self.commands_lock.release() - self.mailbox = mailbox - self.is_readonly = readonly and True or False + self.is_readonly = bool(readonly) if readonly: name = 'EXAMINE' else: @@ -1240,7 +1243,7 @@ class IMAP4(object): # Must quote command args if "atom-specials" present, # and not already quoted. NB: single quotes are removed. - if not isinstance(arg, basestring): + if not isinstance(arg, string_types): return arg if len(arg) >= 2 and (arg[0],arg[-1]) in (('(',')'),('"','"')): return arg @@ -1252,8 +1255,8 @@ class IMAP4(object): def _choose_nonull_or_dflt(self, dflt, *args): - if isinstance(dflt, basestring): - dflttyp = basestring # Allow any string type + if isinstance(dflt, string_types): + dflttyp = string_types # Allow any string type else: dflttyp = type(dflt) for arg in args: @@ -1303,12 +1306,16 @@ class IMAP4(object): self._check_bye() - for typ in ('OK', 'NO', 'BAD'): - self._get_untagged_response(typ) + if name in ('EXAMINE', 'SELECT'): + self.untagged_responses = [] # Flush all untagged responses + else: + for typ in ('OK', 'NO', 'BAD'): + while self._get_untagged_response(typ): + continue - if self._get_untagged_response('READ-ONLY', leave=True) and not self.is_readonly: - self.literal = None - raise self.readonly('mailbox status changed to READ-ONLY') + if self._get_untagged_response('READ-ONLY', leave=True) and not self.is_readonly: + self.literal = None + raise self.readonly('mailbox status changed to READ-ONLY') if self.Terminate: raise self.abort('connection closed') @@ -1323,7 +1330,7 @@ class IMAP4(object): literal = self.literal if literal is not None: self.literal = None - if isinstance(literal, basestring): + if isinstance(literal, string_types): literator = None data = '%s {%s}' % (data, len(literal)) else: @@ -1389,9 +1396,10 @@ class IMAP4(object): return typ, dat - def _command_completer(self, (response, cb_arg, error)): + def _command_completer(self, cb_arg_list): # Called for callback commands + (response, cb_arg, error) = cb_arg_list rqb, kw = cb_arg rqb.callback = kw['callback'] rqb.callback_arg = kw.get('cb_arg') @@ -1665,7 +1673,7 @@ class IMAP4(object): if __debug__: self._log(1, 'starting') - typ, val, tb = self.abort, 'connection terminated', None + typ, val = self.abort, 'connection terminated' while not self.Terminate: @@ -1683,12 +1691,11 @@ class IMAP4(object): try: line = self.inq.get(True, timeout) - except Queue.Empty: + except queue.Empty: if self.idle_rqb is None: if resp_timeout is not None and self.tagged_commands: if __debug__: self._log(1, 'response timeout') typ, val = self.abort, 'no response after %s secs' % resp_timeout - tb = traceback.extract_stack() break continue if self.idle_timeout > time.time(): @@ -1700,41 +1707,33 @@ class IMAP4(object): if __debug__: self._log(1, 'inq None - terminating') break - # self.inq can contain tuple, it means we got an exception. - # For example of code that produces tuple see IMAP4._reader() - # and IMAP4._writer(). - # - # XXX: may be it will be more explicit to create own exception - # XXX: class and pass it instead of tuple. - if not isinstance(line, basestring): - typ, val, tb = line + if not isinstance(line, string_types): + typ, val = line break try: self._put_response(line) except: typ, val = self.error, 'program error: %s - %s' % sys.exc_info()[:2] - tb = sys.exc_info()[2] break self.Terminate = True - if not tb: - tb = traceback.extract_stack() - if __debug__: self._log(1, 'terminating: %s' % repr(val)) while not self.ouq.empty(): try: - self.ouq.get_nowait().abort_tb(typ, val, tb) - except Queue.Empty: + qel = self.ouq.get_nowait() + if qel is not None: + qel.abort(typ, val) + except queue.Empty: break self.ouq.put(None) self.commands_lock.acquire() - for name in self.tagged_commands.keys(): + for name in list(self.tagged_commands.keys()): rqb = self.tagged_commands.pop(name) - rqb.abort_tb(typ, val, tb) + rqb.abort(typ, val) self.state_change_free.set() self.commands_lock.release() if __debug__: self._log(3, 'state_change_free.set') @@ -1795,13 +1794,22 @@ class IMAP4(object): rxzero = 0 while True: - stop = data.find('\n', start) - if stop < 0: - line_part += data[start:] - break - stop += 1 - line_part, start, line = \ - '', stop, line_part + data[start:stop] + if bytes != str: + stop = data.find(b'\n', start) + if stop < 0: + line_part += data[start:].decode() + break + stop += 1 + line_part, start, line = \ + '', stop, line_part + data[start:stop].decode() + else: + stop = data.find('\n', start) + if stop < 0: + line_part += data[start:] + break + stop += 1 + line_part, start, line = \ + '', stop, line_part + data[start:stop] if __debug__: self._log(4, '< %s' % line) self.inq.put(line) if self.TerminateReader: @@ -1816,7 +1824,7 @@ class IMAP4(object): self._print_log() if self.debug: self.debug += 4 # Output all self._log(1, reason) - self.inq.put((self.abort, reason, sys.exc_info()[2])) + self.inq.put((self.abort, reason)) break poll.unregister(self.read_fd) @@ -1862,13 +1870,22 @@ class IMAP4(object): rxzero = 0 while True: - stop = data.find('\n', start) - if stop < 0: - line_part += data[start:] - break - stop += 1 - line_part, start, line = \ - '', stop, line_part + data[start:stop] + if bytes != str: + stop = data.find(b'\n', start) + if stop < 0: + line_part += data[start:].decode() + break + stop += 1 + line_part, start, line = \ + '', stop, line_part + data[start:stop].decode() + else: + stop = data.find('\n', start) + if stop < 0: + line_part += data[start:] + break + stop += 1 + line_part, start, line = \ + '', stop, line_part + data[start:stop] if __debug__: self._log(4, '< %s' % line) self.inq.put(line) if self.TerminateReader: @@ -1880,7 +1897,7 @@ class IMAP4(object): self._print_log() if self.debug: self.debug += 4 # Output all self._log(1, reason) - self.inq.put((self.abort, reason, sys.exc_info()[2])) + self.inq.put((self.abort, reason)) break if __debug__: self._log(1, 'finished') @@ -1893,7 +1910,6 @@ class IMAP4(object): if __debug__: self._log(1, 'starting') reason = 'Terminated' - tb = None while not self.Terminate: rqb = self.ouq.get() @@ -1905,19 +1921,15 @@ class IMAP4(object): if __debug__: self._log(4, '> %s' % rqb.data) except: reason = 'socket error: %s - %s' % sys.exc_info()[:2] - tb = sys.exc_info()[2] if __debug__: if not self.Terminate: self._print_log() if self.debug: self.debug += 4 # Output all self._log(1, reason) - rqb.abort_tb(self.abort, reason, tb) + rqb.abort(self.abort, reason) break - if not tb: - tb = traceback.extract_stack() - - self.inq.put((self.abort, reason, tb)) + self.inq.put((self.abort, reason)) if __debug__: self._log(1, 'finished') @@ -1952,7 +1964,7 @@ class IMAP4(object): return t = '\n\t\t' - l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l) + l = ['%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or '') for x in l] self.debug_lock.acquire() self._mesg('untagged responses dump:%s%s' % (t, t.join(l))) self.debug_lock.release() @@ -2082,16 +2094,28 @@ class IMAP4_SSL(IMAP4): data = self.compressor.compress(data) data += self.compressor.flush(zlib.Z_SYNC_FLUSH) - if hasattr(self.sock, "sendall"): - self.sock.sendall(data) + if bytes != str: + if hasattr(self.sock, "sendall"): + self.sock.sendall(bytes(data, 'utf8')) + else: + dlen = len(data) + while dlen > 0: + sent = self.sock.write(bytes(data, 'utf8')) + if sent == dlen: + break # avoid copy + data = data[sent:] + dlen = dlen - sent else: - bytes = len(data) - while bytes > 0: - sent = self.sock.write(data) - if sent == bytes: - break # avoid copy - data = data[sent:] - bytes = bytes - sent + if hasattr(self.sock, "sendall"): + self.sock.sendall(data) + else: + dlen = len(data) + while dlen > 0: + sent = self.sock.write(data) + if sent == dlen: + break # avoid copy + data = data[sent:] + dlen = dlen - sent def ssl(self): @@ -2165,7 +2189,10 @@ class IMAP4_stream(IMAP4): data = self.compressor.compress(data) data += self.compressor.flush(zlib.Z_SYNC_FLUSH) - self.writefile.write(data) + if bytes != str: + self.writefile.write(bytes(data, 'utf8')) + else: + self.writefile.write(data) self.writefile.flush() @@ -2243,7 +2270,7 @@ class _IdleCont(object): MonthNames = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] -Mon2num = dict(zip((x.encode() for x in MonthNames[1:]), range(1, 13))) +Mon2num = dict(list(zip((x.encode() for x in MonthNames[1:]), list(range(1, 13))))) InternalDate = re.compile(r'.*INTERNALDATE "' r'(?P[ 0123][0-9])-(?P[A-Z][a-z][a-z])-(?P[0-9][0-9][0-9][0-9])' @@ -2347,7 +2374,7 @@ if __name__ == '__main__': try: optlist, args = getopt.getopt(sys.argv[1:], 'd:il:s:p:') - except getopt.error, val: + except getopt.error as val: optlist, args = (), () debug, debug_buf_lvl, port, stream_command, keyfile, certfile, idle_intr = (None,)*7 @@ -2380,13 +2407,14 @@ if __name__ == '__main__': % {'user':USER, 'lf':'\n', 'data':data} test_seq1 = [ + ('list', ('""', '""')), ('list', ('""', '%')), - ('create', ('/tmp/imaplib2_test.0',)), - ('rename', ('/tmp/imaplib2_test.0', '/tmp/imaplib2_test.1')), - ('CREATE', ('/tmp/imaplib2_test.2',)), - ('append', ('/tmp/imaplib2_test.2', None, None, test_mesg)), - ('list', ('/tmp', 'imaplib2_test*')), - ('select', ('/tmp/imaplib2_test.2',)), + ('create', ('imaplib2_test0',)), + ('rename', ('imaplib2_test0', 'imaplib2_test1')), + ('CREATE', ('imaplib2_test2',)), + ('append', ('imaplib2_test2', None, None, test_mesg)), + ('list', ('', 'imaplib2_test%')), + ('select', ('imaplib2_test2',)), ('search', (None, 'SUBJECT', 'IMAP4 test')), ('fetch', ("'1:*'", '(FLAGS INTERNALDATE RFC822)')), ('store', ('1', 'FLAGS', '(\Deleted)')), @@ -2405,12 +2433,17 @@ if __name__ == '__main__': ('uid', ('SEARCH', 'ALL')), ('uid', ('THREAD', 'references', 'UTF-8', '(SEEN)')), ('recent', ()), + ('examine', ()), + ('select', ()), + ('examine', ()), + ('select', ()), ) AsyncError = None - def responder((response, cb_arg, error)): + def responder(cb_arg_list): + (response, cb_arg, error) = cb_arg_list global AsyncError cmd, args = cb_arg if error is not None: @@ -2467,7 +2500,7 @@ if __name__ == '__main__': for cmd,args in test_seq1: run(cmd, args) - for ml in run('list', ('/tmp/', 'imaplib2_test%'), cb=False): + for ml in run('list', ('', 'imaplib2_test%'), cb=False): mo = re.match(r'.*"([^"]+)"$', ml) if mo: path = mo.group(1) else: path = ml.split()[-1] @@ -2475,7 +2508,7 @@ if __name__ == '__main__': if 'ID' in M.capabilities: run('id', ()) - run('id', ('("name", "imaplib2")',)) + run('id', ("(name imaplib2)",)) run('id', ("version", __version__, "os", os.uname()[0])) for cmd,args in test_seq2: @@ -2527,16 +2560,16 @@ if __name__ == '__main__': M._mesg('unused untagged responses in order, most recent last:') for typ,dat in M.pop_untagged_responses(): M._mesg('\t%s %s' % (typ, dat)) - print 'All tests OK.' + print('All tests OK.') except: if not idle_intr or not 'IDLE' in M.capabilities: - print 'Tests failed.' + print('Tests failed.') if not debug: - print ''' + print(''' If you would like to see debugging output, try: %s -d5 -''' % sys.argv[0] +''' % sys.argv[0]) raise