# Copyright (C) 2003 John Goerzen # # # Portions Copyright (C) 2007 David Favro # # 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 # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA from offlineimap import threadutil, mbnames, CustomConfig import offlineimap.repository.Base, offlineimap.repository.LocalStatus from offlineimap.ui import UIBase from offlineimap.threadutil import InstanceLimitedThread, ExitNotifyThread from threading import Event import os def getaccountlist(customconfig): return customconfig.getsectionlist('Account') def AccountListGenerator(customconfig): return [Account(customconfig, accountname) for accountname in getaccountlist(customconfig)] def AccountHashGenerator(customconfig): retval = {} for item in AccountListGenerator(customconfig): retval[item.getname()] = item return retval mailboxes = [] class Account(CustomConfig.ConfigHelperMixin): def __init__(self, config, name): self.config = config self.name = name self.metadatadir = config.getmetadatadir() self.localeval = config.getlocaleval() self.ui = UIBase.getglobalui() self.refreshperiod = self.getconffloat('autorefresh', 0.0) if self.refreshperiod == 0.0: self.refreshperiod = None def getlocaleval(self): return self.localeval def getconfig(self): return self.config def getname(self): return self.name def getsection(self): return 'Account ' + self.getname() def sleeper(self): """Sleep handler. Returns same value as UIBase.sleep: 0 if timeout expired, 1 if there was a request to cancel the timer, and 2 if there is a request to abort the program. Also, returns 100 if configured to not sleep at all.""" if not self.refreshperiod: return 100 kaobjs = [] if hasattr(self, 'localrepos'): kaobjs.append(self.localrepos) if hasattr(self, 'remoterepos'): kaobjs.append(self.remoterepos) for item in kaobjs: item.startkeepalive() refreshperiod = int(self.refreshperiod * 60) sleepresult = self.ui.sleep(refreshperiod) if sleepresult == 2: # Cancel keep-alive, but don't bother terminating threads for item in kaobjs: item.stopkeepalive(abrupt = 1) return sleepresult else: # Cancel keep-alive and wait for thread to terminate. for item in kaobjs: item.stopkeepalive(abrupt = 0) return sleepresult class AccountSynchronizationMixin: def syncrunner(self): self.ui.registerthread(self.name) self.ui.acct(self.name) accountmetadata = self.getaccountmeta() if not os.path.exists(accountmetadata): os.mkdir(accountmetadata, 0700) self.remoterepos = offlineimap.repository.Base.LoadRepository(self.getconf('remoterepository'), self, 'remote') # Connect to the local repository. self.localrepos = offlineimap.repository.Base.LoadRepository(self.getconf('localrepository'), self, 'local') # Connect to the local cache. self.statusrepos = offlineimap.repository.LocalStatus.LocalStatusRepository(self.getconf('localrepository'), self) if not self.refreshperiod: self.sync() self.ui.acctdone(self.name) return looping = 1 while looping: self.sync() looping = self.sleeper() != 2 self.ui.acctdone(self.name) def getaccountmeta(self): return os.path.join(self.metadatadir, 'Account-' + self.name) def sync(self): # We don't need an account lock because syncitall() goes through # each account once, then waits for all to finish. try: remoterepos = self.remoterepos localrepos = self.localrepos statusrepos = self.statusrepos self.ui.syncfolders(remoterepos, localrepos) remoterepos.syncfoldersto(localrepos) folderthreads = [] for remotefolder in remoterepos.getfolders(): thread = InstanceLimitedThread(\ instancename = 'FOLDER_' + self.remoterepos.getname(), target = syncfolder, name = "Folder sync %s[%s]" % \ (self.name, remotefolder.getvisiblename()), args = (self.name, remoterepos, remotefolder, localrepos, statusrepos)) thread.setDaemon(1) thread.start() folderthreads.append(thread) threadutil.threadsreset(folderthreads) mbnames.write() localrepos.holdordropconnections() remoterepos.holdordropconnections() finally: pass class SyncableAccount(Account, AccountSynchronizationMixin): pass def syncfolder(accountname, remoterepos, remotefolder, localrepos, statusrepos): global mailboxes ui = UIBase.getglobalui() ui.registerthread(accountname) # Load local folder. localfolder = localrepos.\ getfolder(remotefolder.getvisiblename().\ replace(remoterepos.getsep(), localrepos.getsep())) # Write the mailboxes mbnames.add(accountname, localfolder.getvisiblename()) # Load local folder ui.syncingfolder(remoterepos, remotefolder, localrepos, localfolder) ui.loadmessagelist(localrepos, localfolder) localfolder.cachemessagelist() ui.messagelistloaded(localrepos, localfolder, len(localfolder.getmessagelist().keys())) # Load status folder. statusfolder = statusrepos.getfolder(remotefolder.getvisiblename().\ replace(remoterepos.getsep(), statusrepos.getsep())) if localfolder.getuidvalidity() == None: # This is a new folder, so delete the status cache to be sure # we don't have a conflict. statusfolder.deletemessagelist() statusfolder.cachemessagelist() # If either the local or the status folder has messages and there is a UID # validity problem, warn and abort. If there are no messages, UW IMAPd # loses UIDVALIDITY. But we don't really need it if both local folders are # empty. So, in that case, just save it off. if len(localfolder.getmessagelist()) or len(statusfolder.getmessagelist()): if not localfolder.isuidvalidityok(): ui.validityproblem(localfolder) localrepos.restore_atime() return if not remotefolder.isuidvalidityok(): ui.validityproblem(remotefolder) localrepos.restore_atime() return else: localfolder.saveuidvalidity() remotefolder.saveuidvalidity() # Load remote folder. ui.loadmessagelist(remoterepos, remotefolder) remotefolder.cachemessagelist() ui.messagelistloaded(remoterepos, remotefolder, len(remotefolder.getmessagelist().keys())) # if not statusfolder.isnewfolder(): # Delete local copies of remote messages. This way, # if a message's flag is modified locally but it has been # deleted remotely, we'll delete it locally. Otherwise, we # try to modify a deleted message's flags! This step # need only be taken if a statusfolder is present; otherwise, # there is no action taken *to* the remote repository. remotefolder.syncmessagesto_delete(localfolder, [localfolder, statusfolder]) ui.syncingmessages(localrepos, localfolder, remoterepos, remotefolder) localfolder.syncmessagesto(statusfolder, [remotefolder, statusfolder]) # Synchronize remote changes. ui.syncingmessages(remoterepos, remotefolder, localrepos, localfolder) remotefolder.syncmessagesto(localfolder, [localfolder, statusfolder]) # Make sure the status folder is up-to-date. ui.syncingmessages(localrepos, localfolder, statusrepos, statusfolder) localfolder.syncmessagesto(statusfolder) statusfolder.save() localrepos.restore_atime()