/offlineimap/head: changeset 297

Changed to a more account-centric behavior. The refresh time is now a
per-account variable. Implemented new account classes. User interfaces
must now be updated to take advantage of this.
This commit is contained in:
jgoerzen
2003-01-04 05:57:46 +01:00
parent 1691cdbf0f
commit 854eaf3055
11 changed files with 789 additions and 204 deletions

View File

@ -0,0 +1,46 @@
# Copyright (C) 2003 John Goerzen
# <jgoerzen@complete.org>
#
# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
from ConfigParser import ConfigParser
from offlineimap.localeval import LocalEval
import os
class CustomConfigParser(ConfigParser):
def getdefault(self, section, option, default, *args, **kwargs):
"""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)
else:
return default
def getmetadatadir(self):
metadatadir = os.path.expanduser(self.getdefault("general", "metadata", "~/.offlineimap"))
if not os.path.exists(metadatadir):
os.mkdir(metadatadir, 0700)
return metadatadir
def getlocaleval(self):
if self.has_option("general", "pythonfile"):
path = os.path.expanduser(self.get("general", "pythonfile"))
else:
path = None
return LocalEval(path)
def getaccountlist(self):
return [x for x in self.sections() if x != 'general']

View File

@ -0,0 +1,211 @@
# Copyright (C) 2003 John Goerzen
# <jgoerzen@complete.org>
#
# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
from offlineimap import imapserver, repository, threadutil
from offlineimap.ui import UIBase
from offlineimap.threadutil import InstanceLimitedThread, ExitNotifyThread
from threading import Event
import os
mailboxes = []
class Account:
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 = 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 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
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()
return sleepresult
else:
# Cancel keep-alive and wait for thread to terminate.
if self.keepalive:
kaevent.set()
kathread.join()
return sleepresult
class AccountSynchronizationMixin:
def syncrunner(self):
self.ui.acct(self.name)
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 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.ui.syncfolders(remoterepos, localrepos)
remoterepos.syncfoldersto(localrepos)
folderthreads = []
for remotefolder in remoterepos.getfolders():
thread = InstanceLimitedThread(\
instancename = 'FOLDER_' + self.name,
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)
if not self.hold:
server.close()
finally:
pass
class SyncableAccount(Account, AccountSynchronizationMixin):
pass
def syncfolder(accountname, remoterepos, remotefolder, localrepos,
statusrepos):
global mailboxes
ui = UIBase.getglobalui()
# Load local folder.
localfolder = localrepos.\
getfolder(remotefolder.getvisiblename().\
replace(remoterepos.getsep(), localrepos.getsep()))
# Write the mailboxes
mailboxes.append({'accountname': accountname,
'foldername': 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, save it off.
if (len(localfolder.getmessagelist()) or \
len(statusfolder.getmessagelist())) and \
not localfolder.isuidvalidityok(remotefolder):
ui.validityproblem(remotefolder)
return
else:
localfolder.saveuidvalidity(remotefolder.getuidvalidity())
# 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)
# Make sure the status folder is up-to-date.
ui.syncingmessages(localrepos, localfolder, statusrepos, statusfolder)
localfolder.syncmessagesto(statusfolder)
statusfolder.save()

View File

@ -21,7 +21,7 @@ from offlineimap.localeval import LocalEval
from offlineimap.threadutil import InstanceLimitedThread, ExitNotifyThread
from offlineimap.ui import UIBase
import re, os, os.path, offlineimap, sys
from ConfigParser import ConfigParser
from offlineimap.CustomConfig import CustomConfigParser
from threading import *
from getopt import getopt
@ -51,20 +51,14 @@ def startup(versionno):
threadutil.setprofiledir(profiledir)
sys.stderr.write("WARNING: profile mode engaged;\nPotentially large data will be created in " + profiledir + "\n")
config = ConfigParser()
config = CustomConfigParser()
if not os.path.exists(configfilename):
sys.stderr.write(" *** Config file %s does not exist; aborting!\n" % configfilename)
sys.exit(1)
config.read(configfilename)
if config.has_option("general", "pythonfile"):
path=os.path.expanduser(config.get("general", "pythonfile"))
else:
path=None
localeval = LocalEval(path)
ui = offlineimap.ui.detector.findUI(config, localeval, options.get('-u'))
ui = offlineimap.ui.detector.findUI(config, options.get('-u'))
ui.init_banner()
UIBase.setglobalui(ui)
@ -74,12 +68,9 @@ def startup(versionno):
if debugtype == 'imap':
imaplib.Debug = 5
if '-o' in options and config.has_option("general", "autorefresh"):
config.remove_option("general", "autorefresh")
metadatadir = os.path.expanduser(config.get("general", "metadata"))
if not os.path.exists(metadatadir):
os.mkdir(metadatadir, 0700)
if '-o' in options:
for section in config.getaccountlist():
config.remove_option(section, "autorefresh")
accounts = config.get("general", "accounts")
if '-a' in options:
@ -105,17 +96,11 @@ def startup(versionno):
threadutil.initInstanceLimit(instancename,
config.getint(account, "maxconnections"))
mailboxes = []
servers = {}
threadutil.initexitnotify()
t = ExitNotifyThread(target=syncmaster.sync_with_timer,
t = ExitNotifyThread(target=syncmaster.syncitall,
name='Sync Runner',
kwargs = {'accounts': accounts,
'metadatadir': metadatadir,
'servers': servers,
'config': config,
'localeval': localeval})
'config': config})
t.setDaemon(1)
t.start()
try:

View File

@ -19,9 +19,10 @@
import os.path
import re # for folderfilter
def genmbnames(config, localeval, boxlist):
def genmbnames(config, boxlist):
"""Takes a configparser object and a boxlist, which is a list of hashes
containing 'accountname' and 'foldername' keys."""
localeval = config.getlocaleval()
if not config.getboolean("mbnames", "enabled"):
return
file = open(os.path.expanduser(config.get("mbnames", "filename")), "wt")

View File

@ -18,187 +18,29 @@
from offlineimap import imaplib, imapserver, repository, folder, mbnames, threadutil, version
from offlineimap.threadutil import InstanceLimitedThread, ExitNotifyThread
import offlineimap.accounts
from offlineimap.accounts import SyncableAccount
from offlineimap.ui import UIBase
import re, os, os.path, offlineimap, sys
from ConfigParser import ConfigParser
from threading import *
def syncaccount(accountname, metadatadir, servers, config,
localeval, *args):
ui = UIBase.getglobalui()
# We don't need an account lock because syncitall() goes through
# each account once, then waits for all to finish.
try:
ui.acct(accountname)
accountmetadata = os.path.join(metadatadir, accountname)
if not os.path.exists(accountmetadata):
os.mkdir(accountmetadata, 0700)
server = None
if accountname in servers:
server = servers[accountname]
else:
server = imapserver.ConfigedIMAPServer(config, accountname)
servers[accountname] = server
remoterepos = repository.IMAP.IMAPRepository(config, localeval, accountname, server)
# Connect to the Maildirs.
localrepos = repository.Maildir.MaildirRepository(os.path.expanduser(config.get(accountname, "localfolders")), accountname, config)
# Connect to the local cache.
statusrepos = repository.LocalStatus.LocalStatusRepository(accountmetadata)
ui.syncfolders(remoterepos, localrepos)
remoterepos.syncfoldersto(localrepos)
ui.acct(accountname)
folderthreads = []
for remotefolder in remoterepos.getfolders():
thread = InstanceLimitedThread(\
instancename = 'FOLDER_' + accountname,
target = syncfolder,
name = "Folder sync %s[%s]" % \
(accountname, remotefolder.getvisiblename()),
args = (accountname, remoterepos, remotefolder, localrepos,
statusrepos))
thread.setDaemon(1)
thread.start()
folderthreads.append(thread)
threadutil.threadsreset(folderthreads)
if not (config.has_option(accountname, 'holdconnectionopen') and \
config.getboolean(accountname, 'holdconnectionopen')):
server.close()
finally:
pass
def syncfolder(accountname, remoterepos, remotefolder, localrepos,
statusrepos):
ui = UIBase.getglobalui()
# Load local folder.
localfolder = localrepos.\
getfolder(remotefolder.getvisiblename().\
replace(remoterepos.getsep(), localrepos.getsep()))
# Write the mailboxes
mailboxes.append({'accountname': accountname,
'foldername': 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()
def syncaccount(threads, 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)
# 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
else:
localfolder.saveuidvalidity(remotefolder.getuidvalidity())
# 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)
# Make sure the status folder is up-to-date.
ui.syncingmessages(localrepos, localfolder, statusrepos, statusfolder)
localfolder.syncmessagesto(statusfolder)
statusfolder.save()
def syncitall(accounts, metadatadir, servers, config, localeval):
ui = UIBase.getglobalui()
global mailboxes
mailboxes = [] # Reset.
threads = []
for accountname in accounts:
thread = InstanceLimitedThread(instancename = 'ACCOUNTLIMIT',
target = syncaccount,
name = "Account sync %s" % accountname,
args = (accountname, metadatadir,
servers, config,
localeval))
thread.setDaemon(1)
thread.start()
threads.append(thread)
# Wait for the threads to finish.
threadutil.threadsreset(threads)
mbnames.genmbnames(config, localeval, mailboxes)
def sync_with_timer(accounts, metadatadir, servers, config,
localeval):
ui = UIBase.getglobalui()
def syncitall(accounts, config):
currentThread().setExitMessage('SYNC_WITH_TIMER_TERMINATE')
syncitall(accounts, metadatadir, servers, config, localeval)
if config.has_option('general', 'autorefresh'):
refreshperiod = config.getint('general', 'autorefresh') * 60
while 1:
# Set up keep-alives.
kaevents = {}
kathreads = {}
for accountname in accounts:
if config.has_option(accountname, 'holdconnectionopen') and \
config.getboolean(accountname, 'holdconnectionopen') and \
config.has_option(accountname, 'keepalive'):
event = Event()
kaevents[accountname] = event
thread = ExitNotifyThread(target = servers[accountname].keepalive,
name = "Keep alive " + accountname,
args = (config.getint(accountname, 'keepalive'), event))
thread.setDaemon(1)
thread.start()
kathreads[accountname] = thread
if ui.sleep(refreshperiod) == 2:
# Cancel keep-alives, but don't bother terminating threads
for event in kaevents.values():
event.set()
break
else:
# Cancel keep-alives and wait for threads to terminate.
for event in kaevents.values():
event.set()
for thread in kathreads.values():
thread.join()
syncitall(accounts, metadatadir, servers, config,
localeval)
ui = UIBase.getglobalui()
threads = threadutil.threadlist()
offlineimap.accounts.mailboxes = [] # Reset.
for accountname in accounts:
syncaccount(threads, config, accountname)
# Wait for the threads to finish.
threads.reset()
mbnames.genmbnames(config, offlineimap.accounts.mailboxes)

View File

@ -0,0 +1,17 @@
#!/usr/bin/python2.2 -i
import hmac
def getpassword():
return 'tanstaaftanstaaf'
def md5handler(response):
challenge = response.strip()
print "challenge is", challenge
msg = getpassword()
reply = hmac.new(challenge, msg)
retval = 'tim' + ' ' + \
reply.hexdigest()
while len(retval) < 64:
retval += "\0"
print "md5handler returning", retval
return retval

View File

@ -48,6 +48,42 @@ def threadsreset(threadlist):
for thr in threadlist:
thr.join()
class threadlist:
def __init__(self):
self.lock = Lock()
self.list = []
def add(self, thread):
self.lock.acquire()
try:
self.list.append(thread)
finally:
self.lock.release()
def remove(self, thread):
self.lock.acquire()
try:
self.list.remove(thread)
finally:
self.lock.release()
def pop(self):
self.lock.acquire()
try:
if not len(self.list):
return None
return self.list.pop()
finally:
self.lock.release()
def reset(self):
while 1:
thread = self.pop()
if not thread:
return
thread.join()
######################################################################
# Exit-notify threads
######################################################################

View File

@ -20,7 +20,7 @@ import sys, time
from UIBase import UIBase
class Basic(UIBase):
def getpass(s, accountname, config):
def getpass(s, accountname, config, errmsg = None):
raise NotImplementedError, "Prompting for a password is not supported in noninteractive mode."
def _msg(s, msg):

View File

@ -140,6 +140,10 @@ class UIBase:
if s.verbose >= 0:
s._msg("***** Processing account %s" % accountname)
def acctdone(s, accountname):
if s.verbose >= 0:
s._msg("***** Finished processing account " + accountname)
def syncfolders(s, srcrepos, destrepos):
if s.verbose >= 0:
s._msg("Copying folder structure from %s to %s" % \

View File

@ -19,7 +19,7 @@
import offlineimap.ui
import sys
def findUI(config, localeval, chosenUI=None):
def findUI(config, chosenUI=None):
uistrlist = ['Tk.Blinkenlights', 'Tk.VerboseUI', 'TTY.TTYUI',
'Noninteractive.Basic', 'Noninteractive.Quiet']
namespace={}
@ -34,7 +34,7 @@ def findUI(config, localeval, chosenUI=None):
uistrlist = config.get("general", "ui").replace(" ", "").split(",")
for uistr in uistrlist:
uimod = getUImod(uistr, localeval, namespace)
uimod = getUImod(uistr, config.getlocaleval(), namespace)
if uimod:
uiinstance = uimod(config)
if uiinstance.isusable():