diff --git a/offlineimap/head/bin/offlineimap b/offlineimap/head/bin/offlineimap index 9730a55..aecfd95 100644 --- a/offlineimap/head/bin/offlineimap +++ b/offlineimap/head/bin/offlineimap @@ -18,4 +18,4 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from offlineimap import init -init.startup('3.99.10') +init.startup('3.99.12') diff --git a/offlineimap/head/debian/changelog b/offlineimap/head/debian/changelog index 3032b3b..1981f0d 100644 --- a/offlineimap/head/debian/changelog +++ b/offlineimap/head/debian/changelog @@ -1,3 +1,38 @@ +offlineimap (3.99.12) unstable; urgency=low + + * This is a 4.0 TRACK release, and may be unstable or in flux! + * Big news: OfflineIMAP can now sync two remote IMAP servers to each + other, with no need to write a Maildir at all. + * WARNING: the format of the configuration file *AND* the local + status area changes with this release! + * Major reworking of internal management of accounts. Previously, the + account defined a local Maildir and a remote IMAP server. Now, the + account is simply a connection between two repositories. For + traditional ones, an account basically specifies a refresh interval, + a Maildir repository, and an IMAP repository. + * Added a notion of a repository to the configuration file. Repositories + currently available are IMAP and Maildir. Combined with the new account + system, this lets the user define powerful combinations without + duplicating information. + * When uploading messages to an IMAP server, OfflineIMAP generates its + own X-OfflineIMAP header rather than trying to guess the new message UID + based on the Message-Id header. This leads to greater reliability when + uploading messages, especially when dealing with duplicate messages. + This change was required to permit reliable IMAP-to-IMAP syncing, but + helps with regular IMAP-to-Maildir syncing as well. + * Local status area under ~/.offlineimap revamped. It now contains + separate subdirectories for each account and repository, and they + contain UID validity information, UID mapping (for IMAP-to-IMAP + syncing). UID validity information is no longer stored in the Maildir + itself. + * New debug type: "thread" to debug multithreading. + * preauth tunnels no longer require remoteuser, remotepass, host, + or port in the configuration file. + * Logging for preauth tunnels is more sensible. + * Fixed a logic error for syncs with a reference that returns no folders. + + -- John Goerzen Thu, 17 Apr 2003 17:20:08 -0500 + offlineimap (3.99.11) unstable; urgency=low * Curses interface can now be resized. Closes: #176342. diff --git a/offlineimap/head/offlineimap.conf b/offlineimap/head/offlineimap.conf index d925772..efb215c 100644 --- a/offlineimap/head/offlineimap.conf +++ b/offlineimap/head/offlineimap.conf @@ -147,16 +147,52 @@ fontsize = 8 # This is an account definition clause. You'll have one of these # for each account listed in general/accounts above. -[Test] - +[Account Test] ########## Basic settings +# These settings specify the two folders that you will be syncing. +# You'll need to have a "Repository ..." section for each one. + +localrepository = LocalExample +remoterepository = RemoteExample + +########## Advanced settings + +# You can have offlineimap continue running indefinately, automatically +# syncing your mail periodically. If you want that, specify how +# frequently to do that (in minutes) here. + +# autorefresh = 5 + +[Repository LocalExample] + +# This is one of the two repositories that you'll work with given the +# above example. Each repository requires a "type" declaration. +# +# The types supported are Maildir and IMAP. +# + +type = Maildir + # Specify local repository. Your IMAP folders will be synchronized # to maildirs created under this path. OfflineIMAP will create the # maildirs for you as needed. localfolders = ~/Test +# You can specify the "path separator character" used for your Maildir +# folders. This is inserted in-between the components of the tree. +# It defaults to ".". If you want your Maildir folders to be nested, +# set it to "/". + +sep = . + +[Repository RemoteExample] + +# And this is the remote repository. For now, we only support IMAP here. + +type = IMAP + # Specify the remote hostname. remotehost = examplehost @@ -198,13 +234,6 @@ remoteuser = username ########## Advanced settings -# You can have offlineimap continue running indefinately, automatically -# syncing your mail periodically. If you want that, specify how -# frequently to do that (in minutes) here. - -# autorefresh = 5 - - # Some IMAP servers need a "reference" which often refers to the # "folder root". This is most commonly needed with UW IMAP, where # you might need to specify the directory in which your mail is @@ -212,6 +241,40 @@ remoteuser = username # # reference = Mail +# OfflineIMAP can use multiple connections to the server in order +# to perform multiple synchronization actions simultaneously. +# This may place a higher burden on the server. In most cases, +# setting this value to 2 or 3 will speed up the sync, but in some +# cases, it may slow things down. The safe answer is 1. You should +# probably never set it to a value more than 5. + +maxconnections = 1 + +# OfflineIMAP normally closes IMAP server connections between refreshes if +# the global option autorefresh is specified. If you wish it to keep the +# connection open, set this to true. If not specified, the default is +# false. Keeping the connection open means a faster sync start the +# next time and may use fewer server resources on connection, but uses +# more server memory. This setting has no effect if autorefresh is not set. + +holdconnectionopen = no + +# If you want to have "keepalives" sent while waiting between syncs, +# specify the amount of time IN SECONDS between keepalives here. Note that +# sometimes more than this amount of time might pass, so don't make it +# tight. This setting has no effect if autorefresh and holdconnectionopen +# are not both set. + +# keepalive = 60 + +# Normally, OfflineIMAP will expunge deleted messages from the server. +# You can disable that if you wish. This means that OfflineIMAP will +# mark them deleted on the server, but not actually delete them. +# You must use some other IMAP client to delete them if you use this +# setting; otherwise, the messgaes will just pile up there forever. +# Therefore, this setting is definately NOT recommended. +# +# expunge = no # You can specify a folder translator. This must be a eval-able # Python expression that takes a foldername arg and returns the new # value. I suggest a lambda. This example below will remove "INBOX." from @@ -290,44 +353,3 @@ remoteuser = username # # foldersort = lambda x, y: -cmp(x, y) -# OfflineIMAP can use multiple connections to the server in order -# to perform multiple synchronization actions simultaneously. -# This may place a higher burden on the server. In most cases, -# setting this value to 2 or 3 will speed up the sync, but in some -# cases, it may slow things down. The safe answer is 1. You should -# probably never set it to a value more than 5. - -maxconnections = 1 - -# OfflineIMAP normally closes IMAP server connections between refreshes if -# the global option autorefresh is specified. If you wish it to keep the -# connection open, set this to true. If not specified, the default is -# false. Keeping the connection open means a faster sync start the -# next time and may use fewer server resources on connection, but uses -# more server memory. This setting has no effect if autorefresh is not set. - -holdconnectionopen = no - -# If you want to have "keepalives" sent while waiting between syncs, -# specify the amount of time IN SECONDS between keepalives here. Note that -# sometimes more than this amount of time might pass, so don't make it -# tight. This setting has no effect if autorefresh and holdconnectionopen -# are not both set. - -# keepalive = 60 - -# You can specify the "path separator character" used for your Maildir -# folders. This is inserted in-between the components of the tree. -# It defaults to ".". If you want your Maildir folders to be nested, -# set it to "/". - -sep = . - -# Normally, OfflineIMAP will expunge deleted messages from the server. -# You can disable that if you wish. This means that OfflineIMAP will -# mark them deleted on the server, but not actually delete them. -# You must use some other IMAP client to delete them if you use this -# setting; otherwise, the messgaes will just pile up there forever. -# Therefore, this setting is definately NOT recommended. -# -# expunge = no diff --git a/offlineimap/head/offlineimap.conf.minimal b/offlineimap/head/offlineimap.conf.minimal index 50b3d27..4b87ec8 100644 --- a/offlineimap/head/offlineimap.conf.minimal +++ b/offlineimap/head/offlineimap.conf.minimal @@ -4,7 +4,15 @@ [general] accounts = Test -[Test] +[Account Test] +localrepository = Main +remoterepository = Example + +[Repository Main] +type = Maildir localfolders = ~/Test + +[Repository Example] +type = IMAP remotehost = examplehost remoteuser = jgoerzen diff --git a/offlineimap/head/offlineimap.py b/offlineimap/head/offlineimap.py index 6a60c23..511af14 100644 --- a/offlineimap/head/offlineimap.py +++ b/offlineimap/head/offlineimap.py @@ -18,4 +18,4 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from offlineimap import init -init.startup('3.99.10') +init.startup('3.99.12') diff --git a/offlineimap/head/offlineimap.sgml b/offlineimap/head/offlineimap.sgml index 2c9a0a1..87dc127 100644 --- a/offlineimap/head/offlineimap.sgml +++ b/offlineimap/head/offlineimap.sgml @@ -1,4 +1,3 @@ - OfflineIMAP"> ]> @@ -11,7 +10,7 @@
jgoerzen@complete.org
JohnGoerzen - $Date: 2003-04-16 09:23:45 -0500 (Wed, 16 Apr 2003) $ + $Date: 2003-04-17 13:25:30 -0500 (Thu, 17 Apr 2003) $
@@ -333,13 +332,15 @@ cd offlineimap-x.y.z requires one or more debugtypes, separated by commas. These define what exactly will be - debugged, and include two options: imap - and maildir. The imap + debugged, and include three options: imap, + maildir, and thread. + The imap option will enable IMAP protocol stream and parsing debugging. Note that the output may contain passwords, so take care to remove that from the debugging output before sending it to anyone else. The maildir option will enable debugging for - certain Maildir operations. + certain Maildir operations. And thread + will debug the threading model. -o @@ -1032,3 +1033,10 @@ rm -r ~/.offlineimap/AccountName/INBOX + + diff --git a/offlineimap/head/offlineimap/CustomConfig.py b/offlineimap/head/offlineimap/CustomConfig.py index 341ce0e..9e03682 100644 --- a/offlineimap/head/offlineimap/CustomConfig.py +++ b/offlineimap/head/offlineimap/CustomConfig.py @@ -54,6 +54,40 @@ class CustomConfigParser(ConfigParser): path = None return LocalEval(path) - def getaccountlist(self): - return [x for x in self.sections() if x != 'general'] + def getsectionlist(self, key): + """Returns a list of sections that start with key + " ". That is, + if key is "Account", returns all section names that start with + "Account ", but strips off the "Account ". For instance, for + "Account Test", returns "Test".""" + + key = key + ' ' + return [x[len(key):] for x in self.sections() \ + if x.startswith(key)] + +def CustomConfigDefault(): + """Just a sample constant that won't occur anywhere else to use for the + default.""" + pass + +class ConfigHelperMixin: + def _confighelper_runner(self, option, default, defaultfunc, mainfunc): + if default != CustomConfigDefault: + return apply(defaultfunc, [self.getsection(), option, default]) + else: + return apply(mainfunc, [self.getsection(), option]) + + def getconf(self, option, default = CustomConfigDefault): + return self._confighelper_runner(option, default, + self.getconfig().getdefault, + self.getconfig().get) + + def getconfboolean(self, option, default = CustomConfigDefault): + return self._confighelper_runner(option, default, + self.getconfig().getdefaultboolean, + self.getconfig().getboolean) + + def getconfint(self, option, default = CustomConfigDefault): + return self._confighelper_runner(option, default, + self.getconfig().getdefaultint, + self.getconfig().getint) diff --git a/offlineimap/head/offlineimap/accounts.py b/offlineimap/head/offlineimap/accounts.py index 3b84899..168a092 100644 --- a/offlineimap/head/offlineimap/accounts.py +++ b/offlineimap/head/offlineimap/accounts.py @@ -15,39 +15,49 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -from offlineimap import imapserver, repository, threadutil, mbnames +from offlineimap import repository, threadutil, mbnames, CustomConfig 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: +class Account(CustomConfig.ConfigHelperMixin): def __init__(self, config, name): self.config = config self.name = name self.metadatadir = config.getmetadatadir() self.localeval = config.getlocaleval() - self.server = imapserver.ConfigedIMAPServer(config, self.name) self.ui = UIBase.getglobalui() - if self.config.has_option(self.name, 'autorefresh'): - self.refreshperiod = self.config.getint(self.name, 'autorefresh') - else: + self.refreshperiod = self.getconfint('autorefresh', 0) + if self.refreshperiod == 0: self.refreshperiod = None - self.hold = self.config.has_option(self.name, 'holdconnectionopen') \ - and self.config.getboolean(self.name, 'holdconnectionopen') - if self.config.has_option(self.name, 'keepalive'): - self.keepalive = self.config.getint(self.name, 'keepalive') - else: - self.keepalive = None - def getconf(self, option, default = None): - if default != None: - return self.config.get(self.name, option) - else: - return self.config.getdefault(self.name, option, - default) + 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: @@ -58,31 +68,46 @@ class Account: 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 = self.refreshperiod * 60 - if self.keepalive: - kaevent = Event() - kathread = ExitNotifyThread(target = self.server.keepalive, - name = "Keep alive " + self.name, - args = (self.keepalive, kaevent)) - kathread.setDaemon(1) - kathread.start() sleepresult = self.ui.sleep(refreshperiod) if sleepresult == 2: # Cancel keep-alive, but don't bother terminating threads - if self.keepalive: - kaevent.set() + for item in kaobjs: + item.stopkeepalive(abrupt = 1) return sleepresult else: # Cancel keep-alive and wait for thread to terminate. - if self.keepalive: - kaevent.set() - kathread.join() + 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 = repository.Base.LoadRepository(self.getconf('remoterepository'), self, 'remote') + + # Connect to the local repository. + self.localrepos = repository.Base.LoadRepository(self.getconf('localrepository'), self, 'local') + + # Connect to the local cache. + self.statusrepos = repository.LocalStatus.LocalStatusRepository(self.getconf('localrepository'), self) + if not self.refreshperiod: self.sync() self.ui.acctdone(self.name) @@ -93,32 +118,23 @@ class AccountSynchronizationMixin: 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: - accountmetadata = os.path.join(self.metadatadir, self.name) - if not os.path.exists(accountmetadata): - os.mkdir(accountmetadata, 0700) - - remoterepos = repository.IMAP.IMAPRepository(self.config, - self.localeval, - self.name, - self.server) - - # Connect to the Maildirs. - localrepos = repository.Maildir.MaildirRepository(os.path.expanduser(self.config.get(self.name, "localfolders")), self.name, self.config) - - # Connect to the local cache. - statusrepos = repository.LocalStatus.LocalStatusRepository(accountmetadata, self.name) - + 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.name, + instancename = 'FOLDER_' + self.remoterepos.getname(), target = syncfolder, name = "Folder sync %s[%s]" % \ (self.name, remotefolder.getvisiblename()), @@ -129,8 +145,8 @@ class AccountSynchronizationMixin: folderthreads.append(thread) threadutil.threadsreset(folderthreads) mbnames.write() - if not self.hold: - self.server.close() + localrepos.holdordropconnections() + remoterepos.holdordropconnections() finally: pass @@ -166,19 +182,22 @@ def syncfolder(accountname, remoterepos, remotefolder, localrepos, 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, save it off. - if (len(localfolder.getmessagelist()) or \ - len(statusfolder.getmessagelist())) and \ - not localfolder.isuidvalidityok(remotefolder): - ui.validityproblem(remotefolder) - return + # 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, localfolder.getsaveduidvalidity(), + localfolder.getuidvalidity()) + return + if not remotefolder.isuidvalidityok(): + ui.validityproblem(remotefolder, remotefolder.getsaveduidvalidity(), + remotefolder.getuidvalidity()) + return else: - localfolder.saveuidvalidity(remotefolder.getuidvalidity()) + localfolder.saveuidvalidity() + remotefolder.saveuidvalidity() # Load remote folder. ui.loadmessagelist(remoterepos, remotefolder) diff --git a/offlineimap/head/offlineimap/folder/Base.py b/offlineimap/head/offlineimap/folder/Base.py index afc4041..8d1bbae 100644 --- a/offlineimap/head/offlineimap/folder/Base.py +++ b/offlineimap/head/offlineimap/folder/Base.py @@ -20,8 +20,12 @@ from threading import * from offlineimap import threadutil from offlineimap.threadutil import InstanceLimitedThread from offlineimap.ui import UIBase +import os.path, re class BaseFolder: + def __init__(self): + self.uidlock = Lock() + def getname(self): """Returns name""" return self.name @@ -68,15 +72,52 @@ class BaseFolder: else: return self.getname() - def isuidvalidityok(self, remotefolder): - raise NotImplementedException + def getfolderbasename(self): + foldername = self.getname() + foldername = foldername.replace(self.repository.getsep(), '.') + foldername = re.sub('/\.$', '/dot', foldername) + foldername = re.sub('^\.$', 'dot', foldername) + return foldername + + def isuidvalidityok(self): + if self.getsaveduidvalidity() != None: + return self.getsaveduidvalidity() == self.getuidvalidity() + else: + self.saveuidvalidity() + return 1 + + def _getuidfilename(self): + return os.path.join(self.repository.getuiddir(), + self.getfolderbasename()) + + def getsaveduidvalidity(self): + if hasattr(self, '_base_saved_uidvalidity'): + return self._base_saved_uidvalidity + uidfilename = self._getuidfilename() + if not os.path.exists(uidfilename): + self._base_saved_uidvalidity = None + else: + file = open(uidfilename, "rt") + self._base_saved_uidvalidity = long(file.readline().strip()) + file.close() + return self._base_saved_uidvalidity + + def saveuidvalidity(self): + newval = self.getuidvalidity() + uidfilename = self._getuidfilename() + self.uidlock.acquire() + try: + file = open(uidfilename + ".tmp", "wt") + file.write("%d\n" % newval) + file.close() + os.rename(uidfilename + ".tmp", uidfilename) + self._base_saved_uidvalidity = newval + finally: + self.uidlock.release() def getuidvalidity(self): raise NotImplementedException - def saveuidvalidity(self, newval): - raise NotImplementedException - def cachemessagelist(self): """Reads the message list from disk or network and stores it in memory for later use. This list will not be re-read from disk or diff --git a/offlineimap/head/offlineimap/folder/IMAP.py b/offlineimap/head/offlineimap/folder/IMAP.py index 9bb7785..6e04380 100644 --- a/offlineimap/head/offlineimap/folder/IMAP.py +++ b/offlineimap/head/offlineimap/folder/IMAP.py @@ -19,7 +19,7 @@ from Base import BaseFolder from offlineimap import imaputil, imaplib from offlineimap.ui import UIBase -import rfc822, time, string +import rfc822, time, string, random, binascii from StringIO import StringIO from copy import copy @@ -27,9 +27,7 @@ from copy import copy class IMAPFolder(BaseFolder): def __init__(self, imapserver, name, visiblename, accountname, repository): self.config = imapserver.config - self.expunge = 1 - if self.config.has_option(accountname, 'expunge'): - self.expunge = self.config.getboolean(accountname, 'expunge') + self.expunge = repository.getexpunge() self.name = imaputil.dequote(name) self.root = None # imapserver.root self.sep = imapserver.delim @@ -38,6 +36,8 @@ class IMAPFolder(BaseFolder): self.visiblename = visiblename self.accountname = accountname self.repository = repository + self.randomgenerator = random.Random() + BaseFolder.__init__(self) def getaccountname(self): return self.accountname @@ -49,7 +49,7 @@ class IMAPFolder(BaseFolder): self.imapserver.connectionwait() def getcopyinstancelimit(self): - return 'MSGCOPY_' + self.accountname + return 'MSGCOPY_' + self.repository.getname() def getvisiblename(self): return self.visiblename @@ -70,7 +70,12 @@ class IMAPFolder(BaseFolder): try: # Primes untagged_responses imapobj.select(self.getfullname(), readonly = 1) - maxmsgid = long(imapobj.untagged_responses['EXISTS'][0]) + try: + # Some mail servers do not return an EXISTS response if + # the folder is empty. + maxmsgid = long(imapobj.untagged_responses['EXISTS'][0]) + except KeyError: + return if maxmsgid < 1: # No messages; return return @@ -107,7 +112,37 @@ class IMAPFolder(BaseFolder): def getmessageflags(self, uid): return self.messagelist[uid]['flags'] - + + def savemessage_getnewheader(self, content): + headername = 'X-OfflineIMAP-%s-' % str(binascii.crc32(content)).replace('-', 'x') + headername += binascii.hexlify(self.repository.getname()) + '-' + headername += binascii.hexlify(self.getname()) + headervalue= '%d-' % long(time.time()) + headervalue += str(self.randomgenerator.random()).replace('.', '') + return (headername, headervalue) + + def savemessage_addheader(self, content, headername, headervalue): + insertionpoint = content.find("\r\n") + leader = content[0:insertionpoint] + newline = "\r\n%s: %s" % (headername, headervalue) + trailer = content[insertionpoint:] + return leader + newline + trailer + + def savemessage_searchforheader(self, imapobj, headername, headervalue): + # Now find the UID it got. + headervalue = imapobj._quote(headervalue) + try: + matchinguids = imapobj.uid('search', None, + '(HEADER %s %s)' % (headername, headervalue))[1][0] + except imapobj.error: + # IMAP server doesn't implement search or had a problem. + return 0 + matchinguids = matchinguids.split(' ') + if len(matchinguids) != 1 or matchinguids[0] == None: + raise ValueError, "While attempting to find UID for message with header %s, got wrong-sized matchinguids of %s" % (headername, str(matchinguids)) + matchinguids.sort() + return long(matchinguids[0]) + def savemessage(self, uid, content, flags): imapobj = self.imapserver.acquireconnection() try: @@ -123,9 +158,6 @@ class IMAPFolder(BaseFolder): # In order to get the new uid, we need to save off the message ID. message = rfc822.Message(StringIO(content)) - mid = message.getheader('Message-Id') - if mid != None: - mid = imapobj._quote(mid) datetuple = rfc822.parsedate(message.getheader('Date')) # Will be None if missing or not in a valid format. if datetuple == None: @@ -145,35 +177,31 @@ class IMAPFolder(BaseFolder): if content.find("\r\n") == -1: # Convert line endings if not already content = content.replace("\n", "\r\n") + (headername, headervalue) = self.savemessage_getnewheader(content) + content = self.savemessage_addheader(content, headername, + headervalue) + assert(imapobj.append(self.getfullname(), imaputil.flagsmaildir2imap(flags), date, content)[0] == 'OK') + # Checkpoint. Let it write out the messages, etc. assert(imapobj.check()[0] == 'OK') - if mid == None: - # No message ID in original message -- no sense trying to - # search for it. - return 0 - # Now find the UID it got. + + # Keep trying until we get the UID. try: - matchinguids = imapobj.uid('search', None, - '(HEADER Message-Id %s)' % mid)[1][0] - except imapobj.error: - # IMAP server doesn't implement search or had a problem. - return 0 - matchinguids = matchinguids.split(' ') - if len(matchinguids) != 1 or matchinguids[0] == None: - return 0 - matchinguids.sort() - try: - uid = long(matchinguids[-1]) + uid = self.savemessage_searchforheader(imapobj, headername, + headervalue) except ValueError: - return 0 - self.messagelist[uid] = {'uid': uid, 'flags': flags} - return uid + assert(imapobj.noop()[0] == 'OK') + uid = self.savemessage_searchforheader(imapobj, headername, + headervalue) finally: self.imapserver.releaseconnection(imapobj) + self.messagelist[uid] = {'uid': uid, 'flags': flags} + return uid + def savemessageflags(self, uid, flags): imapobj = self.imapserver.acquireconnection() try: @@ -197,9 +225,15 @@ class IMAPFolder(BaseFolder): def addmessageflags(self, uid, flags): self.addmessagesflags([uid], flags) - def addmessagesflags(self, uidlist, flags): + def addmessagesflags_noconvert(self, uidlist, flags): self.processmessagesflags('+', uidlist, flags) + def addmessagesflags(self, uidlist, flags): + """This is here for the sake of UIDMaps.py -- deletemessages must + add flags and get a converted UID, and if we don't have noconvert, + then UIDMaps will try to convert it twice.""" + self.addmessagesflags_noconvert(uidlist, flags) + def deletemessageflags(self, uid, flags): self.deletemessagesflags([uid], flags) @@ -254,15 +288,18 @@ class IMAPFolder(BaseFolder): self.messagelist[uid]['flags'].remove(flag) def deletemessage(self, uid): - self.deletemessages([uid]) + self.deletemessages_noconvert([uid]) def deletemessages(self, uidlist): + self.deletemessages_noconvert(uidlist) + + def deletemessages_noconvert(self, uidlist): # Weed out ones not in self.messagelist uidlist = [uid for uid in uidlist if uid in self.messagelist] if not len(uidlist): return - self.addmessagesflags(uidlist, ['T']) + self.addmessagesflags_noconvert(uidlist, ['T']) imapobj = self.imapserver.acquireconnection() try: try: diff --git a/offlineimap/head/offlineimap/folder/LocalStatus.py b/offlineimap/head/offlineimap/folder/LocalStatus.py index 5370971..195eff6 100644 --- a/offlineimap/head/offlineimap/folder/LocalStatus.py +++ b/offlineimap/head/offlineimap/folder/LocalStatus.py @@ -27,11 +27,13 @@ class LocalStatusFolder(BaseFolder): self.root = root self.sep = '.' self.filename = os.path.join(root, name) + self.filename = repository.getfolderfilename(name) self.messagelist = None self.repository = repository self.savelock = threading.Lock() self.doautosave = 1 self.accountname = accountname + BaseFolder.__init__(self) def getaccountname(self): return self.accountname diff --git a/offlineimap/head/offlineimap/folder/Maildir.py b/offlineimap/head/offlineimap/folder/Maildir.py index 5fa2f2a..71d73fc 100644 --- a/offlineimap/head/offlineimap/folder/Maildir.py +++ b/offlineimap/head/offlineimap/folder/Maildir.py @@ -43,10 +43,10 @@ class MaildirFolder(BaseFolder): self.name = name self.root = root self.sep = sep - self.uidfilename = os.path.join(self.getfullname(), "offlineimap.uidvalidity") self.messagelist = None self.repository = repository self.accountname = accountname + BaseFolder.__init__(self) def getaccountname(self): return self.accountname @@ -55,31 +55,10 @@ class MaildirFolder(BaseFolder): return os.path.join(self.getroot(), self.getname()) def getuidvalidity(self): - if hasattr(self, 'uidvalidity'): - return self.uidvalidity - if not os.path.exists(self.uidfilename): - self.uidvalidity = None - else: - file = open(self.uidfilename, "rt") - self.uidvalidity = long(file.readline().strip()) - file.close() - return self.uidvalidity + """Maildirs have no notion of uidvalidity, so we just return a magic + token.""" + return 42 - def saveuidvalidity(self, newval): - file = open(self.uidfilename + ".tmp", "wt") - file.write("%d\n" % newval) - file.close() - os.rename(self.uidfilename + ".tmp", self.uidfilename) - self.uidvalidity = newval - - def isuidvalidityok(self, remotefolder): - myval = self.getuidvalidity() - if myval != None: - return myval == remotefolder.getuidvalidity() - else: - self.saveuidvalidity(remotefolder.getuidvalidity()) - return 1 - def _scanfolder(self): """Cache the message list. Maildir flags are: R (replied) diff --git a/offlineimap/head/offlineimap/imapserver.py b/offlineimap/head/offlineimap/imapserver.py index 828e875..ca2034a 100644 --- a/offlineimap/head/offlineimap/imapserver.py +++ b/offlineimap/head/offlineimap/imapserver.py @@ -1,5 +1,5 @@ # IMAP server support -# Copyright (C) 2002 John Goerzen +# Copyright (C) 2002, 2003 John Goerzen # # # This program is free software; you can redistribute it and/or modify @@ -48,11 +48,11 @@ class UsefulIMAP4_SSL(UsefulIMAPMixIn, imaplib.IMAP4_SSL): pass class UsefulIMAP4_Tunnel(UsefulIMAPMixIn, imaplib.IMAP4_Tunnel): pass class IMAPServer: - def __init__(self, config, accountname, + def __init__(self, config, reposname, username = None, password = None, hostname = None, port = None, ssl = 1, maxconnections = 1, tunnel = None, reference = '""'): - self.account = accountname + self.reposname = reposname self.config = config self.username = username self.password = password @@ -80,7 +80,8 @@ class IMAPServer: if self.password != None and self.passworderror == None: return self.password - self.password = UIBase.getglobalui().getpass(self.account, self.config, + self.password = UIBase.getglobalui().getpass(self.reposname, + self.config, self.passworderror) self.passworderror = None @@ -152,17 +153,18 @@ class IMAPServer: self.connectionlock.release() # Release until need to modify data - UIBase.getglobalui().connecting(self.hostname, self.port) - success = 0 while not success: # Generate a new connection. if self.tunnel: + UIBase.getglobalui().connecting('tunnel', self.tunnel) imapobj = UsefulIMAP4_Tunnel(self.tunnel) success = 1 elif self.usessl: + UIBase.getglobalui().connecting(self.hostname, self.port) imapobj = UsefulIMAP4_SSL(self.hostname, self.port) else: + UIBase.getglobalui().connecting(self.hostname, self.port) imapobj = UsefulIMAP4(self.hostname, self.port) if not self.tunnel: @@ -258,39 +260,34 @@ class ConfigedIMAPServer(IMAPServer): object and an account name. The passwordhash is used if passwords for certain accounts are known. If the password for this account is listed, it will be obtained from there.""" - def __init__(self, config, accountname, passwordhash = {}): + def __init__(self, repository, passwordhash = {}): """Initialize the object. If the account is not a tunnel, the password is required.""" - host = config.get(accountname, "remotehost") - user = config.get(accountname, "remoteuser") - port = None - if config.has_option(accountname, "remoteport"): - port = config.getint(accountname, "remoteport") - ssl = config.getdefaultboolean(accountname, "ssl", 0) - usetunnel = config.has_option(accountname, "preauthtunnel") - reference = '""' - if config.has_option(accountname, "reference"): - reference = config.get(accountname, "reference") + self.repos = repository + self.config = self.repos.getconfig() + usetunnel = self.repos.getpreauthtunnel() + if not usetunnel: + host = self.repos.gethost() + user = self.repos.getuser() + port = self.repos.getport() + ssl = self.repos.getssl() + reference = self.repos.getreference() server = None password = None - if accountname in passwordhash: - password = passwordhash[accountname] + + if repository.getname() in passwordhash: + password = passwordhash[repository.getname()] # Connect to the remote server. if usetunnel: - IMAPServer.__init__(self, config, accountname, - tunnel = config.get(accountname, "preauthtunnel"), + IMAPServer.__init__(self, self.config, self.repos.getname(), + tunnel = usetunnel, reference = reference, - maxconnections = config.getint(accountname, "maxconnections")) + maxconnections = self.repos.getmaxconnections()) else: if not password: - if config.has_option(accountname, 'remotepass'): - password = config.get(accountname, 'remotepass') - elif config.has_option(accountname, 'remotepassfile'): - passfile = open(os.path.expanduser(config.get(accountname, "remotepassfile"))) - password = passfile.readline().strip() - passfile.close() - IMAPServer.__init__(self, config, accountname, + password = self.repos.getpassword() + IMAPServer.__init__(self, self.config, self.repos.getname(), user, password, host, port, ssl, - config.getdefaultint(accountname, "maxconnections", 1), + self.repos.getmaxconnections(), reference = reference) diff --git a/offlineimap/head/offlineimap/init.py b/offlineimap/head/offlineimap/init.py index 4faffd9..2820019 100644 --- a/offlineimap/head/offlineimap/init.py +++ b/offlineimap/head/offlineimap/init.py @@ -1,5 +1,5 @@ # OfflineIMAP initialization code -# Copyright (C) 2002 John Goerzen +# Copyright (C) 2002, 2003 John Goerzen # # # This program is free software; you can redistribute it and/or modify @@ -16,13 +16,14 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -from offlineimap import imaplib, imapserver, repository, folder, mbnames, threadutil, version, syncmaster +from offlineimap import imaplib, imapserver, repository, folder, mbnames, threadutil, version, syncmaster, accounts from offlineimap.localeval import LocalEval from offlineimap.threadutil import InstanceLimitedThread, ExitNotifyThread from offlineimap.ui import UIBase import re, os, os.path, offlineimap, sys, fcntl from offlineimap.CustomConfig import CustomConfigParser from threading import * +import threading from getopt import getopt lockfd = None @@ -78,18 +79,26 @@ def startup(versionno): ui.add_debug(debugtype.strip()) if debugtype == 'imap': imaplib.Debug = 5 + if debugtype == 'thread': + threading._VERBOSE = 1 if '-o' in options: - for section in config.getaccountlist(): - config.remove_option(section, "autorefresh") + # FIXME: maybe need a better + for section in accounts.getaccountlist(config): + config.remove_option('Account ' + section, "autorefresh") lock(config, ui) - accounts = config.get("general", "accounts") + activeaccounts = config.get("general", "accounts") if '-a' in options: - accounts = options['-a'] - accounts = accounts.replace(" ", "") - accounts = accounts.split(",") + activeaccounts = options['-a'] + activeaccounts = activeaccounts.replace(" ", "") + activeaccounts = activeaccounts.split(",") + allaccounts = accounts.AccountHashGenerator(config) + + syncaccounts = {} + for account in activeaccounts: + syncaccounts[account] = allaccounts[account] server = None remoterepos = None @@ -101,18 +110,19 @@ def startup(versionno): threadutil.initInstanceLimit("ACCOUNTLIMIT", config.getdefaultint("general", "maxsyncaccounts", 1)) - for account in accounts: - for instancename in ["FOLDER_" + account, "MSGCOPY_" + account]: + for reposname in config.getsectionlist('Repository'): + for instancename in ["FOLDER_" + reposname, + "MSGCOPY_" + reposname]: if '-1' in options: threadutil.initInstanceLimit(instancename, 1) else: threadutil.initInstanceLimit(instancename, - config.getdefaultint(account, "maxconnections", 1)) + config.getdefaultint('Repository ' + reposname, "maxconnections", 1)) threadutil.initexitnotify() t = ExitNotifyThread(target=syncmaster.syncitall, name='Sync Runner', - kwargs = {'accounts': accounts, + kwargs = {'accounts': syncaccounts, 'config': config}) t.setDaemon(1) t.start() diff --git a/offlineimap/head/offlineimap/repository/Base.py b/offlineimap/head/offlineimap/repository/Base.py index 5a46042..cfca83e 100644 --- a/offlineimap/head/offlineimap/repository/Base.py +++ b/offlineimap/head/offlineimap/repository/Base.py @@ -1,5 +1,5 @@ # Base repository support -# Copyright (C) 2002 John Goerzen +# Copyright (C) 2002, 2003 John Goerzen # # # This program is free software; you can redistribute it and/or modify @@ -16,7 +16,71 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -class BaseRepository: +from offlineimap import CustomConfig +import os.path + +def LoadRepository(name, account, reqtype): + from offlineimap.repository.IMAP import IMAPRepository, MappedIMAPRepository + from offlineimap.repository.Maildir import MaildirRepository + if reqtype == 'remote': + # For now, we don't support Maildirs on the remote side. + typemap = {'IMAP': IMAPRepository} + elif reqtype == 'local': + typemap = {'IMAP': MappedIMAPRepository, + 'Maildir': MaildirRepository} + else: + raise ValueError, "Request type %s not supported" % reqtype + config = account.getconfig() + repostype = config.get('Repository ' + name, 'type').strip() + return typemap[repostype](name, account) + +class BaseRepository(CustomConfig.ConfigHelperMixin): + def __init__(self, reposname, account): + self.account = account + self.config = account.getconfig() + self.name = reposname + self.localeval = account.getlocaleval() + 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) + self.mapdir = os.path.join(self.uiddir, 'UIDMapping') + if not os.path.exists(self.mapdir): + os.mkdir(self.mapdir, 0700) + self.uiddir = os.path.join(self.uiddir, 'FolderValidity') + if not os.path.exists(self.uiddir): + os.mkdir(self.uiddir, 0700) + + def holdordropconnections(self): + pass + + def dropconnections(self): + pass + + def getaccount(self): + return self.account + + def getname(self): + return self.name + + 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 + + def getconfig(self): + return self.config + + def getlocaleval(self): + return self.account.getlocaleval() + def getfolders(self): """Returns a list of ALL folders on this server.""" return [] @@ -68,3 +132,14 @@ class BaseRepository: # if not key in srchash: # dest.deletefolder(key) + ##### Keepalive + + def startkeepalive(self): + """The default implementation will do nothing.""" + pass + + def stopkeepalive(self, abrupt = 0): + """Stop keep alive. If abrupt is 1, stop it but don't bother waiting + for the threads to terminate.""" + pass + diff --git a/offlineimap/head/offlineimap/repository/IMAP.py b/offlineimap/head/offlineimap/repository/IMAP.py index 24671b4..7ff24e8 100644 --- a/offlineimap/head/offlineimap/repository/IMAP.py +++ b/offlineimap/head/offlineimap/repository/IMAP.py @@ -17,38 +17,116 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from Base import BaseRepository -from offlineimap import folder, imaputil +from offlineimap import folder, imaputil, imapserver +from offlineimap.folder.UIDMaps import MappedIMAPFolder +from offlineimap.threadutil import ExitNotifyThread import re, types from threading import * class IMAPRepository(BaseRepository): - def __init__(self, config, localeval, accountname, imapserver): - """Initialize an IMAPRepository object. Takes an IMAPServer - object.""" - self.imapserver = imapserver - self.config = config - self.accountname = accountname + def __init__(self, reposname, account): + """Initialize an IMAPRepository object.""" + BaseRepository.__init__(self, reposname, account) + self.imapserver = imapserver.ConfigedIMAPServer(self) self.folders = None self.nametrans = lambda foldername: foldername self.folderfilter = lambda foldername: 1 self.folderincludes = [] self.foldersort = cmp - if config.has_option(accountname, 'nametrans'): - self.nametrans = localeval.eval(config.get(accountname, 'nametrans'), {'re': re}) - if config.has_option(accountname, 'folderfilter'): - self.folderfilter = localeval.eval(config.get(accountname, 'folderfilter'), {'re': re}) - if config.has_option(accountname, 'folderincludes'): - self.folderincludes = localeval.eval(config.get(accountname, 'folderincludes'), {'re': re}) - if config.has_option(accountname, 'foldersort'): - self.foldersort = localeval.eval(config.get(accountname, 'foldersort'), {'re': re}) + 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() + if not keepalivetime: return + self.kaevent = Event() + self.kathread = ExitNotifyThread(target = self.imapserver.keepalive, + name = "Keep alive " + self.getname(), + args = (keepalivetime, self.kaevent)) + self.kathread.setDaemon(1) + self.kathread.start() + + def stopkeepalive(self, abrupt = 0): + if not hasattr(self, 'kaevent'): + # Keepalive is not active. + return + + self.kaevent.set() + if not abrupt: + self.kathread.join() + del self.kathread + del self.kaevent + + def holdordropconnections(self): + if not self.getholdconnectionopen(): + self.dropconnections() + + def dropconnections(self): + self.imapserver.close() + + def getholdconnectionopen(self): + return self.getconfboolean("holdconnectionopen", 0) + + def getkeepalive(self): + return self.getconfint("keepalive", 0) def getsep(self): return self.imapserver.delim + def gethost(self): + return self.getconf('remotehost') + + def getuser(self): + return self.getconf('remoteuser') + + def getport(self): + return self.getconfint('remoteport', None) + + def getssl(self): + return self.getconfboolean('ssl', 0) + + def getpreauthtunnel(self): + return self.getconf('preauthtunnel', None) + + def getreference(self): + return self.getconf('reference', '""') + + def getmaxconnections(self): + return self.getconfint('maxconnections', 1) + + def getexpunge(self): + return self.getconfboolean('expunge', 1) + + def getpassword(self): + password = self.getconf('remotepass', None) + if password != None: + return password + passfile = self.getconf('remotepassfile', None) + if passfile != None: + fd = open(os.path.expanduser(passfile)) + password = passfile.readline().strip() + passfile.close() + return password + return None + def getfolder(self, foldername): - return folder.IMAP.IMAPFolder(self.imapserver, foldername, - self.nametrans(foldername), - accountname, self) + return self.getfoldertype()(self.imapserver, foldername, + self.nametrans(foldername), + self.accountname, self) + + def getfoldertype(self): + return folder.IMAP.IMAPFolder def getfolders(self): if self.folders != None: @@ -60,7 +138,8 @@ class IMAPRepository(BaseRepository): finally: self.imapserver.releaseconnection(imapobj) for string in listresult: - if type(string) == types.StringType and string == '': + if string == None or \ + (type(string) == types.StringType and string == ''): # Bug in imaplib: empty strings in results from # literals. continue @@ -71,13 +150,31 @@ class IMAPRepository(BaseRepository): foldername = imaputil.dequote(name) if not self.folderfilter(foldername): continue - retval.append(folder.IMAP.IMAPFolder(self.imapserver, foldername, - self.nametrans(foldername), - self.accountname, self)) + retval.append(self.getfoldertype()(self.imapserver, foldername, + self.nametrans(foldername), + self.accountname, self)) for foldername in self.folderincludes: - retval.append(folder.IMAP.IMAPFolder(self.imapserver, foldername, - self.nametrans(foldername), - self.accountname, self)) + retval.append(self.getfoldertype()(self.imapserver, foldername, + self.nametrans(foldername), + self.accountname, self)) retval.sort(lambda x, y: self.foldersort(x.getvisiblename(), y.getvisiblename())) self.folders = retval return retval + + def makefolder(self, foldername): + #if self.getreference() != '""': + # newname = self.getreference() + self.getsep() + foldername + #else: + # newname = foldername + newname = foldername + imapobj = self.imapserver.acquireconnection() + try: + result = imapobj.create(newname) + if result[0] != 'OK': + raise RuntimeError, "Repository %s could not create folder %s: %s" % (self.getname(), foldername, str(result)) + finally: + self.imapserver.releaseconnection(imapobj) + +class MappedIMAPRepository(IMAPRepository): + def getfoldertype(self): + return MappedIMAPFolder diff --git a/offlineimap/head/offlineimap/repository/LocalStatus.py b/offlineimap/head/offlineimap/repository/LocalStatus.py index 840c09e..45ec047 100644 --- a/offlineimap/head/offlineimap/repository/LocalStatus.py +++ b/offlineimap/head/offlineimap/repository/LocalStatus.py @@ -18,18 +18,22 @@ from Base import BaseRepository from offlineimap import folder -import os +import os, re class LocalStatusRepository(BaseRepository): - def __init__(self, directory, accountname): - self.directory = directory + def __init__(self, reposname, account): + BaseRepository.__init__(self, reposname, account) + self.directory = os.path.join(account.getaccountmeta(), 'LocalStatus') + if not os.path.exists(self.directory): + os.mkdir(self.directory, 0700) self.folders = None - self.accountname = accountname def getsep(self): return '.' def getfolderfilename(self, foldername): + foldername = re.sub('/\.$', '/dot', foldername) + foldername = re.sub('^\.$', 'dot', foldername) return os.path.join(self.directory, foldername) def makefolder(self, foldername): diff --git a/offlineimap/head/offlineimap/repository/Maildir.py b/offlineimap/head/offlineimap/repository/Maildir.py index 066486f..268b70a 100644 --- a/offlineimap/head/offlineimap/repository/Maildir.py +++ b/offlineimap/head/offlineimap/repository/Maildir.py @@ -23,25 +23,24 @@ from mailbox import Maildir import os class MaildirRepository(BaseRepository): - def __init__(self, root, accountname, config): + def __init__(self, reposname, account): """Initialize a MaildirRepository object. Takes a path name to the directory holding all the Maildir directories.""" + BaseRepository.__init__(self, reposname, account) - self.root = root + self.root = self.getlocalroot() self.folders = None - self.accountname = accountname - self.config = config self.ui = UIBase.getglobalui() self.debug("MaildirRepository initialized, sep is " + repr(self.getsep())) + def getlocalroot(self): + return os.path.expanduser(self.getconf('localfolders')) + def debug(self, msg): self.ui.debug('maildir', msg) def getsep(self): - if self.config.has_option(self.accountname, 'sep'): - return self.config.get(self.accountname, 'sep').strip() - else: - return '.' + return self.getconf('sep', '.').strip() def makefolder(self, foldername): self.debug("makefolder called with arg " + repr(foldername)) @@ -65,7 +64,8 @@ class MaildirRepository(BaseRepository): # makedirs will fail because the higher-up dir already exists. # So, check to see if this is indeed the case. - if self.getsep() == '/' and os.path.isdir(foldername): + if (self.getsep() == '/' or self.getconfboolean('existsok', 0)) \ + and os.path.isdir(foldername): self.debug("makefolder: %s already is a directory" % foldername) # Already exists. Sanity-check that it's not a Maildir. for subdir in ['cur', 'new', 'tmp']: diff --git a/offlineimap/head/offlineimap/syncmaster.py b/offlineimap/head/offlineimap/syncmaster.py index 4c720aa..7249733 100644 --- a/offlineimap/head/offlineimap/syncmaster.py +++ b/offlineimap/head/offlineimap/syncmaster.py @@ -1,5 +1,5 @@ # OfflineIMAP synchronization master code -# Copyright (C) 2002 John Goerzen +# Copyright (C) 2002, 2003 John Goerzen # # # This program is free software; you can redistribute it and/or modify @@ -25,21 +25,16 @@ import re, os, os.path, offlineimap, sys from ConfigParser import ConfigParser from threading import * -def syncaccount(threads, config, accountname): +def syncaccount(config, accountname): account = SyncableAccount(config, accountname) thread = InstanceLimitedThread(instancename = 'ACCOUNTLIMIT', target = account.syncrunner, name = "Account sync %s" % accountname) thread.setDaemon(1) thread.start() - threads.add(thread) def syncitall(accounts, config): - currentThread().setExitMessage('SYNC_WITH_TIMER_TERMINATE') ui = UIBase.getglobalui() - threads = threadutil.threadlist() mbnames.init(config, accounts) for accountname in accounts: - syncaccount(threads, config, accountname) - # Wait for the threads to finish. - threads.reset() + syncaccount(config, accountname) diff --git a/offlineimap/head/offlineimap/threadutil.py b/offlineimap/head/offlineimap/threadutil.py index 3e01718..096034a 100644 --- a/offlineimap/head/offlineimap/threadutil.py +++ b/offlineimap/head/offlineimap/threadutil.py @@ -1,4 +1,4 @@ -# Copyright (C) 2002 John Goerzen +# Copyright (C) 2002, 2003 John Goerzen # Thread support module # # @@ -113,12 +113,14 @@ def exitnotifymonitorloop(callback): global exitcondition, exitthreads while 1: # Loop forever. exitcondition.acquire() - while not len(exitthreads): - exitcondition.wait(1) + try: + while not len(exitthreads): + exitcondition.wait(1) - while len(exitthreads): - callback(exitthreads.pop(0)) # Pull off in order added! - exitcondition.release() + while len(exitthreads): + callback(exitthreads.pop(0)) # Pull off in order added! + finally: + exitcondition.release() def threadexited(thread): """Called when a thread exits.""" @@ -132,11 +134,6 @@ def threadexited(thread): ui.threadException(thread) # Expected to terminate sys.exit(100) # Just in case... os._exit(100) - elif thread.getExitMessage() == 'SYNC_WITH_TIMER_TERMINATE': - ui.terminate() - # Just in case... - sys.exit(100) - os._exit(100) else: ui.threadExited(thread) diff --git a/offlineimap/head/offlineimap/ui/UIBase.py b/offlineimap/head/offlineimap/ui/UIBase.py index 0eae1a2..083d21d 100644 --- a/offlineimap/head/offlineimap/ui/UIBase.py +++ b/offlineimap/head/offlineimap/ui/UIBase.py @@ -155,7 +155,7 @@ class UIBase: if hostname == None: hostname = '' if port != None: - port = ":%d" % port + port = ":%s" % str(port) displaystr = ' to %s%s.' % (hostname, port) if hostname == '' and port == None: displaystr = '.' @@ -182,9 +182,9 @@ class UIBase: s.getnicename(srcrepos), s.getnicename(destrepos))) - def validityproblem(s, folder): - s.warn("UID validity problem for folder %s; skipping it" % \ - folder.getname()) + def validityproblem(s, folder, saved, new): + s.warn("UID validity problem for folder %s (saved %d; got %d); skipping it" % \ + (folder.getname(), saved, new)) def loadmessagelist(s, repos, folder): if s.verbose > 0: diff --git a/offlineimap/head/offlineimap/version.py b/offlineimap/head/offlineimap/version.py index 83afa19..9a642d3 100644 --- a/offlineimap/head/offlineimap/version.py +++ b/offlineimap/head/offlineimap/version.py @@ -1,8 +1,8 @@ productname = 'OfflineIMAP' -versionstr = "3.99.10" -revno = long('$Rev: 367 $'[6:-2]) +versionstr = "3.99.12" +revno = long('$Rev: 439 $'[6:-2]) revstr = "Rev %d" % revno -datestr = '$Date: 2003-04-16 09:23:45 -0500 (Wed, 16 Apr 2003) $' +datestr = '$Date: 2003-04-17 16:16:00 -0500 (Thu, 17 Apr 2003) $' versionlist = versionstr.split(".") major = versionlist[0]