From 24db42916c418b336dec6310ce8cae1d9a1ea4cb Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Tue, 13 Sep 2011 16:27:54 +0200 Subject: [PATCH 01/52] IMAP savemessage(): Don't loop indefinitely on failure We were retrying indefinitely on imapobj.abort() (as that is what imaplib2 suggests), but if the failure occurs repeatedly, we'll never quit this loop. So implement a counter that errs out after unsuccessful retries. Signed-off-by: Sebastian Spaeth Signed-off-by: Nicolas Sebrecht --- offlineimap/folder/IMAP.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/offlineimap/folder/IMAP.py b/offlineimap/folder/IMAP.py index f3d1ad8..67603dd 100644 --- a/offlineimap/folder/IMAP.py +++ b/offlineimap/folder/IMAP.py @@ -495,11 +495,10 @@ class IMAPFolder(BaseFolder): self.savemessageflags(uid, flags) return uid + retry_left = 2 # succeeded in APPENDING? imapobj = self.imapserver.acquireconnection() try: - success = False # succeeded in APPENDING? - while not success: - + while retry_left: # UIDPLUS extension provides us with an APPENDUID response. use_uidplus = 'UIDPLUS' in imapobj.capabilities @@ -536,12 +535,20 @@ class IMAPFolder(BaseFolder): (typ, dat) = imapobj.append(self.getfullname(), imaputil.flagsmaildir2imap(flags), date, content) - success = True + retry_left = 0 # Mark as success except imapobj.abort, e: # connection has been reset, release connection and retry. self.ui.error(e, exc_info()[2]) self.imapserver.releaseconnection(imapobj, True) imapobj = self.imapserver.acquireconnection() + if not retry_left: + raise OfflineImapError("Saving msg in folder '%s', " + "repository '%s' failed. Server reponded; %s %s\n" + "Message content was: %s" % + (self, self.getrepository(), + typ, dat, dbg_output), + OfflineImapError.ERROR.MESSAGE) + retry_left -= 1 except imapobj.error, e: # If the server responds with 'BAD', append() raise()s directly. # So we need to prepare a response ourselves. From 7c83d505f8707192f81a6e74c272a668eb62c1c7 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Tue, 13 Sep 2011 15:17:55 +0200 Subject: [PATCH 02/52] IMAP cachefolder: Fix returning None on select We rely on the number of mails being returned by the imapobj.select() call, however that only happens if we "force" a real select() to occur. Pass in the force parameter that I dropped earlier (we did not make use of the return value when I dropped it, that is how it slipped through). Signed-off-by: Sebastian Spaeth Signed-off-by: Nicolas Sebrecht --- offlineimap/folder/IMAP.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/offlineimap/folder/IMAP.py b/offlineimap/folder/IMAP.py index 67603dd..4dcae7d 100644 --- a/offlineimap/folder/IMAP.py +++ b/offlineimap/folder/IMAP.py @@ -120,7 +120,7 @@ class IMAPFolder(BaseFolder): imapobj = self.imapserver.acquireconnection() try: - res_type, imapdata = imapobj.select(self.getfullname(), True) + res_type, imapdata = imapobj.select(self.getfullname(), True, True) if imapdata == [None] or imapdata[0] == '0': # Empty folder, no need to populate message list return From fe5714022471ed221fc39a7e0784ece344bfc93e Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Thu, 15 Sep 2011 12:06:06 +0200 Subject: [PATCH 03/52] Fix repository 'readonly' configuration The readonly feature was introduced to safeguard repositories from accidental modifications. Unfortunately, my patch treated the readonly setting as a string and not as a boolean, so if *anything* was set in the configuration file as 'readonly', this value evaluated to True Fortunately this was safe, we never treated a repository that we wanted read-only as read-write. We always treated them readonly if something was configured as "readonly=..." even if that was False. The fix is simply to use getconfboolean() rather than getconf() which checks for True/False/On/Off/yes/no/1/0 and hands back the correct boolean. Reported-by: Tomasz Nowak Signed-off-by: Sebastian Spaeth Signed-off-by: Nicolas Sebrecht --- offlineimap/accounts.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/offlineimap/accounts.py b/offlineimap/accounts.py index 31ea2b7..428cbd8 100644 --- a/offlineimap/accounts.py +++ b/offlineimap/accounts.py @@ -231,7 +231,7 @@ class SyncableAccount(Account): localrepos = self.localrepos statusrepos = self.statusrepos # replicate the folderstructure from REMOTE to LOCAL - if not localrepos.getconf('readonly', False): + if not localrepos.getconfboolean('readonly', False): self.ui.syncfolders(remoterepos, localrepos) remoterepos.syncfoldersto(localrepos, statusrepos) @@ -345,7 +345,7 @@ def syncfolder(accountname, remoterepos, remotefolder, localrepos, remotefolder.getmessagecount()) # Synchronize remote changes. - if not localrepos.getconf('readonly', False): + if not localrepos.getconfboolean('readonly', False): ui.syncingmessages(remoterepos, remotefolder, localrepos, localfolder) remotefolder.syncmessagesto(localfolder, statusfolder) else: @@ -353,7 +353,7 @@ def syncfolder(accountname, remoterepos, remotefolder, localrepos, % localrepos.getname()) # Synchronize local changes - if not remoterepos.getconf('readonly', False): + if not remoterepos.getconfboolean('readonly', False): ui.syncingmessages(localrepos, localfolder, remoterepos, remotefolder) localfolder.syncmessagesto(remotefolder, statusfolder) else: From d2af21d57d3241b1888f8f7d7c2c878e57da7ee3 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Thu, 15 Sep 2011 15:45:09 +0200 Subject: [PATCH 04/52] Simplify testing for 'subscribedonly' setting We only explicitly tested for 'yes' when we have a nice function to get boolean settings which also works with Treu/False/NO, etc... Signed-off-by: Sebastian Spaeth Signed-off-by: Nicolas Sebrecht --- offlineimap/repository/IMAP.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/offlineimap/repository/IMAP.py b/offlineimap/repository/IMAP.py index 76d0870..daa31c9 100644 --- a/offlineimap/repository/IMAP.py +++ b/offlineimap/repository/IMAP.py @@ -280,8 +280,7 @@ class IMAPRepository(BaseRepository): imapobj = self.imapserver.acquireconnection() # check whether to list all folders, or subscribed only listfunction = imapobj.list - if self.config.has_option(self.getsection(), 'subscribedonly'): - if self.getconf('subscribedonly') == "yes": + if self.getconfboolean('subscribedonly', False): listfunction = imapobj.lsub try: listresult = listfunction(directory = self.imapserver.reference)[1] From c1a2b1559de9be285ecb7254ea755d19693cd363 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Thu, 15 Sep 2011 15:06:30 +0200 Subject: [PATCH 05/52] repository: Simplify restore_atime code repository.BaseRepository().restore_atime() was testing in complex ways that it only operates on a Maildir and that the 'restoreatime' setting is set. This is unecessary, we can simply make the base implementation a NoOp, and move the implementation to MaildirRepository(). This will save a tad of work for everyone doing IMAP<->IMAP synchronization and simplify the code. Also document the functions. Signed-off-by: Sebastian Spaeth Signed-off-by: Nicolas Sebrecht --- offlineimap/repository/Base.py | 14 +++++--------- offlineimap/repository/Maildir.py | 10 +++++++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/offlineimap/repository/Base.py b/offlineimap/repository/Base.py index 184bc9a..72a5f88 100644 --- a/offlineimap/repository/Base.py +++ b/offlineimap/repository/Base.py @@ -39,17 +39,13 @@ class BaseRepository(object, CustomConfig.ConfigHelperMixin): if not os.path.exists(self.uiddir): os.mkdir(self.uiddir, 0700) - # The 'restoreatime' config parameter only applies to local Maildir - # mailboxes. def restore_atime(self): - if self.config.get('Repository ' + self.name, 'type').strip() != \ - 'Maildir': - return + """Sets folders' atime back to their values after a sync - if not self.config.has_option('Repository ' + self.name, 'restoreatime') or not self.config.getboolean('Repository ' + self.name, 'restoreatime'): - return - - return self.restore_folder_atimes() + Controlled by the 'restoreatime' config parameter (default + False), applies only to local Maildir mailboxes and does nothing + on all other repository types.""" + pass def connect(self): """Establish a connection to the remote, if necessary. This exists diff --git a/offlineimap/repository/Maildir.py b/offlineimap/repository/Maildir.py index ef3a723..75e0fe6 100644 --- a/offlineimap/repository/Maildir.py +++ b/offlineimap/repository/Maildir.py @@ -39,15 +39,19 @@ class MaildirRepository(BaseRepository): os.mkdir(self.root, 0700) def _append_folder_atimes(self, foldername): + """Store the atimes of a folder's new|cur in self.folder_atimes""" p = os.path.join(self.root, foldername) new = os.path.join(p, 'new') cur = os.path.join(p, 'cur') f = p, os.stat(new)[ST_ATIME], os.stat(cur)[ST_ATIME] self.folder_atimes.append(f) - def restore_folder_atimes(self): - if not self.folder_atimes: - return + def restore_atime(self): + """Sets folders' atime back to their values after a sync + + Controlled by the 'restoreatime' config parameter.""" + if not self.getconfboolean('restoreatime', False): + return # not configured for f in self.folder_atimes: t = f[1], os.stat(os.path.join(f[0], 'new'))[ST_MTIME] From ee1706fa90e514f40d27d5d950b211bd4725b079 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Thu, 15 Sep 2011 15:06:32 +0200 Subject: [PATCH 06/52] documentation: Clarify restoreatime setting Better document what this actually does and that most people won't be needing it. (Especially as mount setting such as relatime|noatime now reduce the amount of atime changes anyway) Signed-off-by: Sebastian Spaeth Signed-off-by: Nicolas Sebrecht --- Changelog.draft.rst | 2 ++ offlineimap.conf | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Changelog.draft.rst b/Changelog.draft.rst index 9063d5e..e4b8055 100644 --- a/Changelog.draft.rst +++ b/Changelog.draft.rst @@ -16,6 +16,8 @@ New Features Changes ------- +* Documentation improvements concerning 'restoreatime' and some code cleanup + Bug Fixes --------- diff --git a/offlineimap.conf b/offlineimap.conf index 2d5532d..224e933 100644 --- a/offlineimap.conf +++ b/offlineimap.conf @@ -285,12 +285,12 @@ localfolders = ~/Test sep = . -# Some users on *nix platforms may not want the atime (last access -# time) to be modified by OfflineIMAP. In these cases, they would -# want to set restoreatime to yes. OfflineIMAP will make an effort -# to not touch the atime if you do that. +# Some users may not want the atime (last access time) of folders to be +# modified by OfflineIMAP. If 'restoreatime' is set to yes, OfflineIMAP +# will restore the atime of the "new" and "cur" folders in each maildir +# folder to their original value after each sync. # -# In most cases, the default of no should be sufficient. +# In nearly all cases, the default should be fine. restoreatime = no From 5bcfbc152593feb4d6a134b706ef3cf1f6314e72 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Thu, 15 Sep 2011 15:06:31 +0200 Subject: [PATCH 07/52] MaildirRepository: Beautify restore_atime code Beauty of code is probably a subjective measure, but this patch hopefully is an improvement over the previous incarnation without changing functionality. Signed-off-by: Sebastian Spaeth Signed-off-by: Nicolas Sebrecht --- offlineimap/repository/Maildir.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/offlineimap/repository/Maildir.py b/offlineimap/repository/Maildir.py index 75e0fe6..aeb9776 100644 --- a/offlineimap/repository/Maildir.py +++ b/offlineimap/repository/Maildir.py @@ -43,21 +43,21 @@ class MaildirRepository(BaseRepository): p = os.path.join(self.root, foldername) new = os.path.join(p, 'new') cur = os.path.join(p, 'cur') - f = p, os.stat(new)[ST_ATIME], os.stat(cur)[ST_ATIME] - self.folder_atimes.append(f) + atimes = (p, os.path.getatime(new), os.path.getatime(cur)) + self.folder_atimes.append(atimes) def restore_atime(self): """Sets folders' atime back to their values after a sync Controlled by the 'restoreatime' config parameter.""" if not self.getconfboolean('restoreatime', False): - return # not configured + return # not configured to restore - for f in self.folder_atimes: - t = f[1], os.stat(os.path.join(f[0], 'new'))[ST_MTIME] - os.utime(os.path.join(f[0], 'new'), t) - t = f[2], os.stat(os.path.join(f[0], 'cur'))[ST_MTIME] - os.utime(os.path.join(f[0], 'cur'), t) + for (dirpath, new_atime, cur_atime) in self.folder_atimes: + new_dir = os.path.join(dirpath, 'new') + cur_dir = os.path.join(dirpath, 'cur') + os.utime(new_dir, (new_atime, os.path.getmtime(new_dir))) + os.utime(cur_dir, (cur_atime, os.path.getmtime(cur_dir))) def getlocalroot(self): return os.path.expanduser(self.getconf('localfolders')) From fe4f385e2c38676ccdb635546756b38b6a7aae79 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Mon, 12 Sep 2011 10:26:42 +0200 Subject: [PATCH 08/52] folder.IMAP: Improve dropped connection handling in quickchanged() The quickchanged() function was not handling dropped connections yet. If IMAP4.select() throws a FOLDER_RETRY error, we will now discard the connection, reconnect and retry. Signed-off-by: Sebastian Spaeth Signed-off-by: Nicolas Sebrecht --- offlineimap/folder/IMAP.py | 50 ++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/offlineimap/folder/IMAP.py b/offlineimap/folder/IMAP.py index 4dcae7d..25ebe38 100644 --- a/offlineimap/folder/IMAP.py +++ b/offlineimap/folder/IMAP.py @@ -89,27 +89,35 @@ class IMAPFolder(BaseFolder): # An IMAP folder has definitely changed if the number of # messages or the UID of the last message have changed. Otherwise # only flag changes could have occurred. - imapobj = self.imapserver.acquireconnection() - try: - # Primes untagged_responses - imaptype, imapdata = imapobj.select(self.getfullname(), readonly = 1, force = 1) - # 1. Some mail servers do not return an EXISTS response - # if the folder is empty. 2. ZIMBRA servers can return - # multiple EXISTS replies in the form 500, 1000, 1500, - # 1623 so check for potentially multiple replies. - if imapdata == [None]: - return True - maxmsgid = 0 - for msgid in imapdata: - maxmsgid = max(long(msgid), maxmsgid) - - # Different number of messages than last time? - if maxmsgid != statusfolder.getmessagecount(): - return True - - finally: - self.imapserver.releaseconnection(imapobj) - return False + retry = True # Should we attempt another round or exit? + while retry: + retry = False + imapobj = self.imapserver.acquireconnection() + try: + # Select folder and get number of messages + restype, imapdata = imapobj.select(self.getfullname(), True, + True) + except OfflineImapError, e: + # retry on dropped connections, raise otherwise + self.imapserver.releaseconnection(imapobj, true) + if e.severity == OfflineImapError.ERROR.FOLDER_RETRY: + retry = True + else: raise + finally: + self.imapserver.releaseconnection(imapobj) + # 1. Some mail servers do not return an EXISTS response + # if the folder is empty. 2. ZIMBRA servers can return + # multiple EXISTS replies in the form 500, 1000, 1500, + # 1623 so check for potentially multiple replies. + if imapdata == [None]: + return True + maxmsgid = 0 + for msgid in imapdata: + maxmsgid = max(long(msgid), maxmsgid) + # Different number of messages than last time? + if maxmsgid != statusfolder.getmessagecount(): + return True + return False def cachemessagelist(self): maxage = self.config.getdefaultint("Account %s" % self.accountname, From 7941ea7e7d6baf5549689946cbe1b8e476fb93fc Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Mon, 12 Sep 2011 10:26:43 +0200 Subject: [PATCH 09/52] folder.IMAP: Make use of the new connection discarding In getmessage() we were releaseing a connection when we detected a dropped connection, but it turns out that this was not enough, we need to explicitely discard it when we detect a dropped one. So add the drop_conn=True parameter that was recently introduced to force the discarding of the dead conection. Signed-off-by: Sebastian Spaeth Signed-off-by: Nicolas Sebrecht --- offlineimap/folder/IMAP.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/offlineimap/folder/IMAP.py b/offlineimap/folder/IMAP.py index 25ebe38..034d80d 100644 --- a/offlineimap/folder/IMAP.py +++ b/offlineimap/folder/IMAP.py @@ -99,7 +99,7 @@ class IMAPFolder(BaseFolder): True) except OfflineImapError, e: # retry on dropped connections, raise otherwise - self.imapserver.releaseconnection(imapobj, true) + self.imapserver.releaseconnection(imapobj, True) if e.severity == OfflineImapError.ERROR.FOLDER_RETRY: retry = True else: raise @@ -117,7 +117,7 @@ class IMAPFolder(BaseFolder): # Different number of messages than last time? if maxmsgid != statusfolder.getmessagecount(): return True - return False + return False def cachemessagelist(self): maxage = self.config.getdefaultint("Account %s" % self.accountname, @@ -221,7 +221,7 @@ class IMAPFolder(BaseFolder): fails_left = 0 except imapobj.abort(), e: # Release dropped connection, and get a new one - self.imapserver.releaseconnection(imapobj) + self.imapserver.releaseconnection(imapobj, True) imapobj = self.imapserver.acquireconnection() self.ui.error(e, exc_info()[2]) fails_left -= 1 From c93cd9bb1aa8dec871a9202c37bd74372bf748fe Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Fri, 16 Sep 2011 10:54:22 +0200 Subject: [PATCH 10/52] BaseFolder(): Save name and repository As all Folders share these parameters, we can safely handle them in BaseFolder. This makes sense, as BaseFolder has a getname() function that returns self.name but nothing actually set self.name. It also saves a few lines of code. Signed-off-by: Sebastian Spaeth Signed-off-by: Nicolas Sebrecht --- offlineimap/folder/Base.py | 8 +++++++- offlineimap/folder/Gmail.py | 4 ++-- offlineimap/folder/IMAP.py | 5 ++--- offlineimap/folder/LocalStatus.py | 4 +--- offlineimap/folder/Maildir.py | 5 +---- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/offlineimap/folder/Base.py b/offlineimap/folder/Base.py index 5bd8061..9c8feeb 100644 --- a/offlineimap/folder/Base.py +++ b/offlineimap/folder/Base.py @@ -28,8 +28,14 @@ except NameError: from sets import Set as set class BaseFolder(object): - def __init__(self): + def __init__(self, name, repository): + """ + :para name: Path & name of folder minus root or reference + :para repository: Repository() in which the folder is. + """ self.ui = getglobalui() + self.name = name + self.repository = repository def getname(self): """Returns name""" diff --git a/offlineimap/folder/Gmail.py b/offlineimap/folder/Gmail.py index 3ca11cd..e65793d 100644 --- a/offlineimap/folder/Gmail.py +++ b/offlineimap/folder/Gmail.py @@ -34,12 +34,12 @@ class GmailFolder(IMAPFolder): """ def __init__(self, imapserver, name, visiblename, accountname, repository): + super(GmailFolder, self).__init__(imapserver, name, visiblename, + accountname, repository) self.realdelete = repository.getrealdelete(name) self.trash_folder = repository.gettrashfolder(name) #: Gmail will really delete messages upon EXPUNGE in these folders self.real_delete_folders = [ self.trash_folder, repository.getspamfolder() ] - IMAPFolder.__init__(self, imapserver, name, visiblename, \ - accountname, repository) def deletemessages_noconvert(self, uidlist): uidlist = [uid for uid in uidlist if uid in self.messagelist] diff --git a/offlineimap/folder/IMAP.py b/offlineimap/folder/IMAP.py index 034d80d..f17e2bc 100644 --- a/offlineimap/folder/IMAP.py +++ b/offlineimap/folder/IMAP.py @@ -33,18 +33,17 @@ except NameError: class IMAPFolder(BaseFolder): def __init__(self, imapserver, name, visiblename, accountname, repository): + name = imaputil.dequote(name) + super(IMAPFolder, self).__init__(name, repository) self.config = imapserver.config self.expunge = repository.getexpunge() - self.name = imaputil.dequote(name) self.root = None # imapserver.root self.sep = imapserver.delim self.imapserver = imapserver self.messagelist = None self.visiblename = visiblename self.accountname = accountname - self.repository = repository self.randomgenerator = random.Random() - BaseFolder.__init__(self) #self.ui is set in BaseFolder def selectro(self, imapobj): diff --git a/offlineimap/folder/LocalStatus.py b/offlineimap/folder/LocalStatus.py index be9d1e3..732c5a6 100644 --- a/offlineimap/folder/LocalStatus.py +++ b/offlineimap/folder/LocalStatus.py @@ -27,18 +27,16 @@ magicline = "OFFLINEIMAP LocalStatus CACHE DATA - DO NOT MODIFY - FORMAT 1" class LocalStatusFolder(BaseFolder): def __init__(self, root, name, repository, accountname, config): - self.name = name + super(LocalStatusFolder, self).__init__(name, repository) self.root = root self.sep = '.' self.config = config self.filename = os.path.join(root, self.getfolderbasename()) self.messagelist = {} - self.repository = repository self.savelock = threading.Lock() self.doautosave = config.getdefaultboolean("general", "fsync", False) """Should we perform fsyncs as often as possible?""" self.accountname = accountname - super(LocalStatusFolder, self).__init__() def getaccountname(self): return self.accountname diff --git a/offlineimap/folder/Maildir.py b/offlineimap/folder/Maildir.py index cedcb5d..20b306b 100644 --- a/offlineimap/folder/Maildir.py +++ b/offlineimap/folder/Maildir.py @@ -59,13 +59,12 @@ def gettimeseq(): class MaildirFolder(BaseFolder): def __init__(self, root, name, sep, repository, accountname, config): - self.name = name + super(MaildirFolder, self).__init__(name, repository) self.config = config self.dofsync = config.getdefaultboolean("general", "fsync", True) self.root = root self.sep = sep self.messagelist = None - self.repository = repository self.accountname = accountname self.wincompatible = self.config.getdefaultboolean( @@ -77,8 +76,6 @@ class MaildirFolder(BaseFolder): self.infosep = '!' self.flagmatchre = re.compile(self.infosep + '.*2,([A-Z]+)') - - BaseFolder.__init__(self) #self.ui is set in BaseFolder.init() # Cache the full folder path, as we use getfullname() very often self._fullname = os.path.join(self.getroot(), self.getname()) From 410e2d35e97ce6a0fcbc3519a7725d3bfb8702f4 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Fri, 16 Sep 2011 10:54:23 +0200 Subject: [PATCH 11/52] Set accountname in BaseFolder, and don't pass it in initialization We passed in the accountname to all derivatives of BaseFolder, such as IMAPFolder(...,repository,...,accountname), although it is perfectly possible to get the accountname from the Repository(). So remove this unneeded parameter. Each backend had to define getaccountname() (although the function is hardly used and most accessed .accountname directly). On the other hand BaseFolder was using getaccountname but it never defined the function. So make the sane thing, remove all definitions from backends and define accountname() once in Basefolder. It was made a property and not just a (public) attribute, so it will show up in our developer documentation as public API. Signed-off-by: Sebastian Spaeth Signed-off-by: Nicolas Sebrecht --- offlineimap/folder/Base.py | 11 ++++++++--- offlineimap/folder/Gmail.py | 4 ++-- offlineimap/folder/IMAP.py | 6 +----- offlineimap/folder/LocalStatus.py | 6 +----- offlineimap/folder/LocalStatusSQLite.py | 3 +-- offlineimap/folder/Maildir.py | 6 +----- offlineimap/repository/Base.py | 11 +++++++---- offlineimap/repository/IMAP.py | 6 +++--- offlineimap/repository/LocalStatus.py | 2 +- offlineimap/repository/Maildir.py | 3 +-- 10 files changed, 26 insertions(+), 32 deletions(-) diff --git a/offlineimap/folder/Base.py b/offlineimap/folder/Base.py index 9c8feeb..3339424 100644 --- a/offlineimap/folder/Base.py +++ b/offlineimap/folder/Base.py @@ -44,6 +44,11 @@ class BaseFolder(object): def __str__(self): return self.name + @property + def accountname(self): + """Account name as string""" + return self.repository.accountname + def suggeststhreads(self): """Returns true if this folder suggests using threads for actions; false otherwise. Probably only IMAP will return true.""" @@ -239,7 +244,7 @@ class BaseFolder(object): # self.getmessage(). So, don't call self.getmessage unless # really needed. if register: # output that we start a new thread - self.ui.registerthread(self.getaccountname()) + self.ui.registerthread(self.accountname) try: message = None @@ -295,7 +300,7 @@ class BaseFolder(object): self.ui.error(e, exc_info()[2]) except Exception, e: self.ui.error(e, "Copying message %s [acc: %s]:\n %s" %\ - (uid, self.getaccountname(), + (uid, self.accountname, traceback.format_exc())) raise #raise on unknown errors, so we can fix those @@ -443,5 +448,5 @@ class BaseFolder(object): self.ui.error(e, exc_info()[2]) except Exception, e: self.ui.error(e, exc_info()[2], "Syncing folder %s [acc: %s]" %\ - (self, self.getaccountname())) + (self, self.accountname)) raise # raise unknown Exceptions so we can fix them diff --git a/offlineimap/folder/Gmail.py b/offlineimap/folder/Gmail.py index e65793d..dc301d0 100644 --- a/offlineimap/folder/Gmail.py +++ b/offlineimap/folder/Gmail.py @@ -33,9 +33,9 @@ class GmailFolder(IMAPFolder): http://mail.google.com/support/bin/answer.py?answer=77657&topic=12815 """ - def __init__(self, imapserver, name, visiblename, accountname, repository): + def __init__(self, imapserver, name, visiblename, repository): super(GmailFolder, self).__init__(imapserver, name, visiblename, - accountname, repository) + repository) self.realdelete = repository.getrealdelete(name) self.trash_folder = repository.gettrashfolder(name) #: Gmail will really delete messages upon EXPUNGE in these folders diff --git a/offlineimap/folder/IMAP.py b/offlineimap/folder/IMAP.py index f17e2bc..32625fa 100644 --- a/offlineimap/folder/IMAP.py +++ b/offlineimap/folder/IMAP.py @@ -32,7 +32,7 @@ except NameError: class IMAPFolder(BaseFolder): - def __init__(self, imapserver, name, visiblename, accountname, repository): + def __init__(self, imapserver, name, visiblename, repository): name = imaputil.dequote(name) super(IMAPFolder, self).__init__(name, repository) self.config = imapserver.config @@ -42,7 +42,6 @@ class IMAPFolder(BaseFolder): self.imapserver = imapserver self.messagelist = None self.visiblename = visiblename - self.accountname = accountname self.randomgenerator = random.Random() #self.ui is set in BaseFolder @@ -60,9 +59,6 @@ class IMAPFolder(BaseFolder): except imapobj.readonly: imapobj.select(self.getfullname(), readonly = 1) - def getaccountname(self): - return self.accountname - def suggeststhreads(self): return 1 diff --git a/offlineimap/folder/LocalStatus.py b/offlineimap/folder/LocalStatus.py index 732c5a6..ad7ccb6 100644 --- a/offlineimap/folder/LocalStatus.py +++ b/offlineimap/folder/LocalStatus.py @@ -26,7 +26,7 @@ except NameError: magicline = "OFFLINEIMAP LocalStatus CACHE DATA - DO NOT MODIFY - FORMAT 1" class LocalStatusFolder(BaseFolder): - def __init__(self, root, name, repository, accountname, config): + def __init__(self, root, name, repository, config): super(LocalStatusFolder, self).__init__(name, repository) self.root = root self.sep = '.' @@ -36,10 +36,6 @@ class LocalStatusFolder(BaseFolder): self.savelock = threading.Lock() self.doautosave = config.getdefaultboolean("general", "fsync", False) """Should we perform fsyncs as often as possible?""" - self.accountname = accountname - - def getaccountname(self): - return self.accountname def storesmessages(self): return 0 diff --git a/offlineimap/folder/LocalStatusSQLite.py b/offlineimap/folder/LocalStatusSQLite.py index bd38930..6eccbaa 100644 --- a/offlineimap/folder/LocalStatusSQLite.py +++ b/offlineimap/folder/LocalStatusSQLite.py @@ -46,10 +46,9 @@ class LocalStatusSQLiteFolder(LocalStatusFolder): #current version of our db format cur_version = 1 - def __init__(self, root, name, repository, accountname, config): + def __init__(self, root, name, repository, config): super(LocalStatusSQLiteFolder, self).__init__(root, name, repository, - accountname, config) # dblock protects against concurrent writes in same connection diff --git a/offlineimap/folder/Maildir.py b/offlineimap/folder/Maildir.py index 20b306b..5d75121 100644 --- a/offlineimap/folder/Maildir.py +++ b/offlineimap/folder/Maildir.py @@ -58,14 +58,13 @@ def gettimeseq(): timelock.release() class MaildirFolder(BaseFolder): - def __init__(self, root, name, sep, repository, accountname, config): + def __init__(self, root, name, sep, repository, config): super(MaildirFolder, self).__init__(name, repository) self.config = config self.dofsync = config.getdefaultboolean("general", "fsync", True) self.root = root self.sep = sep self.messagelist = None - self.accountname = accountname self.wincompatible = self.config.getdefaultboolean( "Account "+self.accountname, "maildir-windows-compatible", False) @@ -80,9 +79,6 @@ class MaildirFolder(BaseFolder): # Cache the full folder path, as we use getfullname() very often self._fullname = os.path.join(self.getroot(), self.getname()) - def getaccountname(self): - return self.accountname - def getfullname(self): """Return the absolute file path to the Maildir folder (sans cur|new)""" return self._fullname diff --git a/offlineimap/repository/Base.py b/offlineimap/repository/Base.py index 72a5f88..ae17ee8 100644 --- a/offlineimap/repository/Base.py +++ b/offlineimap/repository/Base.py @@ -22,13 +22,14 @@ from offlineimap import CustomConfig from offlineimap.ui import getglobalui class BaseRepository(object, CustomConfig.ConfigHelperMixin): + def __init__(self, reposname, account): self.ui = getglobalui() self.account = account self.config = account.getconfig() self.name = reposname self.localeval = account.getlocaleval() - self.accountname = self.account.getname() + self._accountname = self.account.getname() self.uiddir = os.path.join(self.config.getmetadatadir(), 'Repository-' + self.name) if not os.path.exists(self.uiddir): os.mkdir(self.uiddir, 0700) @@ -71,15 +72,17 @@ class BaseRepository(object, CustomConfig.ConfigHelperMixin): def __str__(self): return self.name + @property + def accountname(self): + """Account name as string""" + return self._accountname + def getuiddir(self): return self.uiddir def getmapdir(self): return self.mapdir - def getaccountname(self): - return self.accountname - def getsection(self): return 'Repository ' + self.name diff --git a/offlineimap/repository/IMAP.py b/offlineimap/repository/IMAP.py index daa31c9..0de6044 100644 --- a/offlineimap/repository/IMAP.py +++ b/offlineimap/repository/IMAP.py @@ -261,7 +261,7 @@ class IMAPRepository(BaseRepository): def getfolder(self, foldername): return self.getfoldertype()(self.imapserver, foldername, self.nametrans(foldername), - self.accountname, self) + self) def getfoldertype(self): return folder.IMAP.IMAPFolder @@ -303,7 +303,7 @@ class IMAPRepository(BaseRepository): continue retval.append(self.getfoldertype()(self.imapserver, foldername, self.nametrans(foldername), - self.accountname, self)) + self)) if len(self.folderincludes): imapobj = self.imapserver.acquireconnection() try: @@ -320,7 +320,7 @@ class IMAPRepository(BaseRepository): retval.append(self.getfoldertype()(self.imapserver, foldername, self.nametrans(foldername), - self.accountname, self)) + self)) finally: self.imapserver.releaseconnection(imapobj) diff --git a/offlineimap/repository/LocalStatus.py b/offlineimap/repository/LocalStatus.py index a392dcf..20291a1 100644 --- a/offlineimap/repository/LocalStatus.py +++ b/offlineimap/repository/LocalStatus.py @@ -83,7 +83,7 @@ class LocalStatusRepository(BaseRepository): def getfolder(self, foldername): """Return the Folder() object for a foldername""" return self.LocalStatusFolderClass(self.directory, foldername, - self, self.accountname, + self, self.config) def getfolders(self): diff --git a/offlineimap/repository/Maildir.py b/offlineimap/repository/Maildir.py index aeb9776..a7f7c79 100644 --- a/offlineimap/repository/Maildir.py +++ b/offlineimap/repository/Maildir.py @@ -118,7 +118,7 @@ class MaildirRepository(BaseRepository): self._append_folder_atimes(foldername) return folder.Maildir.MaildirFolder(self.root, foldername, self.getsep(), self, - self.accountname, self.config) + self.config) def _getfolders_scandir(self, root, extension = None): """Recursively scan folder 'root'; return a list of MailDirFolder @@ -168,7 +168,6 @@ class MaildirRepository(BaseRepository): foldername, self.getsep(), self, - self.accountname, self.config)) if self.getsep() == '/' and dirname != '.': # Recursively check sub-directories for folders too. From ee75e0921ff105d19f619973dba188fc6900ff45 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Fri, 16 Sep 2011 10:54:24 +0200 Subject: [PATCH 12/52] Remove 'config' as parameter from BaseFolder & derivatives It is possible to get the config parameter from the Repository() which is set in BaseFolder, so we set self.config there and remove the various methods and 'config' parameters that are superfluous. Signed-off-by: Sebastian Spaeth Signed-off-by: Nicolas Sebrecht --- offlineimap/folder/Base.py | 1 + offlineimap/folder/IMAP.py | 1 - offlineimap/folder/LocalStatus.py | 6 +++--- offlineimap/folder/LocalStatusSQLite.py | 5 ++--- offlineimap/folder/Maildir.py | 5 ++--- offlineimap/repository/LocalStatus.py | 3 +-- offlineimap/repository/Maildir.py | 6 ++---- 7 files changed, 11 insertions(+), 16 deletions(-) diff --git a/offlineimap/folder/Base.py b/offlineimap/folder/Base.py index 3339424..c8ed108 100644 --- a/offlineimap/folder/Base.py +++ b/offlineimap/folder/Base.py @@ -36,6 +36,7 @@ class BaseFolder(object): self.ui = getglobalui() self.name = name self.repository = repository + self.config = repository.getconfig() def getname(self): """Returns name""" diff --git a/offlineimap/folder/IMAP.py b/offlineimap/folder/IMAP.py index 32625fa..afaaa0d 100644 --- a/offlineimap/folder/IMAP.py +++ b/offlineimap/folder/IMAP.py @@ -35,7 +35,6 @@ class IMAPFolder(BaseFolder): def __init__(self, imapserver, name, visiblename, repository): name = imaputil.dequote(name) super(IMAPFolder, self).__init__(name, repository) - self.config = imapserver.config self.expunge = repository.getexpunge() self.root = None # imapserver.root self.sep = imapserver.delim diff --git a/offlineimap/folder/LocalStatus.py b/offlineimap/folder/LocalStatus.py index ad7ccb6..821ad88 100644 --- a/offlineimap/folder/LocalStatus.py +++ b/offlineimap/folder/LocalStatus.py @@ -26,15 +26,15 @@ except NameError: magicline = "OFFLINEIMAP LocalStatus CACHE DATA - DO NOT MODIFY - FORMAT 1" class LocalStatusFolder(BaseFolder): - def __init__(self, root, name, repository, config): + def __init__(self, root, name, repository): super(LocalStatusFolder, self).__init__(name, repository) self.root = root self.sep = '.' - self.config = config self.filename = os.path.join(root, self.getfolderbasename()) self.messagelist = {} self.savelock = threading.Lock() - self.doautosave = config.getdefaultboolean("general", "fsync", False) + self.doautosave = self.config.getdefaultboolean("general", "fsync", + False) """Should we perform fsyncs as often as possible?""" def storesmessages(self): diff --git a/offlineimap/folder/LocalStatusSQLite.py b/offlineimap/folder/LocalStatusSQLite.py index 6eccbaa..ba80aa1 100644 --- a/offlineimap/folder/LocalStatusSQLite.py +++ b/offlineimap/folder/LocalStatusSQLite.py @@ -46,10 +46,9 @@ class LocalStatusSQLiteFolder(LocalStatusFolder): #current version of our db format cur_version = 1 - def __init__(self, root, name, repository, config): + def __init__(self, root, name, repository): super(LocalStatusSQLiteFolder, self).__init__(root, name, - repository, - config) + repository) # dblock protects against concurrent writes in same connection self._dblock = Lock() diff --git a/offlineimap/folder/Maildir.py b/offlineimap/folder/Maildir.py index 5d75121..894328c 100644 --- a/offlineimap/folder/Maildir.py +++ b/offlineimap/folder/Maildir.py @@ -58,10 +58,9 @@ def gettimeseq(): timelock.release() class MaildirFolder(BaseFolder): - def __init__(self, root, name, sep, repository, config): + def __init__(self, root, name, sep, repository): super(MaildirFolder, self).__init__(name, repository) - self.config = config - self.dofsync = config.getdefaultboolean("general", "fsync", True) + self.dofsync = self.config.getdefaultboolean("general", "fsync", True) self.root = root self.sep = sep self.messagelist = None diff --git a/offlineimap/repository/LocalStatus.py b/offlineimap/repository/LocalStatus.py index 20291a1..d555064 100644 --- a/offlineimap/repository/LocalStatus.py +++ b/offlineimap/repository/LocalStatus.py @@ -83,8 +83,7 @@ class LocalStatusRepository(BaseRepository): def getfolder(self, foldername): """Return the Folder() object for a foldername""" return self.LocalStatusFolderClass(self.directory, foldername, - self, - self.config) + self) def getfolders(self): """Returns a list of all cached folders.""" diff --git a/offlineimap/repository/Maildir.py b/offlineimap/repository/Maildir.py index a7f7c79..2e185ee 100644 --- a/offlineimap/repository/Maildir.py +++ b/offlineimap/repository/Maildir.py @@ -117,8 +117,7 @@ class MaildirRepository(BaseRepository): if self.config.has_option('Repository ' + self.name, 'restoreatime') and self.config.getboolean('Repository ' + self.name, 'restoreatime'): self._append_folder_atimes(foldername) return folder.Maildir.MaildirFolder(self.root, foldername, - self.getsep(), self, - self.config) + self.getsep(), self) def _getfolders_scandir(self, root, extension = None): """Recursively scan folder 'root'; return a list of MailDirFolder @@ -167,8 +166,7 @@ class MaildirRepository(BaseRepository): retval.append(folder.Maildir.MaildirFolder(self.root, foldername, self.getsep(), - self, - self.config)) + self)) if self.getsep() == '/' and dirname != '.': # Recursively check sub-directories for folders too. retval.extend(self._getfolders_scandir(root, foldername)) From 80e87d0d99d00f5c3998cf249e12d117b892fbbd Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Fri, 16 Sep 2011 10:54:25 +0200 Subject: [PATCH 13/52] Don't pass in 'root' as para to LocalStatusFolders They have the Repository() which contains the root, so no need to pass it in as an extra parameter. Rename repository.LocalStatus()'s self.directory to self.root for consistency with other backends. Signed-off-by: Sebastian Spaeth Signed-off-by: Nicolas Sebrecht --- offlineimap/folder/LocalStatus.py | 7 +++---- offlineimap/folder/LocalStatusSQLite.py | 6 ++---- offlineimap/repository/LocalStatus.py | 19 +++++++++---------- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/offlineimap/folder/LocalStatus.py b/offlineimap/folder/LocalStatus.py index 821ad88..fbe2e24 100644 --- a/offlineimap/folder/LocalStatus.py +++ b/offlineimap/folder/LocalStatus.py @@ -26,11 +26,10 @@ except NameError: magicline = "OFFLINEIMAP LocalStatus CACHE DATA - DO NOT MODIFY - FORMAT 1" class LocalStatusFolder(BaseFolder): - def __init__(self, root, name, repository): + def __init__(self, name, repository): super(LocalStatusFolder, self).__init__(name, repository) - self.root = root self.sep = '.' - self.filename = os.path.join(root, self.getfolderbasename()) + self.filename = os.path.join(self.getroot(), self.getfolderbasename()) self.messagelist = {} self.savelock = threading.Lock() self.doautosave = self.config.getdefaultboolean("general", "fsync", @@ -47,7 +46,7 @@ class LocalStatusFolder(BaseFolder): return self.name def getroot(self): - return self.root + return self.repository.root def getsep(self): return self.sep diff --git a/offlineimap/folder/LocalStatusSQLite.py b/offlineimap/folder/LocalStatusSQLite.py index ba80aa1..08af807 100644 --- a/offlineimap/folder/LocalStatusSQLite.py +++ b/offlineimap/folder/LocalStatusSQLite.py @@ -46,10 +46,8 @@ class LocalStatusSQLiteFolder(LocalStatusFolder): #current version of our db format cur_version = 1 - def __init__(self, root, name, repository): - super(LocalStatusSQLiteFolder, self).__init__(root, name, - repository) - + def __init__(self, name, repository): + super(LocalStatusSQLiteFolder, self).__init__(name, repository) # dblock protects against concurrent writes in same connection self._dblock = Lock() #Try to establish connection, no need for threadsafety in __init__ diff --git a/offlineimap/repository/LocalStatus.py b/offlineimap/repository/LocalStatus.py index d555064..fdd1e19 100644 --- a/offlineimap/repository/LocalStatus.py +++ b/offlineimap/repository/LocalStatus.py @@ -25,14 +25,14 @@ import re class LocalStatusRepository(BaseRepository): def __init__(self, reposname, account): BaseRepository.__init__(self, reposname, account) - self.directory = os.path.join(account.getaccountmeta(), 'LocalStatus') - - #statusbackend can be 'plain' or 'sqlite' + # Root directory in which the LocalStatus folders reside + self.root = os.path.join(account.getaccountmeta(), 'LocalStatus') + # statusbackend can be 'plain' or 'sqlite' backend = self.account.getconf('status_backend', 'plain') if backend == 'sqlite': self._backend = 'sqlite' self.LocalStatusFolderClass = LocalStatusSQLiteFolder - self.directory += '-sqlite' + self.root += '-sqlite' elif backend == 'plain': self._backend = 'plain' self.LocalStatusFolderClass = LocalStatusFolder @@ -40,8 +40,8 @@ class LocalStatusRepository(BaseRepository): raise SyntaxWarning("Unknown status_backend '%s' for account '%s'" \ % (backend, account.name)) - if not os.path.exists(self.directory): - os.mkdir(self.directory, 0700) + if not os.path.exists(self.root): + os.mkdir(self.root, 0700) # self._folders is a list of LocalStatusFolders() self._folders = None @@ -60,7 +60,7 @@ class LocalStatusRepository(BaseRepository): # replace with literal 'dot' if final path name is '.' as '.' is # an invalid file name. basename = re.sub('(^|\/)\.$','\\1dot', basename) - return os.path.join(self.directory, basename) + return os.path.join(self.root, basename) def makefolder(self, foldername): """Create a LocalStatus Folder @@ -82,8 +82,7 @@ class LocalStatusRepository(BaseRepository): def getfolder(self, foldername): """Return the Folder() object for a foldername""" - return self.LocalStatusFolderClass(self.directory, foldername, - self) + return self.LocalStatusFolderClass(foldername, self) def getfolders(self): """Returns a list of all cached folders.""" @@ -91,7 +90,7 @@ class LocalStatusRepository(BaseRepository): return self._folders self._folders = [] - for folder in os.listdir(self.directory): + for folder in os.listdir(self.root): self._folders.append(self.getfolder(folder)) return self._folders From 0d3303ec12a0a528fed8c528fa67e5c1f1ba587f Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Fri, 16 Sep 2011 10:54:26 +0200 Subject: [PATCH 14/52] Remove visiblename as parameter to IMAPFolder creation IMAPFolder has the repository and foldername values so it can get the transposed (aka visiblename) of a folder itself just fine. There is no need to pass it in as an separate parameter. Signed-off-by: Sebastian Spaeth Signed-off-by: Nicolas Sebrecht --- offlineimap/folder/Gmail.py | 5 ++--- offlineimap/folder/IMAP.py | 4 ++-- offlineimap/repository/IMAP.py | 6 +----- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/offlineimap/folder/Gmail.py b/offlineimap/folder/Gmail.py index dc301d0..8d9c0bc 100644 --- a/offlineimap/folder/Gmail.py +++ b/offlineimap/folder/Gmail.py @@ -33,9 +33,8 @@ class GmailFolder(IMAPFolder): http://mail.google.com/support/bin/answer.py?answer=77657&topic=12815 """ - def __init__(self, imapserver, name, visiblename, repository): - super(GmailFolder, self).__init__(imapserver, name, visiblename, - repository) + def __init__(self, imapserver, name, repository): + super(GmailFolder, self).__init__(imapserver, name, repository) self.realdelete = repository.getrealdelete(name) self.trash_folder = repository.gettrashfolder(name) #: Gmail will really delete messages upon EXPUNGE in these folders diff --git a/offlineimap/folder/IMAP.py b/offlineimap/folder/IMAP.py index afaaa0d..ea86d15 100644 --- a/offlineimap/folder/IMAP.py +++ b/offlineimap/folder/IMAP.py @@ -32,7 +32,7 @@ except NameError: class IMAPFolder(BaseFolder): - def __init__(self, imapserver, name, visiblename, repository): + def __init__(self, imapserver, name, repository): name = imaputil.dequote(name) super(IMAPFolder, self).__init__(name, repository) self.expunge = repository.getexpunge() @@ -40,7 +40,7 @@ class IMAPFolder(BaseFolder): self.sep = imapserver.delim self.imapserver = imapserver self.messagelist = None - self.visiblename = visiblename + self.visiblename = repository.nametrans(name) self.randomgenerator = random.Random() #self.ui is set in BaseFolder diff --git a/offlineimap/repository/IMAP.py b/offlineimap/repository/IMAP.py index 0de6044..7392655 100644 --- a/offlineimap/repository/IMAP.py +++ b/offlineimap/repository/IMAP.py @@ -259,9 +259,7 @@ class IMAPRepository(BaseRepository): def getfolder(self, foldername): - return self.getfoldertype()(self.imapserver, foldername, - self.nametrans(foldername), - self) + return self.getfoldertype()(self.imapserver, foldername, self) def getfoldertype(self): return folder.IMAP.IMAPFolder @@ -302,7 +300,6 @@ class IMAPRepository(BaseRepository): foldername) continue retval.append(self.getfoldertype()(self.imapserver, foldername, - self.nametrans(foldername), self)) if len(self.folderincludes): imapobj = self.imapserver.acquireconnection() @@ -319,7 +316,6 @@ class IMAPRepository(BaseRepository): continue retval.append(self.getfoldertype()(self.imapserver, foldername, - self.nametrans(foldername), self)) finally: self.imapserver.releaseconnection(imapobj) From 82e47896cfa8aefea212daa8c0263a617d9ccf08 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Mon, 19 Sep 2011 08:27:18 +0200 Subject: [PATCH 15/52] Don't ask for username in the preauthtunnel case repos.getuesr() asks for a username if none is specified, but in the case of a tunnel connection, we don't need one, so we need to skip the repos.getuser() call here. Signed-off-by: Sebastian Spaeth --- offlineimap/imapserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/offlineimap/imapserver.py b/offlineimap/imapserver.py index 7c740c7..5d2d108 100644 --- a/offlineimap/imapserver.py +++ b/offlineimap/imapserver.py @@ -56,7 +56,7 @@ class IMAPServer: self.config = repos.getconfig() self.tunnel = repos.getpreauthtunnel() self.usessl = repos.getssl() - self.username = repos.getuser() + self.username = None if self.tunnel else repos.getuser() self.password = None self.passworderror = None self.goodpassword = None From f5366343b98fd2ab388d13a40955f55b8559ba9b Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Mon, 19 Sep 2011 09:25:48 +0200 Subject: [PATCH 16/52] Fix default Maildir File permissions. open() and os.open() lead to different file permissions by default, and while we have not changed the os.open that had been used, some code changes led to these permissions slipping through. Fix this by setting the permissions explicitly to 0666 (minus the users umask). Signed-off-by: Sebastian Spaeth --- Changelog.draft.rst | 3 +++ offlineimap/folder/Maildir.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Changelog.draft.rst b/Changelog.draft.rst index e4b8055..909ebd6 100644 --- a/Changelog.draft.rst +++ b/Changelog.draft.rst @@ -21,6 +21,9 @@ Changes Bug Fixes --------- +* New emails are not created with "-rwxr-xr-x" but as "-rw-r--r--" + anymore, fixing a regression in 6.3.4. + Pending for the next major release ================================== diff --git a/offlineimap/folder/Maildir.py b/offlineimap/folder/Maildir.py index 894328c..2bfd394 100644 --- a/offlineimap/folder/Maildir.py +++ b/offlineimap/folder/Maildir.py @@ -238,7 +238,7 @@ class MaildirFolder(BaseFolder): # open file and write it out try: fd = os.open(os.path.join(tmpdir, messagename), - os.O_EXCL|os.O_CREAT|os.O_WRONLY) + os.O_EXCL|os.O_CREAT|os.O_WRONLY, 0666) except OSError, e: if e.errno == 17: #FILE EXISTS ALREADY From c7938dc0819e0a14d1a97e2362e46daa86a7ce85 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Fri, 8 Jul 2011 12:23:16 +0200 Subject: [PATCH 17/52] Per-account locking Previously, we were simply locking offlineimap whenever it was running. Howver there is no reason why we shouldn't be able to invoke it in parallel, e.g. to synchronize several accounts in one offlineimap each. This patch implements the locking per-account, so that it is possible to sync different accounts at the same time. If in refresh mode, we will attempt to loop three times before giving up. This also fixes Debian bug #586655 Signed-off-by: Sebastian Spaeth --- Changelog.draft.rst | 7 +++++++ offlineimap/accounts.py | 36 ++++++++++++++++++++++++++++++++++++ offlineimap/init.py | 20 -------------------- 3 files changed, 43 insertions(+), 20 deletions(-) diff --git a/Changelog.draft.rst b/Changelog.draft.rst index 909ebd6..1b1e644 100644 --- a/Changelog.draft.rst +++ b/Changelog.draft.rst @@ -13,6 +13,13 @@ others. New Features ------------ +* Implement per-account locking, so that it will possible to sync + different accounts at the same time. The old global lock is still in + place for backward compatibility reasons (to be able to run old and + new versions of OfflineImap concurrently) and will be removed in the + future. Starting with this version, OfflineImap will be + forward-compatible with the per-account locking style. + Changes ------- diff --git a/offlineimap/accounts.py b/offlineimap/accounts.py index 428cbd8..5a2a120 100644 --- a/offlineimap/accounts.py +++ b/offlineimap/accounts.py @@ -25,6 +25,11 @@ import os from sys import exc_info import traceback +try: + import fcntl +except: + pass # ok if this fails, we can do without + def getaccountlist(customconfig): return customconfig.getsectionlist('Account') @@ -159,6 +164,35 @@ class SyncableAccount(Account): functions :meth:`syncrunner`, :meth:`sync`, :meth:`syncfolders`, used for syncing.""" + def __init__(self, *args, **kwargs): + Account.__init__(self, *args, **kwargs) + self._lockfd = None + self._lockfilepath = os.path.join(self.config.getmetadatadir(), + "%s.lock" % self) + + def lock(self): + """Lock the account, throwing an exception if it is locked already""" + self._lockfd = open(self._lockfilepath, 'w') + try: + fcntl.lockf(self._lockfd, fcntl.LOCK_EX|fcntl.LOCK_NB) + except NameError: + #fcntl not available (Windows), disable file locking... :( + pass + except IOError: + self._lockfd.close() + raise OfflineImapError("Could not lock account %s." % self, + OfflineImapError.ERROR.REPO) + + def unlock(self): + """Unlock the account, deleting the lock file""" + #If we own the lock file, delete it + if self._lockfd and not self._lockfd.closed: + self._lockfd.close() + try: + os.unlink(self._lockfilepath) + except OSError: + pass #Failed to delete for some reason. + def syncrunner(self): self.ui.registerthread(self.name) self.ui.acct(self.name) @@ -175,6 +209,7 @@ class SyncableAccount(Account): while looping: try: try: + self.lock() self.sync() except (KeyboardInterrupt, SystemExit): raise @@ -194,6 +229,7 @@ class SyncableAccount(Account): if self.refreshperiod: looping = 3 finally: + self.unlock() if looping and self.sleeper() >= 2: looping = 0 self.ui.acctdone(self.name) diff --git a/offlineimap/init.py b/offlineimap/init.py index 93b7224..f7b2eef 100644 --- a/offlineimap/init.py +++ b/offlineimap/init.py @@ -30,14 +30,6 @@ from offlineimap.ui import UI_LIST, setglobalui, getglobalui from offlineimap.CustomConfig import CustomConfigParser -try: - import fcntl - hasfcntl = 1 -except: - hasfcntl = 0 - -lockfd = None - class OfflineImap: """The main class that encapsulates the high level use of OfflineImap. @@ -46,17 +38,6 @@ class OfflineImap: oi = OfflineImap() oi.run() """ - def lock(self, config, ui): - global lockfd, hasfcntl - if not hasfcntl: - return - lockfd = open(config.getmetadatadir() + "/lock", "w") - try: - fcntl.flock(lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB) - except IOError: - ui.locked() - ui.terminate(1) - def run(self): """Parse the commandline and invoke everything""" @@ -253,7 +234,6 @@ class OfflineImap: config.set(section, "folderfilter", folderfilter) config.set(section, "folderincludes", folderincludes) - self.lock(config, ui) self.config = config def sigterm_handler(signum, frame): From 89c705bb2656b8edf028ce569329a012b4515c3e Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Fri, 16 Sep 2011 15:09:20 +0200 Subject: [PATCH 18/52] init.py: Import OfflineImapError The next commit will make use of OfflineImapError but is transient (the old-style lock). The commit is supposed to be reverted after a few releases. So add the new import in a separate commit, because we might need this even when reverting the commit. Signed-off-by: Sebastian Spaeth --- offlineimap/init.py | 1 + 1 file changed, 1 insertion(+) diff --git a/offlineimap/init.py b/offlineimap/init.py index f7b2eef..88baf9f 100644 --- a/offlineimap/init.py +++ b/offlineimap/init.py @@ -26,6 +26,7 @@ import logging from optparse import OptionParser import offlineimap from offlineimap import accounts, threadutil, syncmaster +from offlineimap.error import OfflineImapError from offlineimap.ui import UI_LIST, setglobalui, getglobalui from offlineimap.CustomConfig import CustomConfigParser From 0d9565141765b8b23c1c723d325cf494e47cc80d Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Fri, 8 Jul 2011 12:29:00 +0200 Subject: [PATCH 19/52] Perform legacy global lock in addition to new per-account lock We simply lock OfflineImap the same global way that we have always done in addition to the previously implemented per-account lock. We can keep both systems in parallel and then after a few stable releases, drop the old-style global lock. by reverting this patch Signed-off-by: Sebastian Spaeth --- offlineimap/accounts.py | 1 + offlineimap/init.py | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/offlineimap/accounts.py b/offlineimap/accounts.py index 5a2a120..5138523 100644 --- a/offlineimap/accounts.py +++ b/offlineimap/accounts.py @@ -172,6 +172,7 @@ class SyncableAccount(Account): def lock(self): """Lock the account, throwing an exception if it is locked already""" + # Take a new-style per-account lock self._lockfd = open(self._lockfilepath, 'w') try: fcntl.lockf(self._lockfd, fcntl.LOCK_EX|fcntl.LOCK_NB) diff --git a/offlineimap/init.py b/offlineimap/init.py index 88baf9f..e5560a9 100644 --- a/offlineimap/init.py +++ b/offlineimap/init.py @@ -24,6 +24,10 @@ import signal import socket import logging from optparse import OptionParser +try: + import fcntl +except ImportError: + pass #it's OK import offlineimap from offlineimap import accounts, threadutil, syncmaster from offlineimap.error import OfflineImapError @@ -311,6 +315,18 @@ class OfflineImap: #various initializations that need to be performed: offlineimap.mbnames.init(config, syncaccounts) + #TODO: keep legacy lock for a few versions, then remove. + self._legacy_lock = open(self.config.getmetadatadir() + "/lock", + 'w') + try: + fcntl.lockf(self._legacy_lock, fcntl.LOCK_EX|fcntl.LOCK_NB) + except NameError: + #fcntl not available (Windows), disable file locking... :( + pass + except IOError: + raise OfflineImapError("Could not take global lock.", + OfflineImapError.ERROR.REPO) + if options.singlethreading: #singlethreaded self.sync_singlethreaded(syncaccounts, config) @@ -330,7 +346,7 @@ class OfflineImap: return except (SystemExit): raise - except: + except Exception, e: ui.mainException() def sync_singlethreaded(self, accs, config): From 19ff636390a06fd6c82f6087cb1647e00d34856c Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Mon, 22 Aug 2011 11:49:57 +0200 Subject: [PATCH 20/52] Properly output errors when the main thread receives some Use the ui.error infrastructure that has been put in place and use ui.terminate even if we received an Exception, so that we can output the list of errors that we have. This does away with 2 now unused functions in ui/UIBase.py Signed-off-by: Sebastian Spaeth --- offlineimap/init.py | 3 ++- offlineimap/ui/UIBase.py | 8 -------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/offlineimap/init.py b/offlineimap/init.py index e5560a9..063a2c0 100644 --- a/offlineimap/init.py +++ b/offlineimap/init.py @@ -347,7 +347,8 @@ class OfflineImap: except (SystemExit): raise except Exception, e: - ui.mainException() + ui.error(e) + ui.terminate() def sync_singlethreaded(self, accs, config): """Executed if we do not want a separate syncmaster thread diff --git a/offlineimap/ui/UIBase.py b/offlineimap/ui/UIBase.py index f5dca57..5017934 100644 --- a/offlineimap/ui/UIBase.py +++ b/offlineimap/ui/UIBase.py @@ -344,14 +344,6 @@ class UIBase: s.delThreadDebugLog(thread) s.terminate(100) - def getMainExceptionString(s): - return "Main program terminated with exception:\n%s\n" %\ - traceback.format_exc() + \ - s.getThreadDebugLog(threading.currentThread()) - - def mainException(s): - s._msg(s.getMainExceptionString()) - def terminate(self, exitstatus = 0, errortitle = None, errormsg = None): """Called to terminate the application.""" #print any exceptions that have occurred over the run From 1ac9dc7fb42f797e94f5c8c30b767b1e111fc945 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Fri, 16 Sep 2011 13:57:55 +0200 Subject: [PATCH 21/52] Implement RFC 2595 LOGINDISABLED Warn the user and abort when we attempt a plaintext login, but the server has explicitly disabled plaintext logins. Signed-off-by: Sebastian Spaeth --- Changelog.draft.rst | 9 ++++----- offlineimap/imapserver.py | 6 ++++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Changelog.draft.rst b/Changelog.draft.rst index 1b1e644..632586b 100644 --- a/Changelog.draft.rst +++ b/Changelog.draft.rst @@ -20,6 +20,10 @@ New Features future. Starting with this version, OfflineImap will be forward-compatible with the per-account locking style. +* Implement RFC 2595 LOGINDISABLED. Warn the user and abort when we + attempt a plaintext login but the server has explicitly disabled + plaintext logins rather than crashing. + Changes ------- @@ -30,8 +34,3 @@ Bug Fixes * New emails are not created with "-rwxr-xr-x" but as "-rw-r--r--" anymore, fixing a regression in 6.3.4. - -Pending for the next major release -================================== - -* UIs get shorter and nicer names. (API changing) diff --git a/offlineimap/imapserver.py b/offlineimap/imapserver.py index 5d2d108..506f886 100644 --- a/offlineimap/imapserver.py +++ b/offlineimap/imapserver.py @@ -262,6 +262,12 @@ class IMAPServer: except imapobj.error, val: self.plainauth(imapobj) else: + # Use plaintext login, unless + # LOGINDISABLED (RFC2595) + if 'LOGINDISABLED' in imapobj.capabilities: + raise OfflineImapError("Plaintext login " + "disabled by server. Need to use SSL?", + OfflineImapError.ERROR.REPO) self.plainauth(imapobj) # Would bail by here if there was a failure. success = 1 From a43677a3e61cc25631cc050175791a13b2f9cf61 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Tue, 30 Aug 2011 11:01:17 +0200 Subject: [PATCH 22/52] Shorten self.infosep assignment A more pythonic and less verbose way to do the same. Add a comment what the variable is all about. Signed-off-by: Sebastian Spaeth --- offlineimap/folder/Maildir.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/offlineimap/folder/Maildir.py b/offlineimap/folder/Maildir.py index 2bfd394..f26b07e 100644 --- a/offlineimap/folder/Maildir.py +++ b/offlineimap/folder/Maildir.py @@ -68,11 +68,8 @@ class MaildirFolder(BaseFolder): self.wincompatible = self.config.getdefaultboolean( "Account "+self.accountname, "maildir-windows-compatible", False) - if self.wincompatible == False: - self.infosep = ':' - else: - self.infosep = '!' - + self.infosep = '!' if self.wincompatible else ':' + """infosep is the separator between maildir name and flag appendix""" self.flagmatchre = re.compile(self.infosep + '.*2,([A-Z]+)') #self.ui is set in BaseFolder.init() # Cache the full folder path, as we use getfullname() very often From dfdc4d457be0f411c0e2d0ddc12a6169959f26df Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Tue, 30 Aug 2011 12:36:55 +0200 Subject: [PATCH 23/52] Folder.Maildir: Simplify getmessagetime No need to use os.stat()['st_mtime'] when there is os.path.getmtime() Signed-off-by: Sebastian Spaeth --- offlineimap/folder/Maildir.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/offlineimap/folder/Maildir.py b/offlineimap/folder/Maildir.py index f26b07e..cd3da7d 100644 --- a/offlineimap/folder/Maildir.py +++ b/offlineimap/folder/Maildir.py @@ -202,11 +202,10 @@ class MaildirFolder(BaseFolder): # read it as text? return retval.replace("\r\n", "\n") - def getmessagetime( self, uid ): + def getmessagetime(self, uid): filename = self.messagelist[uid]['filename'] filepath = os.path.join(self.getfullname(), filename) - st = os.stat(filepath) - return st.st_mtime + return os.path.getmtime(filepath) def savemessage(self, uid, content, flags, rtime): # This function only ever saves to tmp/, From 32ca20d0da97b2ed2b377c06f12f4365af37538c Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Tue, 30 Aug 2011 11:19:53 +0200 Subject: [PATCH 24/52] Folder.Maildir: No need to store 'uid' in messagelist dict. The Message UID is already the key to self.messagelist, so we have that information. It is redundant to save the UID again as self.messagelist[uid]{'uid': uid} and we never made use of the information anyway. The same thing should be done with the other 2 backends. Signed-off-by: Sebastian Spaeth --- offlineimap/folder/Maildir.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/offlineimap/folder/Maildir.py b/offlineimap/folder/Maildir.py index cd3da7d..df5dd2e 100644 --- a/offlineimap/folder/Maildir.py +++ b/offlineimap/folder/Maildir.py @@ -165,10 +165,8 @@ class MaildirFolder(BaseFolder): flags = set(flagmatch.group(1)) else: flags = set() - # 'filename' is 'dirannex/filename', e.g. cur/123_U=1_FMD5=1:2,S - retval[uid] = {'uid': uid, - 'flags': flags, - 'filename': file} + # 'filename' is 'dirannex/filename', e.g. cur/123,U=1,FMD5=1:2,S + retval[uid] = {'flags': flags, 'filename': file} return retval def quickchanged(self, statusfolder): @@ -255,7 +253,7 @@ class MaildirFolder(BaseFolder): if rtime != None: os.utime(os.path.join(tmpdir, messagename), (rtime, rtime)) - self.messagelist[uid] = {'uid': uid, 'flags': set(), + self.messagelist[uid] = {'flags': set(), 'filename': os.path.join('tmp', messagename)} # savemessageflags moves msg to 'cur' or 'new' as appropriate self.savemessageflags(uid, flags) From 8ba17c5bd1610bcf242922841f9449618f9f461a Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Sun, 14 Aug 2011 13:38:13 +0200 Subject: [PATCH 25/52] add sync_this variable to all Folder() instances This variable shows if this folder should be synced or is disabled due to a folderfilter statement. This lets us distinguish between a non-existent folder and one that has been filtered out. Previously any filtered folder would simply appear to be non-existing. Signed-off-by: Sebastian Spaeth --- offlineimap/accounts.py | 16 +++++++++++++++- offlineimap/folder/Base.py | 2 ++ offlineimap/repository/IMAP.py | 12 +++++++----- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/offlineimap/accounts.py b/offlineimap/accounts.py index 5138523..13bb12e 100644 --- a/offlineimap/accounts.py +++ b/offlineimap/accounts.py @@ -274,6 +274,10 @@ class SyncableAccount(Account): # iterate through all folders on the remote repo and sync for remotefolder in remoterepos.getfolders(): + if not remotefolder.sync_this: + self.ui.debug('', "Not syncing filtered remote folder '%s'" + "[%s]" % (remotefolder, remoterepos)) + continue # Filtered out remote folder thread = InstanceLimitedThread(\ instancename = 'FOLDER_' + self.remoterepos.getname(), target = syncfolder, @@ -323,7 +327,9 @@ class SyncableAccount(Account): def syncfolder(accountname, remoterepos, remotefolder, localrepos, statusrepos, quick): """This function is called as target for the - InstanceLimitedThread invokation in SyncableAccount.""" + InstanceLimitedThread invokation in SyncableAccount. + + Filtered folders on the remote side will not invoke this function.""" ui = getglobalui() ui.registerthread(accountname) try: @@ -331,6 +337,14 @@ def syncfolder(accountname, remoterepos, remotefolder, localrepos, localfolder = localrepos.\ getfolder(remotefolder.getvisiblename().\ replace(remoterepos.getsep(), localrepos.getsep())) + + #Filtered folders on the remote side will not invoke this + #function, but we need to NOOP if the local folder is filtered + #out too: + if not localfolder.sync_this: + ui.debug('', "Not syncing filtered local folder '%s'" \ + % localfolder) + return # Write the mailboxes mbnames.add(accountname, localfolder.getvisiblename()) diff --git a/offlineimap/folder/Base.py b/offlineimap/folder/Base.py index c8ed108..3cfb18f 100644 --- a/offlineimap/folder/Base.py +++ b/offlineimap/folder/Base.py @@ -33,6 +33,8 @@ class BaseFolder(object): :para name: Path & name of folder minus root or reference :para repository: Repository() in which the folder is. """ + self.sync_this = True + """Should this folder be included in syncing?""" self.ui = getglobalui() self.name = name self.repository = repository diff --git a/offlineimap/repository/IMAP.py b/offlineimap/repository/IMAP.py index 7392655..3bd7300 100644 --- a/offlineimap/repository/IMAP.py +++ b/offlineimap/repository/IMAP.py @@ -295,12 +295,14 @@ class IMAPRepository(BaseRepository): if '\\noselect' in flaglist: continue foldername = imaputil.dequote(name) - if not self.folderfilter(foldername): - self.ui.debug('imap',"Filtering out '%s' due to folderfilter" %\ - foldername) - continue retval.append(self.getfoldertype()(self.imapserver, foldername, self)) + # filter out the folder? + if not self.folderfilter(foldername): + self.ui.debug('imap', "Filtering out '%s'[%s] due to folderfilt" + "er" % (foldername, self)) + retval[-1].sync_this = False + # Add all folderincludes if len(self.folderincludes): imapobj = self.imapserver.acquireconnection() try: @@ -322,7 +324,7 @@ class IMAPRepository(BaseRepository): retval.sort(lambda x, y: self.foldersort(x.getvisiblename(), y.getvisiblename())) self.folders = retval - return retval + return self.folders def makefolder(self, foldername): #if self.getreference() != '""': From c7420e6ad46ea3b7a8dea02f017c748037851830 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Mon, 29 Aug 2011 14:18:53 +0200 Subject: [PATCH 26/52] Debug log/error proof the creation of new IMAP folders Output a debug log line whenever we create a new folder on an IMAP server. Also raise an OfflineImap Error in case we failed to create it. Signed-off-by: Sebastian Spaeth --- offlineimap/repository/IMAP.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/offlineimap/repository/IMAP.py b/offlineimap/repository/IMAP.py index 3bd7300..03dc5ec 100644 --- a/offlineimap/repository/IMAP.py +++ b/offlineimap/repository/IMAP.py @@ -327,16 +327,27 @@ class IMAPRepository(BaseRepository): return self.folders def makefolder(self, foldername): + """Create a folder on the IMAP server + + :param foldername: Full path of the folder to be created.""" + #TODO: IMHO this existing commented out code is correct and + #should be enabled, but this would change the behavior for + #existing configurations who have a 'reference' set on a Mapped + #IMAP server....: #if self.getreference() != '""': # newname = self.getreference() + self.getsep() + foldername #else: # newname = foldername - newname = foldername imapobj = self.imapserver.acquireconnection() try: - result = imapobj.create(newname) + self.ui._msg("Creating new IMAP folder '%s' on server %s" %\ + (foldername, self)) + result = imapobj.create(foldername) if result[0] != 'OK': - raise RuntimeError, "Repository %s could not create folder %s: %s" % (self.getname(), foldername, str(result)) + raise OfflineImapError("Folder '%s'[%s] could not be created. " + "Server responded: %s" % \ + (foldername, self, str(result)), + OfflineImapError.ERROR.FOLDER) finally: self.imapserver.releaseconnection(imapobj) From 792243c78f6138904863f60059db051a6c4e4934 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Mon, 29 Aug 2011 15:33:58 +0200 Subject: [PATCH 27/52] Improve repository/Maildir.getfolder() to use cached objects Previously, getfolder() would always construct new MaildirFolder() objects, independent of whether the folder exists or not. Improve the function to: 1) Scan and cache the folders if not already done 2) Return the same cached object if we ask for the same foldername twice 3) Reduce a tiny bit of code duplication This is important because we handle stuff like folderfilter in the scandir function and if we discard the scanned dir and create a new object on folderget(), we will lose the folderfilter information. Signed-off-by: Sebastian Spaeth --- offlineimap/repository/Maildir.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/offlineimap/repository/Maildir.py b/offlineimap/repository/Maildir.py index 2e185ee..637969f 100644 --- a/offlineimap/repository/Maildir.py +++ b/offlineimap/repository/Maildir.py @@ -19,6 +19,7 @@ from Base import BaseRepository from offlineimap import folder from offlineimap.ui import getglobalui +from offlineimap.error import OfflineImapError import os from stat import * @@ -114,11 +115,20 @@ class MaildirRepository(BaseRepository): self.ui.warn("NOT YET IMPLEMENTED: DELETE FOLDER %s" % foldername) def getfolder(self, foldername): - if self.config.has_option('Repository ' + self.name, 'restoreatime') and self.config.getboolean('Repository ' + self.name, 'restoreatime'): - self._append_folder_atimes(foldername) - return folder.Maildir.MaildirFolder(self.root, foldername, - self.getsep(), self) - + """Return a Folder instance of this Maildir + + If necessary, scan and cache all foldernames to make sure that + we only return existing folders and that 2 calls with the same + name will return the same object.""" + # getfolders() will scan and cache the values *if* necessary + folders = self.getfolders() + for folder in folders: + if foldername == folder.name: + return folder + raise OfflineImapError("getfolder() asked for a nonexisting " + "folder '%s'." % foldername, + OfflineImapError.ERROR.FOLDER) + def _getfolders_scandir(self, root, extension = None): """Recursively scan folder 'root'; return a list of MailDirFolder @@ -157,11 +167,7 @@ class MaildirRepository(BaseRepository): os.path.isdir(os.path.join(fullname, 'tmp'))): # This directory has maildir stuff -- process self.debug(" This is maildir folder '%s'." % foldername) - - if self.config.has_option('Repository %s' % self, - 'restoreatime') and \ - self.config.getboolean('Repository %s' % self, - 'restoreatime'): + if self.getconfboolean('restoreatime', False): self._append_folder_atimes(foldername) retval.append(folder.Maildir.MaildirFolder(self.root, foldername, From a279aa7307ca036f93b660ade84099550ff1a4f2 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Mon, 19 Sep 2011 14:14:44 +0200 Subject: [PATCH 28/52] Maildir: Call top-level directory '', not '.' If nametrans translates to an empty directory we want to find the top-level directory by name '' and not by name '.'. This unbreaks nametrans rules that result in empty folder names. Signed-off-by: Sebastian Spaeth --- offlineimap/repository/Maildir.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/offlineimap/repository/Maildir.py b/offlineimap/repository/Maildir.py index 637969f..afdfb76 100644 --- a/offlineimap/repository/Maildir.py +++ b/offlineimap/repository/Maildir.py @@ -146,7 +146,7 @@ class MaildirRepository(BaseRepository): self.debug(" toppath = %s" % toppath) # Iterate over directories in top & top itself. - for dirname in os.listdir(toppath) + ['.']: + for dirname in os.listdir(toppath) + ['']: self.debug(" *** top of loop") self.debug(" dirname = %s" % dirname) if dirname in ['cur', 'new', 'tmp']: @@ -173,7 +173,7 @@ class MaildirRepository(BaseRepository): foldername, self.getsep(), self)) - if self.getsep() == '/' and dirname != '.': + if self.getsep() == '/' and dirname != '': # Recursively check sub-directories for folders too. retval.extend(self._getfolders_scandir(root, foldername)) self.debug("_GETFOLDERS_SCANDIR RETURNING %s" % \ From 3157a8d793876d050bff29a3830c26a43ed8a78a Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Mon, 19 Sep 2011 14:22:03 +0200 Subject: [PATCH 29/52] repository.Gmail.getfolder() Also remove the removed parameters in the Gmail folder initialization. This is one spot where I had forgotten to also strip the parameters. Signed-off-by: Sebastian Spaeth --- offlineimap/repository/Gmail.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/offlineimap/repository/Gmail.py b/offlineimap/repository/Gmail.py index 20ed1f1..ada2146 100644 --- a/offlineimap/repository/Gmail.py +++ b/offlineimap/repository/Gmail.py @@ -59,8 +59,7 @@ class GmailRepository(IMAPRepository): def getfolder(self, foldername): return self.getfoldertype()(self.imapserver, foldername, - self.nametrans(foldername), - self.accountname, self) + self) def getfoldertype(self): return folder.Gmail.GmailFolder From 9c5f73d3b34f29eb5e4ea5ca157f8b058684c9bc Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Mon, 29 Aug 2011 15:08:26 +0200 Subject: [PATCH 30/52] Move self.folderfilter|nametrans|sort from IMAPFolder to BaseFolder We want to have these functions available for Maildir folders too, so we can folderfilter a Maildir repository too (which is currently not possible) This commit only move the corresponding functions from the IMAP to the Base implementation. It should not change behavior in any way yet. Signed-off-by: Sebastian Spaeth --- offlineimap/repository/Base.py | 18 ++++++++++++++++++ offlineimap/repository/IMAP.py | 18 ------------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/offlineimap/repository/Base.py b/offlineimap/repository/Base.py index ae17ee8..76fa4a3 100644 --- a/offlineimap/repository/Base.py +++ b/offlineimap/repository/Base.py @@ -16,6 +16,7 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +import re import os.path import traceback from offlineimap import CustomConfig @@ -40,6 +41,23 @@ class BaseRepository(object, CustomConfig.ConfigHelperMixin): if not os.path.exists(self.uiddir): os.mkdir(self.uiddir, 0700) + self.nametrans = lambda foldername: foldername + self.folderfilter = lambda foldername: 1 + self.folderincludes = [] + self.foldersort = cmp + if self.config.has_option(self.getsection(), 'nametrans'): + self.nametrans = self.localeval.eval( + self.getconf('nametrans'), {'re': re}) + if self.config.has_option(self.getsection(), 'folderfilter'): + self.folderfilter = self.localeval.eval( + self.getconf('folderfilter'), {'re': re}) + if self.config.has_option(self.getsection(), 'folderincludes'): + self.folderincludes = self.localeval.eval( + self.getconf('folderincludes'), {'re': re}) + if self.config.has_option(self.getsection(), 'foldersort'): + self.foldersort = self.localeval.eval( + self.getconf('foldersort'), {'re': re}) + def restore_atime(self): """Sets folders' atime back to their values after a sync diff --git a/offlineimap/repository/IMAP.py b/offlineimap/repository/IMAP.py index 03dc5ec..5ddf5b9 100644 --- a/offlineimap/repository/IMAP.py +++ b/offlineimap/repository/IMAP.py @@ -21,7 +21,6 @@ from offlineimap import folder, imaputil, imapserver, OfflineImapError from offlineimap.folder.UIDMaps import MappedIMAPFolder from offlineimap.threadutil import ExitNotifyThread from threading import Event -import re import types import os from sys import exc_info @@ -36,23 +35,6 @@ class IMAPRepository(BaseRepository): self._host = None self.imapserver = imapserver.IMAPServer(self) self.folders = None - self.nametrans = lambda foldername: foldername - self.folderfilter = lambda foldername: 1 - self.folderincludes = [] - self.foldersort = cmp - localeval = self.localeval - if self.config.has_option(self.getsection(), 'nametrans'): - self.nametrans = localeval.eval(self.getconf('nametrans'), - {'re': re}) - if self.config.has_option(self.getsection(), 'folderfilter'): - self.folderfilter = localeval.eval(self.getconf('folderfilter'), - {'re': re}) - if self.config.has_option(self.getsection(), 'folderincludes'): - self.folderincludes = localeval.eval(self.getconf('folderincludes'), - {'re': re}) - if self.config.has_option(self.getsection(), 'foldersort'): - self.foldersort = localeval.eval(self.getconf('foldersort'), - {'re': re}) def startkeepalive(self): keepalivetime = self.getkeepalive() From 7d34060217ba7ea0577d9140abf21b49f13a4413 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Mon, 29 Aug 2011 15:09:16 +0200 Subject: [PATCH 31/52] Enable folderfiltering for MailDir repositories too Currently we only filtered IMAP repositories, this patch enables filtering for Maildir repositories too. Signed-off-by: Sebastian Spaeth --- Changelog.draft.rst | 2 ++ offlineimap/repository/Maildir.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/Changelog.draft.rst b/Changelog.draft.rst index 632586b..f83735f 100644 --- a/Changelog.draft.rst +++ b/Changelog.draft.rst @@ -29,6 +29,8 @@ Changes * Documentation improvements concerning 'restoreatime' and some code cleanup +* Maildir repositories now also respond to folderfilter= configurations. + Bug Fixes --------- diff --git a/offlineimap/repository/Maildir.py b/offlineimap/repository/Maildir.py index afdfb76..7c3bb1e 100644 --- a/offlineimap/repository/Maildir.py +++ b/offlineimap/repository/Maildir.py @@ -173,6 +173,12 @@ class MaildirRepository(BaseRepository): foldername, self.getsep(), self)) + # filter out the folder? + if not self.folderfilter(foldername): + self.debug("Filtering out '%s'[%s] due to folderfilt" + "er" % (foldername, self)) + retval[-1].sync_this = False + if self.getsep() == '/' and dirname != '': # Recursively check sub-directories for folders too. retval.extend(self._getfolders_scandir(root, foldername)) From 6b2ec956cfe8e356d3ffd54eee34773deb73279f Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Mon, 19 Sep 2011 13:41:38 +0200 Subject: [PATCH 32/52] Apply nametrans to all Foldertypes getvisiblename() was only defined on IMAP(derived) foldertypes, but we want it on eg. Maildirs too, so we define it centrally in Folder.Base.py rather than only in folder.IMAP.py. Signed-off-by: Sebastian Spaeth --- offlineimap/folder/Base.py | 4 +++- offlineimap/folder/IMAP.py | 4 ---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/offlineimap/folder/Base.py b/offlineimap/folder/Base.py index 3cfb18f..d9fed70 100644 --- a/offlineimap/folder/Base.py +++ b/offlineimap/folder/Base.py @@ -38,6 +38,7 @@ class BaseFolder(object): self.ui = getglobalui() self.name = name self.repository = repository + self.visiblename = repository.nametrans(name) self.config = repository.getconfig() def getname(self): @@ -69,7 +70,8 @@ class BaseFolder(object): return 1 def getvisiblename(self): - return self.name + """The nametrans-transposed name of the folder's name""" + return self.visiblename def getrepository(self): """Returns the repository object that this folder is within.""" diff --git a/offlineimap/folder/IMAP.py b/offlineimap/folder/IMAP.py index ea86d15..041f5a0 100644 --- a/offlineimap/folder/IMAP.py +++ b/offlineimap/folder/IMAP.py @@ -40,7 +40,6 @@ class IMAPFolder(BaseFolder): self.sep = imapserver.delim self.imapserver = imapserver self.messagelist = None - self.visiblename = repository.nametrans(name) self.randomgenerator = random.Random() #self.ui is set in BaseFolder @@ -67,9 +66,6 @@ class IMAPFolder(BaseFolder): def getcopyinstancelimit(self): return 'MSGCOPY_' + self.repository.getname() - def getvisiblename(self): - return self.visiblename - def getuidvalidity(self): imapobj = self.imapserver.acquireconnection() try: From fd6261a50f039c21895d4fa9780d00bd0db19474 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Sun, 14 Aug 2011 13:38:54 +0200 Subject: [PATCH 33/52] Create new folders on srcrepo if needed This will ignore any nametrans rules, so we might want to limit this only to cases where no nametrans has been specified, or we might want to use the nametrans setting of the dest repo. Signed-off-by: Sebastian Spaeth --- Changelog.draft.rst | 7 ++++ offlineimap/repository/Base.py | 62 +++++++++++++++++++++++----------- 2 files changed, 49 insertions(+), 20 deletions(-) diff --git a/Changelog.draft.rst b/Changelog.draft.rst index f83735f..16655a1 100644 --- a/Changelog.draft.rst +++ b/Changelog.draft.rst @@ -24,6 +24,13 @@ New Features attempt a plaintext login but the server has explicitly disabled plaintext logins rather than crashing. +* Folders will now also be automatically created on the REMOTE side of + an account if they exist on the local side. Use the folderfilters + setting on the local side to prevent some folders from migrating to + the remote side. Also, if you have a nametrans setting on the remote + repository, you might need a nametrans setting on the local repository + that leads to the original name (reverse nametrans). + Changes ------- diff --git a/offlineimap/repository/Base.py b/offlineimap/repository/Base.py index 76fa4a3..59bc3cb 100644 --- a/offlineimap/repository/Base.py +++ b/offlineimap/repository/Base.py @@ -19,8 +19,10 @@ import re import os.path import traceback +from sys import exc_info from offlineimap import CustomConfig from offlineimap.ui import getglobalui +from offlineimap.error import OfflineImapError class BaseRepository(object, CustomConfig.ConfigHelperMixin): @@ -134,7 +136,11 @@ class BaseRepository(object, CustomConfig.ConfigHelperMixin): def syncfoldersto(self, dst_repo, status_repo): """Syncs the folders in this repository to those in dest. - It does NOT sync the contents of those folders.""" + It does NOT sync the contents of those folders. nametrans rules + in both directions will be honored, but there are NO checks yet + that forward and backward nametrans actually match up! + Configuring nametrans on BOTH repositories therefore could lead + to infinite folder creation cycles.""" src_repo = self src_folders = src_repo.getfolders() dst_folders = dst_repo.getfolders() @@ -149,31 +155,47 @@ class BaseRepository(object, CustomConfig.ConfigHelperMixin): for folder in dst_folders: dst_hash[folder.getvisiblename()] = folder - # - # Find new folders. - for key in src_hash.keys(): - if not key in dst_hash: + # Find new folders on src_repo. + for src_name, src_folder in src_hash.iteritems(): + if src_folder.sync_this and not src_name in dst_hash: try: - dst_repo.makefolder(key) - status_repo.makefolder(key.replace(dst_repo.getsep(), - status_repo.getsep())) - except (KeyboardInterrupt): + dst_repo.makefolder(src_name) + except OfflineImapError, e: + self.ui.error(e, exc_info()[2], + "Creating folder %s on repository %s" %\ + (src_name, dst_repo)) raise - except: - self.ui.warn("ERROR Attempting to create folder " \ - + key + ":" +traceback.format_exc()) + status_repo.makefolder(src_name.replace(dst_repo.getsep(), + status_repo.getsep())) + # Find new folders on dst_repo. + for dst_name, dst_folder in dst_hash.iteritems(): + if dst_folder.sync_this and not dst_name in src_hash: + # Check that back&forth nametrans lead to identical names + #src_name is the unmodified full src_name + newsrc_name = dst_name.replace(dst_repo.getsep(), + src_repo.getsep()) + folder = self.getfolder(newsrc_name) + newdst_name = folder.getvisiblename().replace( + src_repo.getsep(), dst_repo.getsep()) + if newsrc_name == newdst_name: + assert False, "newdstname %s equals newsrcname %s %s %s" % (newdst_name, newsrc_name, dst_name, folder) + else: + assert False, "newdstname %s Does not equal newsrcname %s %s %s" % (newdst_name, newsrc_name, dst_name, folder) + # end sanity check, actually create the folder - # + try: + src_repo.makefolder(dst_name.replace( + dst_repo.getsep(), src_repo.getsep())) + except OfflineImapError, e: + self.ui.error(e, exc_info()[2], + "Creating folder %s on repository %s" %\ + (src_name, dst_repo)) + raise + status_repo.makefolder(dst_name.replace( + dst_repo.getsep(), status_repo.getsep())) # Find deleted folders. - # # We don't delete folders right now. - #for key in desthash.keys(): - # if not key in srchash: - # dest.deletefolder(key) - - ##### Keepalive - def startkeepalive(self): """The default implementation will do nothing.""" pass From b92e453f0ea7c26c5eceebfb45083338564281e6 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Mon, 19 Sep 2011 14:01:48 +0200 Subject: [PATCH 34/52] sanity check so that nametrans does not lead to infinite folder creation --- offlineimap/repository/Base.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/offlineimap/repository/Base.py b/offlineimap/repository/Base.py index 59bc3cb..ebd024f 100644 --- a/offlineimap/repository/Base.py +++ b/offlineimap/repository/Base.py @@ -170,17 +170,25 @@ class BaseRepository(object, CustomConfig.ConfigHelperMixin): # Find new folders on dst_repo. for dst_name, dst_folder in dst_hash.iteritems(): if dst_folder.sync_this and not dst_name in src_hash: - # Check that back&forth nametrans lead to identical names + # nametrans sanity check! + # Does nametrans back&forth lead to identical names? #src_name is the unmodified full src_name newsrc_name = dst_name.replace(dst_repo.getsep(), src_repo.getsep()) folder = self.getfolder(newsrc_name) newdst_name = folder.getvisiblename().replace( - src_repo.getsep(), dst_repo.getsep()) - if newsrc_name == newdst_name: - assert False, "newdstname %s equals newsrcname %s %s %s" % (newdst_name, newsrc_name, dst_name, folder) - else: - assert False, "newdstname %s Does not equal newsrcname %s %s %s" % (newdst_name, newsrc_name, dst_name, folder) + src_repo.getsep(), dst_repo.getsep()) + if newsrc_name != newdst_name: + raise OfflineImapError("INFINITE FOLDER CREATION DETECTED! " + "Folder '%s' (repository '%s') would be created as fold" + "er '%s' (repository '%s'). The atter becomes '%s' in r" + "eturn, leading to infinite folder creation cycles.\n S" + "OLUTION: 1) Do set your nametrans rules on both reposi" + "tories so they lead to identical names if applied back" + " and forth. 2) Use folderfilter settings on a reposito" + "ry to prevent some folders from being created on the o" "ther side." % (dst_folder, dst_repo, newsrc_name, + src_repo, newdst_name), + OfflineImapError.ERROR.REPO) # end sanity check, actually create the folder try: From eeef8f4bab057e1e73ab7d1d17898643dee003aa Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Mon, 19 Sep 2011 15:07:34 +0200 Subject: [PATCH 35/52] Don't ask for hostname if using a tunnel In tunnel mode, we don't need to ask for a hostname because there is no need for one. Fix this. Signed-off-by: Sebastian Spaeth --- offlineimap/imapserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/offlineimap/imapserver.py b/offlineimap/imapserver.py index 506f886..dc0bc50 100644 --- a/offlineimap/imapserver.py +++ b/offlineimap/imapserver.py @@ -60,7 +60,7 @@ class IMAPServer: self.password = None self.passworderror = None self.goodpassword = None - self.hostname = repos.gethost() + self.hostname = None if self.tunnel else repos.gethost() self.port = repos.getport() if self.port == None: self.port = 993 if self.usessl else 143 From 050bbb3d2d759da8b2785c2a5efd0ee529a1c42b Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Mon, 19 Sep 2011 15:21:27 +0200 Subject: [PATCH 36/52] Reset draft Changelog post-release Empty it, so new stuff can pour in. Signed-off-by: Sebastian Spaeth --- Changelog.draft.rst | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/Changelog.draft.rst b/Changelog.draft.rst index 16655a1..76a0ea3 100644 --- a/Changelog.draft.rst +++ b/Changelog.draft.rst @@ -13,33 +13,8 @@ others. New Features ------------ -* Implement per-account locking, so that it will possible to sync - different accounts at the same time. The old global lock is still in - place for backward compatibility reasons (to be able to run old and - new versions of OfflineImap concurrently) and will be removed in the - future. Starting with this version, OfflineImap will be - forward-compatible with the per-account locking style. - -* Implement RFC 2595 LOGINDISABLED. Warn the user and abort when we - attempt a plaintext login but the server has explicitly disabled - plaintext logins rather than crashing. - -* Folders will now also be automatically created on the REMOTE side of - an account if they exist on the local side. Use the folderfilters - setting on the local side to prevent some folders from migrating to - the remote side. Also, if you have a nametrans setting on the remote - repository, you might need a nametrans setting on the local repository - that leads to the original name (reverse nametrans). - Changes ------- -* Documentation improvements concerning 'restoreatime' and some code cleanup - -* Maildir repositories now also respond to folderfilter= configurations. - Bug Fixes --------- - -* New emails are not created with "-rwxr-xr-x" but as "-rw-r--r--" - anymore, fixing a regression in 6.3.4. From 48fdb1441892e1fdade1e13cd5679755bee53a81 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Mon, 19 Sep 2011 19:59:39 +0200 Subject: [PATCH 37/52] Don't output thread ID in log The thread ID is not really useful and looks ugly. It also makes lines longer than needed, there is more useful information we can put in the log. So do away with it. Signed-off-by: Sebastian Spaeth --- offlineimap/ui/UIBase.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/offlineimap/ui/UIBase.py b/offlineimap/ui/UIBase.py index 5017934..52b1788 100644 --- a/offlineimap/ui/UIBase.py +++ b/offlineimap/ui/UIBase.py @@ -104,11 +104,10 @@ class UIBase: ui.error(exc, sys.exc_info()[2], msg="While syncing Folder %s in " "repo %s") """ - cur_thread = threading.currentThread() if msg: - self._msg("ERROR [%s]: %s\n %s" % (cur_thread, msg, exc)) + self._msg("ERROR: %s\n %s" % (msg, exc)) else: - self._msg("ERROR [%s]: %s" % (cur_thread, exc)) + self._msg("ERROR: %s" % (exc)) if not self.debuglist: # only output tracebacks in debug mode From ee82cd135b51059400d03255c83690500286d33d Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Mon, 19 Sep 2011 20:07:56 +0200 Subject: [PATCH 38/52] Fix typo atter->latter Simply typo in user error message. Signed-off-by: Sebastian Spaeth --- offlineimap/repository/Base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/offlineimap/repository/Base.py b/offlineimap/repository/Base.py index ebd024f..e5c3f1c 100644 --- a/offlineimap/repository/Base.py +++ b/offlineimap/repository/Base.py @@ -181,9 +181,9 @@ class BaseRepository(object, CustomConfig.ConfigHelperMixin): if newsrc_name != newdst_name: raise OfflineImapError("INFINITE FOLDER CREATION DETECTED! " "Folder '%s' (repository '%s') would be created as fold" - "er '%s' (repository '%s'). The atter becomes '%s' in r" - "eturn, leading to infinite folder creation cycles.\n S" - "OLUTION: 1) Do set your nametrans rules on both reposi" + "er '%s' (repository '%s'). The latter becomes '%s' in " + "return, leading to infinite folder creation cycles.\n " + "SOLUTION: 1) Do set your nametrans rules on both reposi" "tories so they lead to identical names if applied back" " and forth. 2) Use folderfilter settings on a reposito" "ry to prevent some folders from being created on the o" "ther side." % (dst_folder, dst_repo, newsrc_name, From b0e88622c4b734dc87d2cc44aba576de62ff40f1 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Mon, 19 Sep 2011 21:06:54 +0200 Subject: [PATCH 39/52] Fix think on how to compare folder names on src and dest It is not easy to think through when to use visiblenames() and whatnot. It seems I managed to not think it through properly. Which might be fixed now. Signed-off-by: Sebastian Spaeth --- offlineimap/repository/Base.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/offlineimap/repository/Base.py b/offlineimap/repository/Base.py index e5c3f1c..b5e91d8 100644 --- a/offlineimap/repository/Base.py +++ b/offlineimap/repository/Base.py @@ -153,7 +153,7 @@ class BaseRepository(object, CustomConfig.ConfigHelperMixin): src_repo.getsep(), dst_repo.getsep())] = folder dst_hash = {} for folder in dst_folders: - dst_hash[folder.getvisiblename()] = folder + dst_hash[folder.name] = folder # Find new folders on src_repo. for src_name, src_folder in src_hash.iteritems(): @@ -173,21 +173,23 @@ class BaseRepository(object, CustomConfig.ConfigHelperMixin): # nametrans sanity check! # Does nametrans back&forth lead to identical names? #src_name is the unmodified full src_name - newsrc_name = dst_name.replace(dst_repo.getsep(), - src_repo.getsep()) + newsrc_name = dst_folder.getvisiblename().replace( + dst_repo.getsep(), + src_repo.getsep()) folder = self.getfolder(newsrc_name) newdst_name = folder.getvisiblename().replace( src_repo.getsep(), dst_repo.getsep()) - if newsrc_name != newdst_name: + if dst_name != newdst_name: raise OfflineImapError("INFINITE FOLDER CREATION DETECTED! " "Folder '%s' (repository '%s') would be created as fold" "er '%s' (repository '%s'). The latter becomes '%s' in " "return, leading to infinite folder creation cycles.\n " - "SOLUTION: 1) Do set your nametrans rules on both reposi" - "tories so they lead to identical names if applied back" - " and forth. 2) Use folderfilter settings on a reposito" - "ry to prevent some folders from being created on the o" "ther side." % (dst_folder, dst_repo, newsrc_name, - src_repo, newdst_name), + "SOLUTION: 1) Do set your nametrans rules on both repos" + "itories so they lead to identical names if applied bac" + "k and forth. 2) Use folderfilter settings on a reposit" + "ory to prevent some folders from being created on the " + "other side." % (dst_name, dst_repo, newsrc_name, + src_repo, newdst_name), OfflineImapError.ERROR.REPO) # end sanity check, actually create the folder From 1c58ebe3484fa72e541acba14ff5498df63c77a7 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Mon, 19 Sep 2011 21:25:50 +0200 Subject: [PATCH 40/52] Do not create folder on REMOTE if it would fall in REMOTE's folderfilter Previously, we only checked if a LOCAL folder falls under the local repositories folderfilter rule when deciding whether a folder should be created on REMOTE. However, we also do not want to create the folder on REMOTE if it would fall under a folderfilter rule there. This patch prevents us from doing so. Signed-off-by: Sebastian Spaeth --- offlineimap/repository/Base.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/offlineimap/repository/Base.py b/offlineimap/repository/Base.py index b5e91d8..a08ef32 100644 --- a/offlineimap/repository/Base.py +++ b/offlineimap/repository/Base.py @@ -177,6 +177,13 @@ class BaseRepository(object, CustomConfig.ConfigHelperMixin): dst_repo.getsep(), src_repo.getsep()) folder = self.getfolder(newsrc_name) + # would src repo filter out the new folder name? In this + # case don't create it on it: + if not self.folderfilter(newsrc_name): + self.ui.debug('', "Not creating folder '%s' (repository '%s" + "') as it would be filtered out on that repository." % + (newsrc_name, self)) + continue newdst_name = folder.getvisiblename().replace( src_repo.getsep(), dst_repo.getsep()) if dst_name != newdst_name: From db28cb77e7a555e889a3c6057f58697859f94ecc Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Tue, 20 Sep 2011 22:25:45 +0200 Subject: [PATCH 41/52] Fix another visiblename() glitch in determining the foldername to be created Commit b0e88622c4b changed dst_hash[folder.visiblename] to dst_hash[folder.name] but we did not adapt all places where it is needed to use visiblename again. This led to attempting to create a name on REMOTE ignoring the nametrans setting on the LOCAL repository. Signed-off-by: Sebastian Spaeth --- offlineimap/repository/Base.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/offlineimap/repository/Base.py b/offlineimap/repository/Base.py index a08ef32..0da307f 100644 --- a/offlineimap/repository/Base.py +++ b/offlineimap/repository/Base.py @@ -172,7 +172,7 @@ class BaseRepository(object, CustomConfig.ConfigHelperMixin): if dst_folder.sync_this and not dst_name in src_hash: # nametrans sanity check! # Does nametrans back&forth lead to identical names? - #src_name is the unmodified full src_name + #src_name is the unmodified full src_name that would be created newsrc_name = dst_folder.getvisiblename().replace( dst_repo.getsep(), src_repo.getsep()) @@ -184,6 +184,7 @@ class BaseRepository(object, CustomConfig.ConfigHelperMixin): "') as it would be filtered out on that repository." % (newsrc_name, self)) continue + # apply reverse nametrans to see if we end up with the same name newdst_name = folder.getvisiblename().replace( src_repo.getsep(), dst_repo.getsep()) if dst_name != newdst_name: @@ -201,15 +202,14 @@ class BaseRepository(object, CustomConfig.ConfigHelperMixin): # end sanity check, actually create the folder try: - src_repo.makefolder(dst_name.replace( - dst_repo.getsep(), src_repo.getsep())) + src_repo.makefolder(newsrc_name) except OfflineImapError, e: self.ui.error(e, exc_info()[2], "Creating folder %s on repository %s" %\ (src_name, dst_repo)) raise - status_repo.makefolder(dst_name.replace( - dst_repo.getsep(), status_repo.getsep())) + status_repo.makefolder(newsrc_name.replace( + src_repo.getsep(), status_repo.getsep())) # Find deleted folders. # We don't delete folders right now. From 347e1eaa3228a5aeac8d5375189646a5fb564f31 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Wed, 21 Sep 2011 02:16:33 +0200 Subject: [PATCH 42/52] update CAPABILITIES after login Some Webservers (I am looking at you Gmail) send different capabilities before and after login, so they can tailor their server capabilities to the user. While legal, this is uncommon and we were not updating our server capabilities. Doing so allows us to detect that Gmail actually supports the UIDPLUS extension, and we will stop mangling headers when uploading to Gmail. This could lead to some performance gains when we upload many messages to Gmail. Signed-off-by: Sebastian Spaeth --- Changelog.draft.rst | 4 ++++ offlineimap/imapserver.py | 8 ++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Changelog.draft.rst b/Changelog.draft.rst index 76a0ea3..9bae1d8 100644 --- a/Changelog.draft.rst +++ b/Changelog.draft.rst @@ -16,5 +16,9 @@ New Features Changes ------- +* Refresh server capabilities after login, so we know that Gmail + supports UIDPLUS (it only announces that after login, not + before). This prevents us from adding custom headers to Gmail uploads. + Bug Fixes --------- diff --git a/offlineimap/imapserver.py b/offlineimap/imapserver.py index dc0bc50..ead3280 100644 --- a/offlineimap/imapserver.py +++ b/offlineimap/imapserver.py @@ -1,6 +1,5 @@ # IMAP server support -# Copyright (C) 2002 - 2007 John Goerzen -# +# Copyright (C) 2002 - 2011 John Goerzen & contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -276,6 +275,11 @@ class IMAPServer: self.passworderror = str(val) raise + # update capabilities after login, e.g. gmail serves different ones + typ, dat = imapobj.capability() + if dat != [None]: + imapobj.capabilities = tuple(dat[-1].upper().split()) + if self.delim == None: listres = imapobj.list(self.reference, '""')[1] if listres == [None] or listres == None: From 275dbfa3fb1349c01f813bf8fe4bd812db671433 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Wed, 21 Sep 2011 02:08:09 +0200 Subject: [PATCH 43/52] Release 6.3.5-rc3 Update Changelog and __VERSION__ for 6.3.5-rc3. Also add in the notes for rc2 to the Changelog. Signed-off-by: Sebastian Spaeth --- Changelog.draft.rst | 4 ---- Changelog.rst | 52 +++++++++++++++++++++++++++++++++++++++++ offlineimap/__init__.py | 2 +- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/Changelog.draft.rst b/Changelog.draft.rst index 9bae1d8..b532734 100644 --- a/Changelog.draft.rst +++ b/Changelog.draft.rst @@ -15,10 +15,6 @@ New Features Changes ------- - -* Refresh server capabilities after login, so we know that Gmail - supports UIDPLUS (it only announces that after login, not - before). This prevents us from adding custom headers to Gmail uploads. Bug Fixes --------- diff --git a/Changelog.rst b/Changelog.rst index 4bdff19..05c9a57 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -11,6 +11,58 @@ ChangeLog on releases. And because I'm lazy, it will also be used as a draft for the releases announces. +OfflineIMAP v6.3.5-rc3 (2011-09-21) +=================================== + +Changes +------- + +* Refresh server capabilities after login, so we know that Gmail + supports UIDPLUS (it only announces that after login, not + before). This prevents us from adding custom headers to Gmail uploads. + +Bug Fixes +--------- + +* Fix the creation of folders on remote repositories, which was still + botched on rc2. + +OfflineIMAP v6.3.5-rc2 (2011-09-19) +=================================== + +New Features +------------ + +* Implement per-account locking, so that it will possible to sync + different accounts at the same time. The old global lock is still in + place for backward compatibility reasons (to be able to run old and + new versions of OfflineImap concurrently) and will be removed in the + future. Starting with this version, OfflineImap will be + forward-compatible with the per-account locking style. + +* Implement RFC 2595 LOGINDISABLED. Warn the user and abort when we + attempt a plaintext login but the server has explicitly disabled + plaintext logins rather than crashing. + +* Folders will now also be automatically created on the REMOTE side of + an account if they exist on the local side. Use the folderfilters + setting on the local side to prevent some folders from migrating to + the remote side. Also, if you have a nametrans setting on the remote + repository, you might need a nametrans setting on the local repository + that leads to the original name (reverse nametrans). + +Changes +------- + +* Documentation improvements concerning 'restoreatime' and some code cleanup + +* Maildir repositories now also respond to folderfilter= configurations. + +Bug Fixes +--------- + +* New emails are not created with "-rwxr-xr-x" but as "-rw-r--r--" + anymore, fixing a regression in 6.3.4. OfflineIMAP v6.3.5-rc1 (2011-09-12) =================================== diff --git a/offlineimap/__init__.py b/offlineimap/__init__.py index 4eeb648..f5f094c 100644 --- a/offlineimap/__init__.py +++ b/offlineimap/__init__.py @@ -1,7 +1,7 @@ __all__ = ['OfflineImap'] __productname__ = 'OfflineIMAP' -__version__ = "6.3.5-rc1" +__version__ = "6.3.5-rc3" __copyright__ = "Copyright 2002-2011 John Goerzen & contributors" __author__ = "John Goerzen" __author_email__= "john@complete.org" From e30ae53b2a6d8f30fbe536c5425297e3de29754c Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Fri, 23 Sep 2011 14:27:25 +0200 Subject: [PATCH 44/52] Folder: Force top-level folder visiblename to '' nametrans rules can lead to different visiblename names for the top-level directory, specifically both '.' and '' (the latter was recently introduced). However, we need to be able to compare folder names to see if we need to create a new directory or whether a directory already exists, so we need to be able to compare a repositories visiblename (=transposed via nametrans rule) with another folder. To make the top-level directory comparison happen, we enforce a top-level name of '', so that comparisons work. Signed-off-by: Sebastian Spaeth --- offlineimap/folder/Base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/offlineimap/folder/Base.py b/offlineimap/folder/Base.py index d9fed70..909db30 100644 --- a/offlineimap/folder/Base.py +++ b/offlineimap/folder/Base.py @@ -39,6 +39,11 @@ class BaseFolder(object): self.name = name self.repository = repository self.visiblename = repository.nametrans(name) + # In case the visiblename becomes '.' (top-level) we use '' as + # that is the name that e.g. the Maildir scanning will return + # for the top-level dir. + if self.visiblename == '.': + self.visiblename = '' self.config = repository.getconfig() def getname(self): From 3d00a8bb4ad8101661736bb0ef9a2d43212ee84d Mon Sep 17 00:00:00 2001 From: Thomas Kahle Date: Fri, 23 Sep 2011 01:20:36 +0100 Subject: [PATCH 45/52] MANUAL: Improve documentation on using python code in the config Hi, the following patch updates the documentation. It was a bit out of context since Sebastian originally just copied it from my webpage. Also, the code for the helper functions is available there under "Download". I did not include it because it would leave larger chunks of python code in the manual which seems weird. Signed-off-by: Thomas Kahle Signed-off-by: Sebastian Spaeth --- docs/MANUAL.rst | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/MANUAL.rst b/docs/MANUAL.rst index 928ae40..a5a1fb3 100644 --- a/docs/MANUAL.rst +++ b/docs/MANUAL.rst @@ -476,7 +476,8 @@ To only get the All Mail folder from a Gmail account, you would e.g. do:: Another nametrans transpose example ----------------------------------- -Put everything in a GMX. subfolder except for the boxes INBOX, Draft, and Sent which should keep the same name:: +Put everything in a GMX. subfolder except for the boxes INBOX, Draft, +and Sent which should keep the same name:: nametrans: lambda folder: folder if folder in ['INBOX', 'Drafts', 'Sent'] \ else re.sub(r'^', r'GMX.', folder) @@ -484,7 +485,9 @@ Put everything in a GMX. subfolder except for the boxes INBOX, Draft, and Sent w 2 IMAP using name translations ------------------------------ -Synchronizing 2 IMAP accounts to local Maildirs that are "next to each other", so that mutt can work on both. Full email setup described by Thomas Kahle at `http://dev.gentoo.org/~tomka/mail.html`_ +Synchronizing 2 IMAP accounts to local Maildirs that are "next to each +other", so that mutt can work on both. Full email setup described by +Thomas Kahle at `http://dev.gentoo.org/~tomka/mail.html`_ offlineimap.conf:: @@ -534,11 +537,25 @@ offlineimap.conf:: ssl = yes maxconnections = 2 -One of the coolest things about offlineimap is that you can inject arbitrary python code. The file specified with:: +One of the coolest things about offlineimap is that you can call +arbitrary python code from your configuration. To do this, specify a +pythonfile with:: pythonfile=~/bin/offlineimap-helpers.py -contains python functions that I used for two purposes: Fetching passwords from the gnome-keyring and translating folder names on the server to local foldernames. The python file should contain all the functions that are called here. get_username and get_password are part of the interaction with gnome-keyring and not printed here. Find them in the example file that is in the tarball or here. The folderfilter is a lambda term that, well, filters which folders to get. `oimaptransfolder_acc2` translates remote folders into local folders with a very simple logic. The `INBOX` folder will simply have the same name as the account while any other folder will have the account name and a dot as a prefix. offlineimap handles the renaming correctly in both directions:: +Your pythonfile needs to contain implementations for the functions +that you want to use in offflineimaprc. The example uses it for two +purposes: Fetching passwords from the gnome-keyring and translating +folder names on the server to local foldernames. An example +implementation of get_username and get_password showing how to query +gnome-keyring is contained in +`http://dev.gentoo.org/~tomka/mail-setup.tar.bz2`_ The folderfilter is +a lambda term that, well, filters which folders to get. The function +`oimaptransfolder_acc2` translates remote folders into local folders +with a very simple logic. The `INBOX` folder will have the same name +as the account while any other folder will have the account name and a +dot as a prefix. This is useful for hierarchichal display in mutt. +Offlineimap handles the renaming correctly in both directions:: import re def oimaptransfolder_acc1(foldername): From 248f23afd6cf6f96c82a12e49d32ff3b00223f87 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Mon, 26 Sep 2011 15:14:10 +0200 Subject: [PATCH 46/52] Do not fail calling dequote() with empty string A report by Dave Abrahams showed that the dequote() function failed when invoked with an empty string. This fixes the function to be more robust. Signed-off-by: Sebastian Spaeth --- offlineimap/imaputil.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/offlineimap/imaputil.py b/offlineimap/imaputil.py index eae9a76..f583312 100644 --- a/offlineimap/imaputil.py +++ b/offlineimap/imaputil.py @@ -34,16 +34,15 @@ def debug(*args): getglobalui().debug('imap', " ".join(msg)) def dequote(string): - """Takes a string which may or may not be quoted and returns it, unquoted. - This function does NOT consider parenthised lists to be quoted. - """ + """Takes string which may or may not be quoted and unquotes it. - if not (string[0] == '"' and string[-1] == '"'): - return string - string = string[1:-1] # Strip off quotes. - string = string.replace('\\"', '"') - string = string.replace('\\\\', '\\') - debug("dequote() returning:", string) + It only considers double quotes. This function does NOT consider + parenthised lists to be quoted. + """ + if string and string.startswith('"') and string.endswith('"'): + string = string[1:-1] # Strip off the surrounding quotes. + string = string.replace('\\"', '"') + string = string.replace('\\\\', '\\') return string def flagsplit(string): From 1b6d76345a772d53bde806a2ef71c81b2d29b8c9 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Mon, 26 Sep 2011 15:27:04 +0200 Subject: [PATCH 47/52] Robustify error msg against more failure When syncfolder() fails, we output an error message containing the foldername per the localfolder variable. However, the localfolder variable is assigned inside our try: block and when the error occurs there, we will have no localfolder variable to use for output. This caused the errormsg to cause an Exception itself which unhelpfully distracts from the root cause of the error. Reconstruct the folder name in a bit more complex way, but in a way so it is guaranteed to work (by relying on parameters passed in to the function). Signed-off-by: Sebastian Spaeth --- offlineimap/accounts.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/offlineimap/accounts.py b/offlineimap/accounts.py index 13bb12e..d28bd34 100644 --- a/offlineimap/accounts.py +++ b/offlineimap/accounts.py @@ -420,8 +420,15 @@ def syncfolder(accountname, remoterepos, remotefolder, localrepos, if e.severity > OfflineImapError.ERROR.FOLDER: raise else: - ui.error(e, exc_info()[2], msg = "Aborting folder sync '%s' " - "[acc: '%s']" % (localfolder, accountname)) + #if the initial localfolder assignement bailed out, the localfolder var will not be available, so we need + ui.error(e, exc_info()[2], msg = "Aborting sync, folder '%s' " + "[acc: '%s']" % ( + remotefolder.getvisiblename().\ + replace(remoterepos.getsep(), localrepos.getsep()), + accountname)) + # we reconstruct foldername above rather than using + # localfolder, as the localfolder var is not + # available if assignment fails. except Exception, e: ui.error(e, msg = "ERROR in syncfolder for %s folder %s: %s" % \ (accountname,remotefolder.getvisiblename(), From 953c58a9c917f467d7256945e207e72f59227a13 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Mon, 26 Sep 2011 15:57:35 +0200 Subject: [PATCH 48/52] Robustify (&fix) error throwing on APPEND If APPEND raises abort(), the (typ, dat) variables will not be set, so we should not be using it for the OfflineImapError Exception string. Fixing and prettifying the string formatting a bit at the same time. Signed-off-by: Sebastian Spaeth --- offlineimap/folder/IMAP.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/offlineimap/folder/IMAP.py b/offlineimap/folder/IMAP.py index 041f5a0..69b202c 100644 --- a/offlineimap/folder/IMAP.py +++ b/offlineimap/folder/IMAP.py @@ -541,21 +541,18 @@ class IMAPFolder(BaseFolder): imapobj = self.imapserver.acquireconnection() if not retry_left: raise OfflineImapError("Saving msg in folder '%s', " - "repository '%s' failed. Server reponded; %s %s\n" + "repository '%s' failed. Server reponded: %s\n" "Message content was: %s" % - (self, self.getrepository(), - typ, dat, dbg_output), + (self, self.getrepository(), str(e), dbg_output), OfflineImapError.ERROR.MESSAGE) retry_left -= 1 - except imapobj.error, e: - # If the server responds with 'BAD', append() raise()s directly. - # So we need to prepare a response ourselves. - typ, dat = 'BAD', str(e) - if typ != 'OK': #APPEND failed - raise OfflineImapError("Saving msg in folder '%s', repository " - "'%s' failed. Server reponded; %s %s\nMessage content was:" - " %s" % (self, self.getrepository(), typ, dat, dbg_output), - OfflineImapError.ERROR.MESSAGE) + except imapobj.error, e: # APPEND failed + # If the server responds with 'BAD', append() + # raise()s directly. So we catch that too. + raise OfflineImapError("Saving msg folder '%s', repo '%s'" + "failed. Server reponded: %s\nMessage content was: " + "%s" % (self, self.getrepository(), str(e), dbg_output), + OfflineImapError.ERROR.MESSAGE) # Checkpoint. Let it write out stuff, etc. Eg searches for # just uploaded messages won't work if we don't do this. (typ,dat) = imapobj.check() From e145beb394b84f533b1762dc9a1779463a9502cd Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Mon, 26 Sep 2011 16:04:00 +0200 Subject: [PATCH 49/52] Fix "command CHECK illegal in state AUTH" Dave identified a case where our new dropped connection handling did not work out correctly: we use the retry_left variable to signify success (0=success if no exception occured). However, we were decrementing the variable AFTER all the exception checks, so if there was one due to a dropped connection, it could well be that we 1) did not raise an exception (because we want to retry), and 2) then DECREMENTED retry_left, which indicated "all is well, no need to retry". The code then continued to check() the append, which failed with the above message (because we obtained a new connection which had not even selected the current folder and we were still in mode AUTH). The fix is of course, to fix our logic: Decrement retry_left first, THEN decide whether to raise() (retry_left==0) or retry (retry_left>0) which would then correctly attempt another loop. I am sorry for this newbie type of logic error. The retry count loop was too hastily slipped in, it seems. Reported-by: Dave Abrahams Signed-off-by: Sebastian Spaeth --- offlineimap/folder/IMAP.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/offlineimap/folder/IMAP.py b/offlineimap/folder/IMAP.py index 69b202c..00361b0 100644 --- a/offlineimap/folder/IMAP.py +++ b/offlineimap/folder/IMAP.py @@ -536,7 +536,7 @@ class IMAPFolder(BaseFolder): retry_left = 0 # Mark as success except imapobj.abort, e: # connection has been reset, release connection and retry. - self.ui.error(e, exc_info()[2]) + retry_left -= 1 self.imapserver.releaseconnection(imapobj, True) imapobj = self.imapserver.acquireconnection() if not retry_left: @@ -545,7 +545,8 @@ class IMAPFolder(BaseFolder): "Message content was: %s" % (self, self.getrepository(), str(e), dbg_output), OfflineImapError.ERROR.MESSAGE) - retry_left -= 1 + self.ui.error(e, exc_info()[2]) + except imapobj.error, e: # APPEND failed # If the server responds with 'BAD', append() # raise()s directly. So we catch that too. From ae8a1cb79f0027e7ef6e842c8c60cec002dc02b6 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Tue, 27 Sep 2011 12:58:23 +0200 Subject: [PATCH 50/52] Remove deprecated calls to apply() apply() has been deprecated since Python 2.3, and won't be working in python 3 anymore. Use the functional equivalent throughout. Signed-off-by: Sebastian Spaeth --- offlineimap/CustomConfig.py | 13 ++++++------- offlineimap/threadutil.py | 3 +-- offlineimap/ui/Curses.py | 2 +- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/offlineimap/CustomConfig.py b/offlineimap/CustomConfig.py index 42132f8..dae20b6 100644 --- a/offlineimap/CustomConfig.py +++ b/offlineimap/CustomConfig.py @@ -24,26 +24,25 @@ class CustomConfigParser(SafeConfigParser): """Same as config.get, but returns the "default" option if there is no such option specified.""" if self.has_option(section, option): - return apply(self.get, [section, option] + list(args), kwargs) + return self.get(*(section, option) + args, **kwargs) else: return default def getdefaultint(self, section, option, default, *args, **kwargs): if self.has_option(section, option): - return apply(self.getint, [section, option] + list(args), kwargs) + return self.getint (*(section, option) + args, **kwargs) else: return default def getdefaultfloat(self, section, option, default, *args, **kwargs): if self.has_option(section, option): - return apply(self.getfloat, [section, option] + list(args), kwargs) + return self.getfloat(*(section, option) + args, **kwargs) else: return default def getdefaultboolean(self, section, option, default, *args, **kwargs): if self.has_option(section, option): - return apply(self.getboolean, [section, option] + list(args), - kwargs) + return self.getboolean(*(section, option) + args, **kwargs) else: return default @@ -91,9 +90,9 @@ class ConfigHelperMixin: def _confighelper_runner(self, option, default, defaultfunc, mainfunc): """Return config value for getsection()""" if default == CustomConfigDefault: - return apply(mainfunc, [self.getsection(), option]) + return mainfunc(*[self.getsection(), option]) else: - return apply(defaultfunc, [self.getsection(), option, default]) + return defaultfunc(*[self.getsection(), option, default]) def getconf(self, option, diff --git a/offlineimap/threadutil.py b/offlineimap/threadutil.py index bb3b63c..38387e9 100644 --- a/offlineimap/threadutil.py +++ b/offlineimap/threadutil.py @@ -214,8 +214,7 @@ def initInstanceLimit(instancename, instancemax): class InstanceLimitedThread(ExitNotifyThread): def __init__(self, instancename, *args, **kwargs): self.instancename = instancename - - apply(ExitNotifyThread.__init__, (self,) + args, kwargs) + super(InstanceLimitedThread, self).__init__(*args, **kwargs) def start(self): instancelimitedsems[self.instancename].acquire() diff --git a/offlineimap/ui/Curses.py b/offlineimap/ui/Curses.py index afe3acb..84b0e9a 100644 --- a/offlineimap/ui/Curses.py +++ b/offlineimap/ui/Curses.py @@ -66,7 +66,7 @@ class CursesUtil: """Perform an operation with full locking.""" self.lock() try: - apply(target, args, kwargs) + target(*args, **kwargs) finally: self.unlock() From 4bfc1e82275233ff11a45bc01e8fdd3bf5f92319 Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Tue, 27 Sep 2011 14:08:20 +0200 Subject: [PATCH 51/52] except imapobj.abort() -> except imapobj.abort When checking for the IMAP4.abort() exception, we need of course to perform: except imapobj.abort: and not except imapobj.abort(): Thanks to Johannes Stezenbach for pointing to the glitch. Signed-off-by: Sebastian Spaeth --- offlineimap/folder/IMAP.py | 2 +- offlineimap/imapserver.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/offlineimap/folder/IMAP.py b/offlineimap/folder/IMAP.py index 00361b0..8dbd7aa 100644 --- a/offlineimap/folder/IMAP.py +++ b/offlineimap/folder/IMAP.py @@ -209,7 +209,7 @@ class IMAPFolder(BaseFolder): res_type, data = imapobj.uid('fetch', str(uid), '(BODY.PEEK[])') fails_left = 0 - except imapobj.abort(), e: + except imapobj.abort, e: # Release dropped connection, and get a new one self.imapserver.releaseconnection(imapobj, True) imapobj = self.imapserver.acquireconnection() diff --git a/offlineimap/imapserver.py b/offlineimap/imapserver.py index ead3280..ef54e31 100644 --- a/offlineimap/imapserver.py +++ b/offlineimap/imapserver.py @@ -549,7 +549,7 @@ class IdleThread(object): try: # End IDLE mode with noop, imapobj can point to a dropped conn. imapobj.noop() - except imapobj.abort(): + except imapobj.abort: self.ui.warn('Attempting NOOP on dropped connection %s' % \ imapobj.identifier) self.parent.releaseconnection(imapobj, True) From c1120c9158f273fe691f89b9eefc34b2623d5a7a Mon Sep 17 00:00:00 2001 From: Sebastian Spaeth Date: Thu, 29 Sep 2011 11:20:57 +0200 Subject: [PATCH 52/52] 6.4.0 release Signed-off-by: Sebastian Spaeth --- Changelog.rst | 7 +++++++ offlineimap/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Changelog.rst b/Changelog.rst index 05c9a57..b301c20 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -11,6 +11,13 @@ ChangeLog on releases. And because I'm lazy, it will also be used as a draft for the releases announces. +OfflineIMAP v6.4.0 (2011-09-29) +=============================== + +This is the first stable release to support the forward-compatible per-account locks and remote folder creation that has been introduced in the 6.3.5 series. + +* Various regression and bug fixes from the last couple of RCs + OfflineIMAP v6.3.5-rc3 (2011-09-21) =================================== diff --git a/offlineimap/__init__.py b/offlineimap/__init__.py index f5f094c..46ff1d7 100644 --- a/offlineimap/__init__.py +++ b/offlineimap/__init__.py @@ -1,7 +1,7 @@ __all__ = ['OfflineImap'] __productname__ = 'OfflineIMAP' -__version__ = "6.3.5-rc3" +__version__ = "6.4.0" __copyright__ = "Copyright 2002-2011 John Goerzen & contributors" __author__ = "John Goerzen" __author_email__= "john@complete.org"