diff --git a/Changelog.draft.rst b/Changelog.draft.rst index fd445ac..87df0cd 100644 --- a/Changelog.draft.rst +++ b/Changelog.draft.rst @@ -20,6 +20,7 @@ Bug Fixes --------- + Pending for the next major release ================================== diff --git a/Changelog.rst b/Changelog.rst index 717e6f8..9e4bf8a 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -12,6 +12,38 @@ ChangeLog releases announces. +OfflineIMAP v6.3.4-rc3 (2011-07-07) +=================================== + +Notes +----- + +Here is a surprising release. :-) + +As expected we have a lot bug fixes in this round (see git log for details), +including a fix for a bug we had for ages (details below) which is a very good +news. + +What makes this cycle so unusual is that I merged a feature to support StartTLS +automatically (thanks Sebastian!). Another very good news. + +We usually don't do much changes so late in a cycle. Now, things are highly +calming down and I hope a lot of people will test this release. Next one could +be the stable! + +New Features +------------ + +* Added StartTLS support, it will automatically be used if the server + supports it. + +Bug Fixes +--------- + +* We protect more robustly against asking for inexistent messages from the + IMAP server, when someone else deletes or moves messages while we sync. + + OfflineIMAP v6.3.4-rc2 (2011-06-15) =================================== diff --git a/offlineimap/__init__.py b/offlineimap/__init__.py index f64f93d..4a0e52c 100644 --- a/offlineimap/__init__.py +++ b/offlineimap/__init__.py @@ -1,22 +1,21 @@ __all__ = ['OfflineImap'] __productname__ = 'OfflineIMAP' -__version__ = "6.3.4-rc2" -__copyright__ = "Copyright (C) 2002 - 2010 John Goerzen" +__version__ = "6.3.4-rc3" +__copyright__ = "Copyright 2002-2011 John Goerzen & contributors" __author__ = "John Goerzen" __author_email__= "john@complete.org" __description__ = "Disconnected Universal IMAP Mail Synchronization/Reader Support" +__license__ = "Licensed under the GNU GPL v2+ (v2 or any later version)" __bigcopyright__ = """%(__productname__)s %(__version__)s -%(__copyright__)s <%(__author_email__)s>""" % locals() - -banner = __bigcopyright__ + """ - -This software comes with ABSOLUTELY NO WARRANTY; see the file -COPYING for details. This is free software, and you are welcome -to distribute it under the conditions laid out in COPYING.""" - +%(__copyright__)s. +%(__license__)s. +""" % locals() __homepage__ = "http://github.com/nicolas33/offlineimap" -__license__ = "Licensed under the GNU GPL v2+ (v2 or any later version)." + + +banner = __bigcopyright__ + from offlineimap.error import OfflineImapError # put this last, so we don't run into circular dependencies using diff --git a/offlineimap/folder/IMAP.py b/offlineimap/folder/IMAP.py index 8851b5b..9297a7b 100644 --- a/offlineimap/folder/IMAP.py +++ b/offlineimap/folder/IMAP.py @@ -23,7 +23,7 @@ import re import time from copy import copy from Base import BaseFolder -from offlineimap import imaputil, imaplibutil +from offlineimap import imaputil, imaplibutil, OfflineImapError class IMAPFolder(BaseFolder): def __init__(self, imapserver, name, visiblename, accountname, repository): @@ -137,7 +137,7 @@ class IMAPFolder(BaseFolder): search_condition += date_search_str if(maxsize != -1): - if(maxage != 1): #There are two conditions - add a space + if(maxage != -1): #There are two conditions - add a space search_condition += " " search_condition += "SMALLER " + self.config.getdefault("Account " + self.accountname, "maxsize", -1) @@ -195,13 +195,24 @@ class IMAPFolder(BaseFolder): def getmessage(self, uid): """Retrieve message with UID from the IMAP server (incl body) - :returns: the message body + :returns: the message body or throws and OfflineImapError + (probably severity MESSAGE) if e.g. no message with + this UID could be found. """ imapobj = self.imapserver.acquireconnection() try: imapobj.select(self.getfullname(), readonly = 1) - res_type, data = imapobj.uid('fetch', '%d' % uid, '(BODY.PEEK[])') - assert res_type == 'OK', "Fetching message with UID '%d' failed" % uid + res_type, data = imapobj.uid('fetch', str(uid), '(BODY.PEEK[])') + if data == [None] or res_type != 'OK': + #IMAP server says bad request or UID does not exist + severity = OfflineImapError.ERROR.MESSAGE + reason = "IMAP server '%s' responded with '%s' to fetching "\ + "message UID '%d'" % (self.getrepository(), res_type, uid) + if data == [None]: + #IMAP server did not find a message with this UID + reason = "IMAP server '%s' does not have a message "\ + "with UID '%s'" % (self.getrepository(), uid) + raise OfflineImapError(reason, severity) # data looks now e.g. [('320 (UID 17061 BODY[] # {2565}','msgbody....')] we only asked for one message, # and that msg is in data[0]. msbody is in [0][1] diff --git a/offlineimap/folder/UIDMaps.py b/offlineimap/folder/UIDMaps.py index 76c5ee4..9233662 100644 --- a/offlineimap/folder/UIDMaps.py +++ b/offlineimap/folder/UIDMaps.py @@ -20,8 +20,11 @@ from threading import * from IMAP import IMAPFolder import os.path -class MappingFolderMixIn: - """Helper class to map between Folder() instances where both side assign a uid +class MappedIMAPFolder(IMAPFolder): + """IMAP class to map between Folder() instances where both side assign a uid + + This Folder is used on the local side, while the remote side should + be an IMAPFolder. Instance variables (self.): r2l: dict mapping message uids: self.r2l[remoteuid]=localuid @@ -29,10 +32,13 @@ class MappingFolderMixIn: #TODO: what is the difference, how are they used? diskr2l: dict mapping message uids: self.r2l[remoteuid]=localuid diskl2r: dict mapping message uids: self.r2l[localuid]=remoteuid""" - def _initmapping(self): + + def __init__(self, *args, **kwargs): + IMAPFolder.__init__(self, *args, **kwargs) self.maplock = Lock() (self.diskr2l, self.diskl2r) = self._loadmaps() - self._mb = self.__class__.__bases__[1] + self._mb = IMAPFolder(*args, **kwargs) + """Representing the local IMAP Folder using local UIDs""" def _getmapfilename(self): return os.path.join(self.repository.getmapdir(), @@ -81,8 +87,8 @@ class MappingFolderMixIn: return [mapping[x] for x in items] def cachemessagelist(self): - self._mb.cachemessagelist(self) - reallist = self._mb.getmessagelist(self) + self._mb.cachemessagelist() + reallist = self._mb.getmessagelist() self.maplock.acquire() try: @@ -137,7 +143,7 @@ class MappingFolderMixIn: cachemessagelist() before calling this function!""" retval = {} - localhash = self._mb.getmessagelist(self) + localhash = self._mb.getmessagelist() self.maplock.acquire() try: for key, value in localhash.items(): @@ -158,7 +164,7 @@ class MappingFolderMixIn: def getmessage(self, uid): """Returns the content of the specified message.""" - return self._mb.getmessage(self, self.r2l[uid]) + return self._mb.getmessage(self.r2l[uid]) def savemessage(self, uid, content, flags, rtime): """Writes a new message, with the specified uid. @@ -185,7 +191,7 @@ class MappingFolderMixIn: self.savemessageflags(uid, flags) return uid - newluid = self._mb.savemessage(self, -1, content, flags, rtime) + newluid = self._mb.savemessage(-1, content, flags, rtime) if newluid < 1: raise ValueError("Backend could not find uid for message") self.maplock.acquire() @@ -197,21 +203,22 @@ class MappingFolderMixIn: self._savemaps(dolock = 0) finally: self.maplock.release() + return uid def getmessageflags(self, uid): - return self._mb.getmessageflags(self, self.r2l[uid]) + return self._mb.getmessageflags(self.r2l[uid]) def getmessagetime(self, uid): return None def savemessageflags(self, uid, flags): - self._mb.savemessageflags(self, self.r2l[uid], flags) + self._mb.savemessageflags(self.r2l[uid], flags) def addmessageflags(self, uid, flags): - self._mb.addmessageflags(self, self.r2l[uid], flags) + self._mb.addmessageflags(self.r2l[uid], flags) def addmessagesflags(self, uidlist, flags): - self._mb.addmessagesflags(self, self._uidlist(self.r2l, uidlist), + self._mb.addmessagesflags(self._uidlist(self.r2l, uidlist), flags) def _mapped_delete(self, uidlist): @@ -232,22 +239,16 @@ class MappingFolderMixIn: self.maplock.release() def deletemessageflags(self, uid, flags): - self._mb.deletemessageflags(self, self.r2l[uid], flags) + self._mb.deletemessageflags(self.r2l[uid], flags) def deletemessagesflags(self, uidlist, flags): - self._mb.deletemessagesflags(self, self._uidlist(self.r2l, uidlist), + self._mb.deletemessagesflags(self._uidlist(self.r2l, uidlist), flags) def deletemessage(self, uid): - self._mb.deletemessage(self, self.r2l[uid]) + self._mb.deletemessage(self.r2l[uid]) self._mapped_delete([uid]) def deletemessages(self, uidlist): - self._mb.deletemessages(self, self._uidlist(self.r2l, uidlist)) + self._mb.deletemessages(self._uidlist(self.r2l, uidlist)) self._mapped_delete(uidlist) - -# Define a class for local part of IMAP. -class MappedIMAPFolder(MappingFolderMixIn, IMAPFolder): - def __init__(self, *args, **kwargs): - IMAPFolder.__init__(self, *args, **kwargs) - self._initmapping() diff --git a/offlineimap/imaplibutil.py b/offlineimap/imaplibutil.py index 20ab336..6492086 100644 --- a/offlineimap/imaplibutil.py +++ b/offlineimap/imaplibutil.py @@ -33,11 +33,9 @@ except ImportError: pass class UsefulIMAPMixIn: - def getstate(self): - return self.state def getselectedfolder(self): - if self.getstate() == 'SELECTED': - return self.selectedfolder + if self.state == 'SELECTED': + return self.mailbox return None def select(self, mailbox='INBOX', readonly=None, force = 0): @@ -58,10 +56,6 @@ class UsefulIMAPMixIn: (mailbox, result) severity = OfflineImapError.ERROR.FOLDER raise OfflineImapError(errstr, severity) - if self.getstate() == 'SELECTED': - self.selectedfolder = mailbox - else: - self.selectedfolder = None return result def _mesg(self, s, tn=None, secs=None): diff --git a/offlineimap/imapserver.py b/offlineimap/imapserver.py index e7892cc..a235fd5 100644 --- a/offlineimap/imapserver.py +++ b/offlineimap/imapserver.py @@ -24,7 +24,6 @@ import offlineimap.accounts import hmac import socket import base64 -import errno from socket import gaierror try: @@ -32,6 +31,7 @@ try: except ImportError: # Protect against python<2.6, use dummy and won't get SSL errors. SSLError = None + try: # do we have a recent pykerberos? have_gss = False @@ -219,6 +219,7 @@ class IMAPServer: try: # Try GSSAPI and continue if it fails if 'AUTH=GSSAPI' in imapobj.capabilities and have_gss: + self.connectionlock.acquire() self.ui.debug('imap', 'Attempting GSSAPI authentication') try: @@ -229,15 +230,26 @@ class IMAPServer: 'GSSAPI Authentication failed') else: self.gssapi = True + kerberos.authGSSClientClean(self.gss_vc) + self.gss_vc = None + self.gss_step = self.GSS_STATE_STEP #if we do self.password = None then the next attempt cannot try... #self.password = None + self.connectionlock.release() if not self.gssapi: + if 'STARTTLS' in imapobj.capabilities and not\ + self.usessl: + self.ui.debug('imap', + 'Using STARTTLS connection') + imapobj.starttls() + if 'AUTH=CRAM-MD5' in imapobj.capabilities: self.ui.debug('imap', - 'Attempting CRAM-MD5 authentication') + 'Attempting CRAM-MD5 authentication') try: - imapobj.authenticate('CRAM-MD5', self.md5handler) + imapobj.authenticate('CRAM-MD5', + self.md5handler) except imapobj.error, val: self.plainauth(imapobj) else: @@ -285,9 +297,8 @@ class IMAPServer: if(self.connectionlock.locked()): self.connectionlock.release() - # now, check for known errors and throw OfflineImapErrors severity = OfflineImapError.ERROR.REPO - if isinstance(e, gaierror): + if type(e) == gaierror: #DNS related errors. Abort Repo sync #TODO: special error msg for e.errno == 2 "Name or service not known"? reason = "Could not resolve name '%s' for repository "\ @@ -296,7 +307,7 @@ class IMAPServer: (self.hostname, self.reposname) raise OfflineImapError(reason, severity) - elif isinstance(e, SSLError) and e.errno == 1: + elif SSLError and isinstance(e, SSLError) and e.errno == 1: # SSL unknown protocol error # happens e.g. when connecting via SSL to a non-SSL service if self.port != 443: @@ -322,8 +333,7 @@ class IMAPServer: if str(e)[:24] == "can't open socket; error": raise OfflineImapError("Could not connect to remote server '%s' "\ "for repository '%s'. Remote does not answer." - % (self.hostname, self.reposname), - OfflineImapError.ERROR.REPO) + % (self.hostname, self.reposname), severity) else: # re-raise all other errors raise diff --git a/offlineimap/imaputil.py b/offlineimap/imaputil.py index 101f9a6..3905851 100644 --- a/offlineimap/imaputil.py +++ b/offlineimap/imaputil.py @@ -143,7 +143,6 @@ def imapsplit(imapstring): elif splitslen == 0: # There was not even an unquoted word. break - debug("imapsplit() returning:", retval) return retval flagmap = [('\\Seen', 'S'), diff --git a/offlineimap/init.py b/offlineimap/init.py index 7258365..93b7224 100644 --- a/offlineimap/init.py +++ b/offlineimap/init.py @@ -343,6 +343,7 @@ class OfflineImap: t.start() threadutil.exitnotifymonitorloop(threadutil.threadexited) + ui.terminate() except KeyboardInterrupt: ui.terminate(1, errormsg = 'CTRL-C pressed, aborting...') return diff --git a/offlineimap/repository/Maildir.py b/offlineimap/repository/Maildir.py index 86716e1..069748a 100644 --- a/offlineimap/repository/Maildir.py +++ b/offlineimap/repository/Maildir.py @@ -100,7 +100,7 @@ class MaildirRepository(BaseRepository): except OSError, e: if e.errno == 17 and os.path.isdir(full_path): self.debug("makefolder: '%s' already has subdir %s" % - (foldername, sudir)) + (foldername, subdir)) else: raise # Invalidate the folder cache diff --git a/offlineimap/ui/UIBase.py b/offlineimap/ui/UIBase.py index 4c60b2a..5524579 100644 --- a/offlineimap/ui/UIBase.py +++ b/offlineimap/ui/UIBase.py @@ -21,7 +21,6 @@ import time import sys import traceback import threading -from StringIO import StringIO import offlineimap debugtypes = {'':'Other offlineimap related sync messages', @@ -309,10 +308,8 @@ class UIBase: s.terminate(100) def getMainExceptionString(s): - sbuf = StringIO() - traceback.print_exc(file = sbuf) - return "Main program terminated with exception:\n" + \ - sbuf.getvalue() + "\n" + \ + return "Main program terminated with exception:\n%s\n" %\ + traceback.format_exc() + \ s.getThreadDebugLog(threading.currentThread()) def mainException(s):