Step 2 of SVN to arch tree conversion
This commit is contained in:
104
offlineimap/CustomConfig.py
Normal file
104
offlineimap/CustomConfig.py
Normal file
@ -0,0 +1,104 @@
|
||||
# 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 getdefaultint(self, section, option, default, *args, **kwargs):
|
||||
if self.has_option(section, option):
|
||||
return apply(self.getint, [section, option] + list(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)
|
||||
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)
|
||||
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 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)
|
||||
|
||||
def getconffloat(self, option, default = CustomConfigDefault):
|
||||
return self._confighelper_runner(option, default,
|
||||
self.getconfig().getdefaultfloat,
|
||||
self.getconfig().getfloat)
|
||||
|
3
offlineimap/__init__.py
Normal file
3
offlineimap/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
__all__ = ['ui', 'folder', 'repository', 'mbnames', 'threadutil', 'init']
|
||||
|
||||
|
233
offlineimap/accounts.py
Normal file
233
offlineimap/accounts.py
Normal file
@ -0,0 +1,233 @@
|
||||
# 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 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, localfolder.getsaveduidvalidity(),
|
||||
localfolder.getuidvalidity())
|
||||
return
|
||||
if not remotefolder.isuidvalidityok():
|
||||
ui.validityproblem(remotefolder, remotefolder.getsaveduidvalidity(),
|
||||
remotefolder.getuidvalidity())
|
||||
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()
|
||||
|
390
offlineimap/folder/Base.py
Normal file
390
offlineimap/folder/Base.py
Normal file
@ -0,0 +1,390 @@
|
||||
# Base folder support
|
||||
# Copyright (C) 2002 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 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
|
||||
|
||||
def suggeststhreads(self):
|
||||
"""Returns true if this folder suggests using threads for actions;
|
||||
false otherwise. Probably only IMAP will return true."""
|
||||
return 0
|
||||
|
||||
def waitforthread(self):
|
||||
"""For threading folders, waits until there is a resource available
|
||||
before firing off a thread. For all others, returns immediately."""
|
||||
pass
|
||||
|
||||
def getcopyinstancelimit(self):
|
||||
"""For threading folders, returns the instancelimitname for
|
||||
InstanceLimitedThreads."""
|
||||
raise NotImplementedException
|
||||
|
||||
def storesmessages(self):
|
||||
"""Should be true for any backend that actually saves message bodies.
|
||||
(Almost all of them). False for the LocalStatus backend. Saves
|
||||
us from having to slurp up messages just for localstatus purposes."""
|
||||
return 1
|
||||
|
||||
def getvisiblename(self):
|
||||
return self.name
|
||||
|
||||
def getrepository(self):
|
||||
"""Returns the repository object that this folder is within."""
|
||||
return self.repository
|
||||
|
||||
def getroot(self):
|
||||
"""Returns the root of the folder, in a folder-specific fashion."""
|
||||
return self.root
|
||||
|
||||
def getsep(self):
|
||||
"""Returns the separator for this folder type."""
|
||||
return self.sep
|
||||
|
||||
def getfullname(self):
|
||||
if self.getroot():
|
||||
return self.getroot() + self.getsep() + self.getname()
|
||||
else:
|
||||
return self.getname()
|
||||
|
||||
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 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
|
||||
memory unless this function is called again."""
|
||||
raise NotImplementedException
|
||||
|
||||
def getmessagelist(self):
|
||||
"""Gets the current message list.
|
||||
You must call cachemessagelist() before calling this function!"""
|
||||
raise NotImplementedException
|
||||
|
||||
def getmessage(self, uid):
|
||||
"""Returns the content of the specified message."""
|
||||
raise NotImplementedException
|
||||
|
||||
def savemessage(self, uid, content, flags):
|
||||
"""Writes a new message, with the specified uid.
|
||||
If the uid is < 0, the backend should assign a new uid and return it.
|
||||
|
||||
If the backend cannot assign a new uid, it returns the uid passed in
|
||||
WITHOUT saving the message.
|
||||
|
||||
If the backend CAN assign a new uid, but cannot find out what this UID
|
||||
is (as is the case with many IMAP servers), it returns 0 but DOES save
|
||||
the message.
|
||||
|
||||
IMAP backend should be the only one that can assign a new uid.
|
||||
|
||||
If the uid is > 0, the backend should set the uid to this, if it can.
|
||||
If it cannot set the uid to that, it will save it anyway.
|
||||
It will return the uid assigned in any case.
|
||||
"""
|
||||
raise NotImplementedException
|
||||
|
||||
def getmessageflags(self, uid):
|
||||
"""Returns the flags for the specified message."""
|
||||
raise NotImplementedException
|
||||
|
||||
def savemessageflags(self, uid, flags):
|
||||
"""Sets the specified message's flags to the given set."""
|
||||
raise NotImplementedException
|
||||
|
||||
def addmessageflags(self, uid, flags):
|
||||
"""Adds the specified flags to the message's flag set. If a given
|
||||
flag is already present, it will not be duplicated."""
|
||||
newflags = self.getmessageflags(uid)
|
||||
for flag in flags:
|
||||
if not flag in newflags:
|
||||
newflags.append(flag)
|
||||
newflags.sort()
|
||||
self.savemessageflags(uid, newflags)
|
||||
|
||||
def addmessagesflags(self, uidlist, flags):
|
||||
for uid in uidlist:
|
||||
self.addmessageflags(uid, flags)
|
||||
|
||||
def deletemessageflags(self, uid, flags):
|
||||
"""Removes each flag given from the message's flag set. If a given
|
||||
flag is already removed, no action will be taken for that flag."""
|
||||
newflags = self.getmessageflags(uid)
|
||||
for flag in flags:
|
||||
if flag in newflags:
|
||||
newflags.remove(flag)
|
||||
newflags.sort()
|
||||
self.savemessageflags(uid, newflags)
|
||||
|
||||
def deletemessagesflags(self, uidlist, flags):
|
||||
for uid in uidlist:
|
||||
self.deletemessageflags(uid, flags)
|
||||
|
||||
def deletemessage(self, uid):
|
||||
raise NotImplementedException
|
||||
|
||||
def deletemessages(self, uidlist):
|
||||
for uid in uidlist:
|
||||
self.deletemessage(uid)
|
||||
|
||||
def syncmessagesto_neguid_msg(self, uid, dest, applyto, register = 1):
|
||||
if register:
|
||||
UIBase.getglobalui().registerthread(self.getaccountname())
|
||||
UIBase.getglobalui().copyingmessage(uid, self, applyto)
|
||||
successobject = None
|
||||
successuid = None
|
||||
message = self.getmessage(uid)
|
||||
flags = self.getmessageflags(uid)
|
||||
for tryappend in applyto:
|
||||
successuid = tryappend.savemessage(uid, message, flags)
|
||||
if successuid >= 0:
|
||||
successobject = tryappend
|
||||
break
|
||||
# Did we succeed?
|
||||
if successobject != None:
|
||||
if successuid: # Only if IMAP actually assigned a UID
|
||||
# Copy the message to the other remote servers.
|
||||
for appendserver in \
|
||||
[x for x in applyto if x != successobject]:
|
||||
appendserver.savemessage(successuid, message, flags)
|
||||
# Copy to its new name on the local server and delete
|
||||
# the one without a UID.
|
||||
self.savemessage(successuid, message, flags)
|
||||
self.deletemessage(uid) # It'll be re-downloaded.
|
||||
else:
|
||||
# Did not find any server to take this message. Ignore.
|
||||
pass
|
||||
|
||||
|
||||
def syncmessagesto_neguid(self, dest, applyto):
|
||||
"""Pass 1 of folder synchronization.
|
||||
|
||||
Look for messages in self with a negative uid. These are messages in
|
||||
Maildirs that were not added by us. Try to add them to the dests,
|
||||
and once that succeeds, get the UID, add it to the others for real,
|
||||
add it to local for real, and delete the fake one."""
|
||||
|
||||
uidlist = [uid for uid in self.getmessagelist().keys() if uid < 0]
|
||||
threads = []
|
||||
|
||||
usethread = None
|
||||
if applyto != None:
|
||||
usethread = applyto[0]
|
||||
|
||||
for uid in uidlist:
|
||||
if usethread and usethread.suggeststhreads():
|
||||
usethread.waitforthread()
|
||||
thread = InstanceLimitedThread(\
|
||||
usethread.getcopyinstancelimit(),
|
||||
target = self.syncmessagesto_neguid_msg,
|
||||
name = "New msg sync from %s" % self.getvisiblename(),
|
||||
args = (uid, dest, applyto))
|
||||
thread.setDaemon(1)
|
||||
thread.start()
|
||||
threads.append(thread)
|
||||
else:
|
||||
self.syncmessagesto_neguid_msg(uid, dest, applyto, register = 0)
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
def copymessageto(self, uid, applyto, register = 1):
|
||||
# Sometimes, it could be the case that if a sync takes awhile,
|
||||
# a message might be deleted from the maildir before it can be
|
||||
# synced to the status cache. This is only a problem with
|
||||
# self.getmessage(). So, don't call self.getmessage unless
|
||||
# really needed.
|
||||
if register:
|
||||
UIBase.getglobalui().registerthread(self.getaccountname())
|
||||
UIBase.getglobalui().copyingmessage(uid, self, applyto)
|
||||
message = ''
|
||||
# If any of the destinations actually stores the message body,
|
||||
# load it up.
|
||||
for object in applyto:
|
||||
if object.storesmessages():
|
||||
message = self.getmessage(uid)
|
||||
break
|
||||
flags = self.getmessageflags(uid)
|
||||
for object in applyto:
|
||||
newuid = object.savemessage(uid, message, flags)
|
||||
if newuid > 0 and newuid != uid:
|
||||
# Change the local uid.
|
||||
self.savemessage(newuid, message, flags)
|
||||
self.deletemessage(uid)
|
||||
uid = newuid
|
||||
|
||||
|
||||
def syncmessagesto_copy(self, dest, applyto):
|
||||
"""Pass 2 of folder synchronization.
|
||||
|
||||
Look for messages present in self but not in dest. If any, add
|
||||
them to dest."""
|
||||
threads = []
|
||||
|
||||
for uid in self.getmessagelist().keys():
|
||||
if uid < 0: # Ignore messages that pass 1 missed.
|
||||
continue
|
||||
if not uid in dest.getmessagelist():
|
||||
if self.suggeststhreads():
|
||||
self.waitforthread()
|
||||
thread = InstanceLimitedThread(\
|
||||
self.getcopyinstancelimit(),
|
||||
target = self.copymessageto,
|
||||
name = "Copy message %d from %s" % (uid,
|
||||
self.getvisiblename()),
|
||||
args = (uid, applyto))
|
||||
thread.setDaemon(1)
|
||||
thread.start()
|
||||
threads.append(thread)
|
||||
else:
|
||||
self.copymessageto(uid, applyto, register = 0)
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
def syncmessagesto_delete(self, dest, applyto):
|
||||
"""Pass 3 of folder synchronization.
|
||||
|
||||
Look for message present in dest but not in self.
|
||||
If any, delete them."""
|
||||
deletelist = []
|
||||
for uid in dest.getmessagelist().keys():
|
||||
if uid < 0:
|
||||
continue
|
||||
if not uid in self.getmessagelist():
|
||||
deletelist.append(uid)
|
||||
if len(deletelist):
|
||||
UIBase.getglobalui().deletingmessages(deletelist, applyto)
|
||||
for object in applyto:
|
||||
object.deletemessages(deletelist)
|
||||
|
||||
def syncmessagesto_flags(self, dest, applyto):
|
||||
"""Pass 4 of folder synchronization.
|
||||
|
||||
Look for any flag matching issues -- set dest message to have the
|
||||
same flags that we have."""
|
||||
|
||||
# As an optimization over previous versions, we store up which flags
|
||||
# are being used for an add or a delete. For each flag, we store
|
||||
# a list of uids to which it should be added. Then, we can call
|
||||
# addmessagesflags() to apply them in bulk, rather than one
|
||||
# call per message as before. This should result in some significant
|
||||
# performance improvements.
|
||||
|
||||
addflaglist = {}
|
||||
delflaglist = {}
|
||||
|
||||
for uid in self.getmessagelist().keys():
|
||||
if uid < 0: # Ignore messages missed by pass 1
|
||||
continue
|
||||
selfflags = self.getmessageflags(uid)
|
||||
destflags = dest.getmessageflags(uid)
|
||||
|
||||
addflags = [x for x in selfflags if x not in destflags]
|
||||
|
||||
for flag in addflags:
|
||||
if not flag in addflaglist:
|
||||
addflaglist[flag] = []
|
||||
addflaglist[flag].append(uid)
|
||||
|
||||
delflags = [x for x in destflags if x not in selfflags]
|
||||
for flag in delflags:
|
||||
if not flag in delflaglist:
|
||||
delflaglist[flag] = []
|
||||
delflaglist[flag].append(uid)
|
||||
|
||||
for object in applyto:
|
||||
for flag in addflaglist.keys():
|
||||
UIBase.getglobalui().addingflags(addflaglist[flag], flag, [object])
|
||||
object.addmessagesflags(addflaglist[flag], [flag])
|
||||
for flag in delflaglist.keys():
|
||||
UIBase.getglobalui().deletingflags(delflaglist[flag], flag, [object])
|
||||
object.deletemessagesflags(delflaglist[flag], [flag])
|
||||
|
||||
def syncmessagesto(self, dest, applyto = None):
|
||||
"""Syncs messages in this folder to the destination.
|
||||
If applyto is specified, it should be a list of folders (don't forget
|
||||
to include dest!) to which all write actions should be applied.
|
||||
It defaults to [dest] if not specified. It is important that
|
||||
the UID generator be listed first in applyto; that is, the other
|
||||
applyto ones should be the ones that "copy" the main action."""
|
||||
if applyto == None:
|
||||
applyto = [dest]
|
||||
|
||||
self.syncmessagesto_neguid(dest, applyto)
|
||||
self.syncmessagesto_copy(dest, applyto)
|
||||
self.syncmessagesto_delete(dest, applyto)
|
||||
|
||||
# Now, the message lists should be identical wrt the uids present.
|
||||
# (except for potential negative uids that couldn't be placed
|
||||
# anywhere)
|
||||
|
||||
self.syncmessagesto_flags(dest, applyto)
|
||||
|
||||
|
358
offlineimap/folder/IMAP.py
Normal file
358
offlineimap/folder/IMAP.py
Normal file
@ -0,0 +1,358 @@
|
||||
# IMAP folder support
|
||||
# Copyright (C) 2002-2004 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 Base import BaseFolder
|
||||
from offlineimap import imaputil, imaplib
|
||||
from offlineimap.ui import UIBase
|
||||
from offlineimap.version import versionstr
|
||||
import rfc822, time, string, random, binascii, re
|
||||
from StringIO import StringIO
|
||||
from copy import copy
|
||||
|
||||
|
||||
class IMAPFolder(BaseFolder):
|
||||
def __init__(self, imapserver, name, visiblename, accountname, 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)
|
||||
|
||||
def getaccountname(self):
|
||||
return self.accountname
|
||||
|
||||
def suggeststhreads(self):
|
||||
return 1
|
||||
|
||||
def waitforthread(self):
|
||||
self.imapserver.connectionwait()
|
||||
|
||||
def getcopyinstancelimit(self):
|
||||
return 'MSGCOPY_' + self.repository.getname()
|
||||
|
||||
def getvisiblename(self):
|
||||
return self.visiblename
|
||||
|
||||
def getuidvalidity(self):
|
||||
imapobj = self.imapserver.acquireconnection()
|
||||
try:
|
||||
# Primes untagged_responses
|
||||
imapobj.select(self.getfullname(), readonly = 1)
|
||||
return long(imapobj.untagged_responses['UIDVALIDITY'][0])
|
||||
finally:
|
||||
self.imapserver.releaseconnection(imapobj)
|
||||
|
||||
def cachemessagelist(self):
|
||||
imapobj = self.imapserver.acquireconnection()
|
||||
self.messagelist = {}
|
||||
|
||||
try:
|
||||
# Primes untagged_responses
|
||||
imapobj.select(self.getfullname(), readonly = 1, force = 1)
|
||||
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
|
||||
|
||||
# Now, get the flags and UIDs for these.
|
||||
# We could conceivably get rid of maxmsgid and just say
|
||||
# '1:*' here.
|
||||
response = imapobj.fetch('1:%d' % maxmsgid, '(FLAGS UID)')[1]
|
||||
finally:
|
||||
self.imapserver.releaseconnection(imapobj)
|
||||
for messagestr in response:
|
||||
# Discard the message number.
|
||||
messagestr = string.split(messagestr, maxsplit = 1)[1]
|
||||
options = imaputil.flags2hash(messagestr)
|
||||
if not options.has_key('UID'):
|
||||
UIBase.getglobalui().warn('No UID in message with options %s' %\
|
||||
str(options),
|
||||
minor = 1)
|
||||
else:
|
||||
uid = long(options['UID'])
|
||||
flags = imaputil.flagsimap2maildir(options['FLAGS'])
|
||||
self.messagelist[uid] = {'uid': uid, 'flags': flags}
|
||||
|
||||
def getmessagelist(self):
|
||||
return self.messagelist
|
||||
|
||||
def getmessage(self, uid):
|
||||
ui = UIBase.getglobalui()
|
||||
imapobj = self.imapserver.acquireconnection()
|
||||
try:
|
||||
imapobj.select(self.getfullname(), readonly = 1)
|
||||
initialresult = imapobj.uid('fetch', '%d' % uid, '(BODY.PEEK[])')
|
||||
ui.debug('imap', 'Returned object from fetching %d: %s' % \
|
||||
(uid, str(initialresult)))
|
||||
return initialresult[1][0][1].replace("\r\n", "\n")
|
||||
|
||||
finally:
|
||||
self.imapserver.releaseconnection(imapobj)
|
||||
|
||||
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('.', '')
|
||||
headervalue += '-v' + versionstr
|
||||
return (headername, headervalue)
|
||||
|
||||
def savemessage_addheader(self, content, headername, headervalue):
|
||||
ui = UIBase.getglobalui()
|
||||
ui.debug('imap',
|
||||
'savemessage_addheader: called to add %s: %s' % (headername,
|
||||
headervalue))
|
||||
insertionpoint = content.find("\r\n")
|
||||
ui.debug('imap', 'savemessage_addheader: insertionpoint = %d' % insertionpoint)
|
||||
leader = content[0:insertionpoint]
|
||||
ui.debug('imap', 'savemessage_addheader: leader = %s' % repr(leader))
|
||||
if insertionpoint == 0 or insertionpoint == -1:
|
||||
newline = ''
|
||||
insertionpoint = 0
|
||||
else:
|
||||
newline = "\r\n"
|
||||
newline += "%s: %s" % (headername, headervalue)
|
||||
ui.debug('imap', 'savemessage_addheader: newline = ' + repr(newline))
|
||||
trailer = content[insertionpoint:]
|
||||
ui.debug('imap', 'savemessage_addheader: trailer = ' + repr(trailer))
|
||||
return leader + newline + trailer
|
||||
|
||||
def savemessage_searchforheader(self, imapobj, headername, headervalue):
|
||||
if imapobj.untagged_responses.has_key('APPENDUID'):
|
||||
return long(imapobj.untagged_responses['APPENDUID'][0].split(' ')[1])
|
||||
|
||||
ui = UIBase.getglobalui()
|
||||
ui.debug('imap', 'savemessage_searchforheader called for %s: %s' % \
|
||||
(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
|
||||
ui.debug('imap', 'savemessage_searchforheader got initial matchinguids: ' + repr(matchinguids))
|
||||
|
||||
matchinguids = matchinguids.split(' ')
|
||||
ui.debug('imap', 'savemessage_searchforheader: matchinguids now ' + \
|
||||
repr(matchinguids))
|
||||
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()
|
||||
ui = UIBase.getglobalui()
|
||||
ui.debug('imap', 'savemessage: called')
|
||||
try:
|
||||
try:
|
||||
imapobj.select(self.getfullname()) # Needed for search
|
||||
except imapobj.readonly:
|
||||
ui.msgtoreadonly(self, uid, content, flags)
|
||||
# Return indicating message taken, but no UID assigned.
|
||||
# Fudge it.
|
||||
return 0
|
||||
|
||||
# This backend always assigns a new uid, so the uid arg is ignored.
|
||||
# In order to get the new uid, we need to save off the message ID.
|
||||
|
||||
message = rfc822.Message(StringIO(content))
|
||||
datetuple = rfc822.parsedate(message.getheader('Date'))
|
||||
# Will be None if missing or not in a valid format.
|
||||
if datetuple == None:
|
||||
datetuple = time.localtime()
|
||||
try:
|
||||
if datetuple[0] < 1981:
|
||||
raise ValueError
|
||||
# This could raise a value error if it's not a valid format.
|
||||
date = imaplib.Time2Internaldate(datetuple)
|
||||
except ValueError:
|
||||
# Argh, sometimes it's a valid format but year is 0102
|
||||
# or something. Argh. It seems that Time2Internaldate
|
||||
# will rause a ValueError if the year is 0102 but not 1902,
|
||||
# but some IMAP servers nonetheless choke on 1902.
|
||||
date = imaplib.Time2Internaldate(time.localtime())
|
||||
|
||||
ui.debug('imap', 'savemessage: using date ' + str(date))
|
||||
content = re.sub("(?<!\r)\n", "\r\n", content)
|
||||
ui.debug('imap', 'savemessage: initial content is: ' + repr(content))
|
||||
|
||||
(headername, headervalue) = self.savemessage_getnewheader(content)
|
||||
ui.debug('imap', 'savemessage: new headers are: %s: %s' % \
|
||||
(headername, headervalue))
|
||||
content = self.savemessage_addheader(content, headername,
|
||||
headervalue)
|
||||
ui.debug('imap', 'savemessage: new content is: ' + repr(content))
|
||||
ui.debug('imap', 'savemessage: new content length is ' + \
|
||||
str(len(content)))
|
||||
|
||||
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')
|
||||
|
||||
# Keep trying until we get the UID.
|
||||
try:
|
||||
ui.debug('imap', 'savemessage: first attempt to get new UID')
|
||||
uid = self.savemessage_searchforheader(imapobj, headername,
|
||||
headervalue)
|
||||
except ValueError:
|
||||
ui.debug('imap', 'savemessage: first attempt to get new UID failed. Going to run a NOOP and try again.')
|
||||
assert(imapobj.noop()[0] == 'OK')
|
||||
uid = self.savemessage_searchforheader(imapobj, headername,
|
||||
headervalue)
|
||||
finally:
|
||||
self.imapserver.releaseconnection(imapobj)
|
||||
|
||||
self.messagelist[uid] = {'uid': uid, 'flags': flags}
|
||||
ui.debug('imap', 'savemessage: returning %d' % uid)
|
||||
return uid
|
||||
|
||||
def savemessageflags(self, uid, flags):
|
||||
imapobj = self.imapserver.acquireconnection()
|
||||
try:
|
||||
try:
|
||||
imapobj.select(self.getfullname())
|
||||
except imapobj.readonly:
|
||||
UIBase.getglobalui().flagstoreadonly(self, [uid], flags)
|
||||
return
|
||||
result = imapobj.uid('store', '%d' % uid, 'FLAGS',
|
||||
imaputil.flagsmaildir2imap(flags))
|
||||
assert result[0] == 'OK', 'Error with store: ' + r[1]
|
||||
finally:
|
||||
self.imapserver.releaseconnection(imapobj)
|
||||
result = result[1][0]
|
||||
if not result:
|
||||
self.messagelist[uid]['flags'] = flags
|
||||
else:
|
||||
flags = imaputil.flags2hash(imaputil.imapsplit(result)[1])['FLAGS']
|
||||
self.messagelist[uid]['flags'] = imaputil.flagsimap2maildir(flags)
|
||||
|
||||
def addmessageflags(self, uid, flags):
|
||||
self.addmessagesflags([uid], 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)
|
||||
|
||||
def deletemessagesflags(self, uidlist, flags):
|
||||
self.processmessagesflags('-', uidlist, flags)
|
||||
|
||||
def processmessagesflags(self, operation, uidlist, flags):
|
||||
imapobj = self.imapserver.acquireconnection()
|
||||
try:
|
||||
try:
|
||||
imapobj.select(self.getfullname())
|
||||
except imapobj.readonly:
|
||||
UIBase.getglobalui().flagstoreadonly(self, uidlist, flags)
|
||||
return
|
||||
r = imapobj.uid('store',
|
||||
imaputil.listjoin(uidlist),
|
||||
operation + 'FLAGS',
|
||||
imaputil.flagsmaildir2imap(flags))
|
||||
assert r[0] == 'OK', 'Error with store: ' + r[1]
|
||||
r = r[1]
|
||||
finally:
|
||||
self.imapserver.releaseconnection(imapobj)
|
||||
# Some IMAP servers do not always return a result. Therefore,
|
||||
# only update the ones that it talks about, and manually fix
|
||||
# the others.
|
||||
needupdate = copy(uidlist)
|
||||
for result in r:
|
||||
if result == None:
|
||||
# Compensate for servers that don't return anything from
|
||||
# STORE.
|
||||
continue
|
||||
attributehash = imaputil.flags2hash(imaputil.imapsplit(result)[1])
|
||||
if not ('UID' in attributehash and 'FLAGS' in attributehash):
|
||||
# Compensate for servers that don't return a UID attribute.
|
||||
continue
|
||||
flags = attributehash['FLAGS']
|
||||
uid = long(attributehash['UID'])
|
||||
self.messagelist[uid]['flags'] = imaputil.flagsimap2maildir(flags)
|
||||
try:
|
||||
needupdate.remove(uid)
|
||||
except ValueError: # Let it slide if it's not in the list
|
||||
pass
|
||||
for uid in needupdate:
|
||||
if operation == '+':
|
||||
for flag in flags:
|
||||
if not flag in self.messagelist[uid]['flags']:
|
||||
self.messagelist[uid]['flags'].append(flag)
|
||||
self.messagelist[uid]['flags'].sort()
|
||||
elif operation == '-':
|
||||
for flag in flags:
|
||||
if flag in self.messagelist[uid]['flags']:
|
||||
self.messagelist[uid]['flags'].remove(flag)
|
||||
|
||||
def deletemessage(self, 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_noconvert(uidlist, ['T'])
|
||||
imapobj = self.imapserver.acquireconnection()
|
||||
try:
|
||||
try:
|
||||
imapobj.select(self.getfullname())
|
||||
except imapobj.readonly:
|
||||
UIBase.getglobalui().deletereadonly(self, uidlist)
|
||||
return
|
||||
if self.expunge:
|
||||
assert(imapobj.expunge()[0] == 'OK')
|
||||
finally:
|
||||
self.imapserver.releaseconnection(imapobj)
|
||||
for uid in uidlist:
|
||||
del self.messagelist[uid]
|
||||
|
||||
|
125
offlineimap/folder/LocalStatus.py
Normal file
125
offlineimap/folder/LocalStatus.py
Normal file
@ -0,0 +1,125 @@
|
||||
# Local status cache virtual folder
|
||||
# Copyright (C) 2002 - 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 Base import BaseFolder
|
||||
import os, threading
|
||||
|
||||
magicline = "OFFLINEIMAP LocalStatus CACHE DATA - DO NOT MODIFY - FORMAT 1"
|
||||
|
||||
class LocalStatusFolder(BaseFolder):
|
||||
def __init__(self, root, name, repository, accountname):
|
||||
self.name = name
|
||||
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
|
||||
|
||||
def storesmessages(self):
|
||||
return 0
|
||||
|
||||
def isnewfolder(self):
|
||||
return not os.path.exists(self.filename)
|
||||
|
||||
def getname(self):
|
||||
return self.name
|
||||
|
||||
def getroot(self):
|
||||
return self.root
|
||||
|
||||
def getsep(self):
|
||||
return self.sep
|
||||
|
||||
def getfullname(self):
|
||||
return self.filename
|
||||
|
||||
def deletemessagelist(self):
|
||||
if not self.isnewfolder():
|
||||
os.unlink(self.filename)
|
||||
|
||||
def cachemessagelist(self):
|
||||
if self.isnewfolder():
|
||||
self.messagelist = {}
|
||||
return
|
||||
file = open(self.filename, "rt")
|
||||
self.messagelist = {}
|
||||
line = file.readline().strip()
|
||||
assert(line == magicline)
|
||||
for line in file.xreadlines():
|
||||
line = line.strip()
|
||||
uid, flags = line.split(':')
|
||||
uid = long(uid)
|
||||
flags = [x for x in flags]
|
||||
self.messagelist[uid] = {'uid': uid, 'flags': flags}
|
||||
file.close()
|
||||
|
||||
def autosave(self):
|
||||
if self.doautosave:
|
||||
self.save()
|
||||
|
||||
def save(self):
|
||||
self.savelock.acquire()
|
||||
try:
|
||||
file = open(self.filename + ".tmp", "wt")
|
||||
file.write(magicline + "\n")
|
||||
for msg in self.messagelist.values():
|
||||
flags = msg['flags']
|
||||
flags.sort()
|
||||
flags = ''.join(flags)
|
||||
file.write("%s:%s\n" % (msg['uid'], flags))
|
||||
file.close()
|
||||
os.rename(self.filename + ".tmp", self.filename)
|
||||
finally:
|
||||
self.savelock.release()
|
||||
|
||||
def getmessagelist(self):
|
||||
return self.messagelist
|
||||
|
||||
def savemessage(self, uid, content, flags):
|
||||
if uid < 0:
|
||||
# We cannot assign a uid.
|
||||
return uid
|
||||
|
||||
if uid in self.messagelist: # already have it
|
||||
self.savemessageflags(uid, flags)
|
||||
return uid
|
||||
|
||||
self.messagelist[uid] = {'uid': uid, 'flags': flags}
|
||||
self.autosave()
|
||||
return uid
|
||||
|
||||
def getmessageflags(self, uid):
|
||||
return self.messagelist[uid]['flags']
|
||||
|
||||
def savemessageflags(self, uid, flags):
|
||||
self.messagelist[uid]['flags'] = flags
|
||||
self.autosave()
|
||||
|
||||
def deletemessage(self, uid):
|
||||
if not uid in self.messagelist:
|
||||
return
|
||||
del(self.messagelist[uid])
|
||||
self.autosave()
|
220
offlineimap/folder/Maildir.py
Normal file
220
offlineimap/folder/Maildir.py
Normal file
@ -0,0 +1,220 @@
|
||||
# Maildir folder support
|
||||
# Copyright (C) 2002 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 Base import BaseFolder
|
||||
from offlineimap import imaputil
|
||||
from offlineimap.ui import UIBase
|
||||
from threading import Lock
|
||||
import os.path, os, re, time, socket, md5
|
||||
|
||||
foldermatchre = re.compile(',FMD5=([0-9a-f]{32})')
|
||||
uidmatchre = re.compile(',U=(\d+)')
|
||||
flagmatchre = re.compile(':.*2,([A-Z]+)')
|
||||
|
||||
timeseq = 0
|
||||
lasttime = long(0)
|
||||
timelock = Lock()
|
||||
|
||||
def gettimeseq():
|
||||
global lasttime, timeseq, timelock
|
||||
timelock.acquire()
|
||||
try:
|
||||
thistime = long(time.time())
|
||||
if thistime == lasttime:
|
||||
timeseq += 1
|
||||
return (thistime, timeseq)
|
||||
else:
|
||||
lasttime = thistime
|
||||
timeseq = 0
|
||||
return (thistime, timeseq)
|
||||
finally:
|
||||
timelock.release()
|
||||
|
||||
class MaildirFolder(BaseFolder):
|
||||
def __init__(self, root, name, sep, repository, accountname):
|
||||
self.name = name
|
||||
self.root = root
|
||||
self.sep = sep
|
||||
self.messagelist = None
|
||||
self.repository = repository
|
||||
self.accountname = accountname
|
||||
BaseFolder.__init__(self)
|
||||
|
||||
def getaccountname(self):
|
||||
return self.accountname
|
||||
|
||||
def getfullname(self):
|
||||
return os.path.join(self.getroot(), self.getname())
|
||||
|
||||
def getuidvalidity(self):
|
||||
"""Maildirs have no notion of uidvalidity, so we just return a magic
|
||||
token."""
|
||||
return 42
|
||||
|
||||
def _scanfolder(self):
|
||||
"""Cache the message list. Maildir flags are:
|
||||
R (replied)
|
||||
S (seen)
|
||||
T (trashed)
|
||||
D (draft)
|
||||
F (flagged)
|
||||
and must occur in ASCII order."""
|
||||
retval = {}
|
||||
files = []
|
||||
nouidcounter = -1 # Messages without UIDs get
|
||||
# negative UID numbers.
|
||||
for dirannex in ['new', 'cur']:
|
||||
fulldirname = os.path.join(self.getfullname(), dirannex)
|
||||
files.extend([os.path.join(fulldirname, filename) for
|
||||
filename in os.listdir(fulldirname)])
|
||||
for file in files:
|
||||
messagename = os.path.basename(file)
|
||||
foldermatch = foldermatchre.search(messagename)
|
||||
if (not foldermatch) or \
|
||||
md5.new(self.getvisiblename()).hexdigest() \
|
||||
!= foldermatch.group(1):
|
||||
# If there is no folder MD5 specified, or if it mismatches,
|
||||
# assume it is a foreign (new) message and generate a
|
||||
# negative uid for it
|
||||
uid = nouidcounter
|
||||
nouidcounter -= 1
|
||||
else: # It comes from our folder.
|
||||
uidmatch = uidmatchre.search(messagename)
|
||||
uid = None
|
||||
if not uidmatch:
|
||||
uid = nouidcounter
|
||||
nouidcounter -= 1
|
||||
else:
|
||||
uid = long(uidmatch.group(1))
|
||||
flagmatch = flagmatchre.search(messagename)
|
||||
flags = []
|
||||
if flagmatch:
|
||||
flags = [x for x in flagmatch.group(1)]
|
||||
flags.sort()
|
||||
retval[uid] = {'uid': uid,
|
||||
'flags': flags,
|
||||
'filename': file}
|
||||
return retval
|
||||
|
||||
def cachemessagelist(self):
|
||||
self.messagelist = self._scanfolder()
|
||||
|
||||
def getmessagelist(self):
|
||||
return self.messagelist
|
||||
|
||||
def getmessage(self, uid):
|
||||
filename = self.messagelist[uid]['filename']
|
||||
file = open(filename, 'rt')
|
||||
retval = file.read()
|
||||
file.close()
|
||||
return retval.replace("\r\n", "\n")
|
||||
|
||||
def savemessage(self, uid, content, flags):
|
||||
ui = UIBase.getglobalui()
|
||||
ui.debug('maildir', 'savemessage: called to write with flags %s and content %s' % \
|
||||
(repr(flags), repr(content)))
|
||||
if uid < 0:
|
||||
# We cannot assign a new uid.
|
||||
return uid
|
||||
if uid in self.messagelist:
|
||||
# We already have it.
|
||||
self.savemessageflags(uid, flags)
|
||||
return uid
|
||||
if 'S' in flags:
|
||||
# If a message has been seen, it goes into the cur
|
||||
# directory. CR debian#152482, [complete.org #4]
|
||||
newdir = os.path.join(self.getfullname(), 'cur')
|
||||
else:
|
||||
newdir = os.path.join(self.getfullname(), 'new')
|
||||
tmpdir = os.path.join(self.getfullname(), 'tmp')
|
||||
messagename = None
|
||||
attempts = 0
|
||||
while 1:
|
||||
if attempts > 15:
|
||||
raise IOError, "Couldn't write to file %s" % messagename
|
||||
timeval, timeseq = gettimeseq()
|
||||
messagename = '%d_%d.%d.%s,U=%d,FMD5=%s' % \
|
||||
(timeval,
|
||||
timeseq,
|
||||
os.getpid(),
|
||||
socket.gethostname(),
|
||||
uid,
|
||||
md5.new(self.getvisiblename()).hexdigest())
|
||||
if os.path.exists(os.path.join(tmpdir, messagename)):
|
||||
time.sleep(2)
|
||||
attempts += 1
|
||||
else:
|
||||
break
|
||||
tmpmessagename = messagename.split(',')[0]
|
||||
ui.debug('maildir', 'savemessage: using temporary name %s' % tmpmessagename)
|
||||
file = open(os.path.join(tmpdir, tmpmessagename), "wt")
|
||||
file.write(content)
|
||||
file.close()
|
||||
ui.debug('maildir', 'savemessage: moving from %s to %s' % \
|
||||
(tmpmessagename, messagename))
|
||||
os.link(os.path.join(tmpdir, tmpmessagename),
|
||||
os.path.join(newdir, messagename))
|
||||
os.unlink(os.path.join(tmpdir, tmpmessagename))
|
||||
self.messagelist[uid] = {'uid': uid, 'flags': [],
|
||||
'filename': os.path.join(newdir, messagename)}
|
||||
self.savemessageflags(uid, flags)
|
||||
ui.debug('maildir', 'savemessage: returning uid %d' % uid)
|
||||
return uid
|
||||
|
||||
def getmessageflags(self, uid):
|
||||
return self.messagelist[uid]['flags']
|
||||
|
||||
def savemessageflags(self, uid, flags):
|
||||
oldfilename = self.messagelist[uid]['filename']
|
||||
newpath, newname = os.path.split(oldfilename)
|
||||
if 'S' in flags:
|
||||
# If a message has been seen, it goes into the cur
|
||||
# directory. CR debian#152482, [complete.org #4]
|
||||
newpath = os.path.join(self.getfullname(), 'cur')
|
||||
else:
|
||||
newpath = os.path.join(self.getfullname(), 'new')
|
||||
infostr = ':'
|
||||
infomatch = re.search('(:.*)$', newname)
|
||||
if infomatch: # If the info string is present..
|
||||
infostr = infomatch.group(1)
|
||||
newname = newname.split(':')[0] # Strip off the info string.
|
||||
infostr = re.sub('2,[A-Z]*', '', infostr)
|
||||
flags.sort()
|
||||
infostr += '2,' + ''.join(flags)
|
||||
newname += infostr
|
||||
|
||||
newfilename = os.path.join(newpath, newname)
|
||||
if (newfilename != oldfilename):
|
||||
os.rename(oldfilename, newfilename)
|
||||
self.messagelist[uid]['flags'] = flags
|
||||
self.messagelist[uid]['filename'] = newfilename
|
||||
|
||||
def deletemessage(self, uid):
|
||||
if not uid in self.messagelist:
|
||||
return
|
||||
filename = self.messagelist[uid]['filename']
|
||||
try:
|
||||
os.unlink(filename)
|
||||
except OSError:
|
||||
# Can't find the file -- maybe already deleted?
|
||||
newmsglist = self._scanfolder()
|
||||
if uid in newmsglist: # Nope, try new filename.
|
||||
os.unlink(newmsglist[uid]['filename'])
|
||||
# Yep -- return.
|
||||
del(self.messagelist[uid])
|
||||
|
2
offlineimap/folder/__init__.py
Normal file
2
offlineimap/folder/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
import Base, IMAP, Maildir, LocalStatus
|
||||
|
1413
offlineimap/imaplib.py
Normal file
1413
offlineimap/imaplib.py
Normal file
File diff suppressed because it is too large
Load Diff
309
offlineimap/imapserver.py
Normal file
309
offlineimap/imapserver.py
Normal file
@ -0,0 +1,309 @@
|
||||
# IMAP server support
|
||||
# Copyright (C) 2002, 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 imaplib, imaputil, threadutil
|
||||
from offlineimap.ui import UIBase
|
||||
from threading import *
|
||||
import thread, hmac, os
|
||||
|
||||
|
||||
class UsefulIMAPMixIn:
|
||||
def getstate(self):
|
||||
return self.state
|
||||
def getselectedfolder(self):
|
||||
if self.getstate() == 'SELECTED':
|
||||
return self.selectedfolder
|
||||
return None
|
||||
|
||||
def select(self, mailbox='INBOX', readonly=None, force = 0):
|
||||
if (not force) and self.getselectedfolder() == mailbox:
|
||||
self.is_readonly = readonly
|
||||
# No change; return.
|
||||
return
|
||||
result = self.__class__.__bases__[1].select(self, mailbox, readonly)
|
||||
if result[0] != 'OK':
|
||||
raise ValueError, "Error from select: %s" % str(result)
|
||||
if self.getstate() == 'SELECTED':
|
||||
self.selectedfolder = mailbox
|
||||
else:
|
||||
self.selectedfolder = None
|
||||
|
||||
class UsefulIMAP4(UsefulIMAPMixIn, imaplib.IMAP4): pass
|
||||
class UsefulIMAP4_SSL(UsefulIMAPMixIn, imaplib.IMAP4_SSL): pass
|
||||
class UsefulIMAP4_Tunnel(UsefulIMAPMixIn, imaplib.IMAP4_Tunnel): pass
|
||||
|
||||
class IMAPServer:
|
||||
def __init__(self, config, reposname,
|
||||
username = None, password = None, hostname = None,
|
||||
port = None, ssl = 1, maxconnections = 1, tunnel = None,
|
||||
reference = '""'):
|
||||
self.reposname = reposname
|
||||
self.config = config
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.passworderror = None
|
||||
self.hostname = hostname
|
||||
self.tunnel = tunnel
|
||||
self.port = port
|
||||
self.usessl = ssl
|
||||
self.delim = None
|
||||
self.root = None
|
||||
if port == None:
|
||||
if ssl:
|
||||
self.port = 993
|
||||
else:
|
||||
self.port = 143
|
||||
self.maxconnections = maxconnections
|
||||
self.availableconnections = []
|
||||
self.assignedconnections = []
|
||||
self.lastowner = {}
|
||||
self.semaphore = BoundedSemaphore(self.maxconnections)
|
||||
self.connectionlock = Lock()
|
||||
self.reference = reference
|
||||
|
||||
def getpassword(self):
|
||||
if self.password != None and self.passworderror == None:
|
||||
return self.password
|
||||
|
||||
self.password = UIBase.getglobalui().getpass(self.reposname,
|
||||
self.config,
|
||||
self.passworderror)
|
||||
self.passworderror = None
|
||||
|
||||
return self.password
|
||||
|
||||
def getdelim(self):
|
||||
"""Returns this server's folder delimiter. Can only be called
|
||||
after one or more calls to acquireconnection."""
|
||||
return self.delim
|
||||
|
||||
def getroot(self):
|
||||
"""Returns this server's folder root. Can only be called after one
|
||||
or more calls to acquireconnection."""
|
||||
return self.root
|
||||
|
||||
|
||||
def releaseconnection(self, connection):
|
||||
self.connectionlock.acquire()
|
||||
self.assignedconnections.remove(connection)
|
||||
self.availableconnections.append(connection)
|
||||
self.connectionlock.release()
|
||||
self.semaphore.release()
|
||||
|
||||
def md5handler(self, response):
|
||||
ui = UIBase.getglobalui()
|
||||
challenge = response.strip()
|
||||
ui.debug('imap', 'md5handler: got challenge %s' % challenge)
|
||||
|
||||
passwd = self.getpassword()
|
||||
retval = self.username + ' ' + hmac.new(passwd, challenge).hexdigest()
|
||||
ui.debug('imap', 'md5handler: returning %s' % retval)
|
||||
return retval
|
||||
|
||||
def plainauth(self, imapobj):
|
||||
UIBase.getglobalui().debug('imap',
|
||||
'Attempting plain authentication')
|
||||
imapobj.login(self.username, self.getpassword())
|
||||
|
||||
|
||||
def acquireconnection(self):
|
||||
"""Fetches a connection from the pool, making sure to create a new one
|
||||
if needed, to obey the maximum connection limits, etc.
|
||||
Opens a connection to the server and returns an appropriate
|
||||
object."""
|
||||
|
||||
self.semaphore.acquire()
|
||||
self.connectionlock.acquire()
|
||||
imapobj = None
|
||||
|
||||
if len(self.availableconnections): # One is available.
|
||||
# Try to find one that previously belonged to this thread
|
||||
# as an optimization. Start from the back since that's where
|
||||
# they're popped on.
|
||||
threadid = thread.get_ident()
|
||||
imapobj = None
|
||||
for i in range(len(self.availableconnections) - 1, -1, -1):
|
||||
tryobj = self.availableconnections[i]
|
||||
if self.lastowner[tryobj] == threadid:
|
||||
imapobj = tryobj
|
||||
del(self.availableconnections[i])
|
||||
break
|
||||
if not imapobj:
|
||||
imapobj = self.availableconnections[0]
|
||||
del(self.availableconnections[0])
|
||||
self.assignedconnections.append(imapobj)
|
||||
self.lastowner[imapobj] = thread.get_ident()
|
||||
self.connectionlock.release()
|
||||
return imapobj
|
||||
|
||||
self.connectionlock.release() # Release until need to modify data
|
||||
|
||||
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:
|
||||
try:
|
||||
if 'AUTH=CRAM-MD5' in imapobj.capabilities:
|
||||
UIBase.getglobalui().debug('imap',
|
||||
'Attempting CRAM-MD5 authentication')
|
||||
try:
|
||||
imapobj.authenticate('CRAM-MD5', self.md5handler)
|
||||
except imapobj.error, val:
|
||||
self.plainauth(imapobj)
|
||||
else:
|
||||
self.plainauth(imapobj)
|
||||
# Would bail by here if there was a failure.
|
||||
success = 1
|
||||
except imapobj.error, val:
|
||||
self.passworderror = str(val)
|
||||
self.password = None
|
||||
|
||||
if self.delim == None:
|
||||
listres = imapobj.list(self.reference, '""')[1]
|
||||
if listres == [None] or listres == None:
|
||||
# Some buggy IMAP servers do not respond well to LIST "" ""
|
||||
# Work around them.
|
||||
listres = imapobj.list(self.reference, '"*"')[1]
|
||||
self.delim, self.root = \
|
||||
imaputil.imapsplit(listres[0])[1:]
|
||||
self.delim = imaputil.dequote(self.delim)
|
||||
self.root = imaputil.dequote(self.root)
|
||||
|
||||
self.connectionlock.acquire()
|
||||
self.assignedconnections.append(imapobj)
|
||||
self.lastowner[imapobj] = thread.get_ident()
|
||||
self.connectionlock.release()
|
||||
return imapobj
|
||||
|
||||
def connectionwait(self):
|
||||
"""Waits until there is a connection available. Note that between
|
||||
the time that a connection becomes available and the time it is
|
||||
requested, another thread may have grabbed it. This function is
|
||||
mainly present as a way to avoid spawning thousands of threads
|
||||
to copy messages, then have them all wait for 3 available connections.
|
||||
It's OK if we have maxconnections + 1 or 2 threads, which is what
|
||||
this will help us do."""
|
||||
threadutil.semaphorewait(self.semaphore)
|
||||
|
||||
def close(self):
|
||||
# Make sure I own all the semaphores. Let the threads finish
|
||||
# their stuff. This is a blocking method.
|
||||
self.connectionlock.acquire()
|
||||
threadutil.semaphorereset(self.semaphore, self.maxconnections)
|
||||
for imapobj in self.assignedconnections + self.availableconnections:
|
||||
imapobj.logout()
|
||||
self.assignedconnections = []
|
||||
self.availableconnections = []
|
||||
self.lastowner = {}
|
||||
self.connectionlock.release()
|
||||
|
||||
def keepalive(self, timeout, event):
|
||||
"""Sends a NOOP to each connection recorded. It will wait a maximum
|
||||
of timeout seconds between doing this, and will continue to do so
|
||||
until the Event object as passed is true. This method is expected
|
||||
to be invoked in a separate thread, which should be join()'d after
|
||||
the event is set."""
|
||||
ui = UIBase.getglobalui()
|
||||
ui.debug('imap', 'keepalive thread started')
|
||||
while 1:
|
||||
ui.debug('imap', 'keepalive: top of loop')
|
||||
event.wait(timeout)
|
||||
ui.debug('imap', 'keepalive: after wait')
|
||||
if event.isSet():
|
||||
ui.debug('imap', 'keepalive: event is set; exiting')
|
||||
return
|
||||
ui.debug('imap', 'keepalive: acquiring connectionlock')
|
||||
self.connectionlock.acquire()
|
||||
numconnections = len(self.assignedconnections) + \
|
||||
len(self.availableconnections)
|
||||
self.connectionlock.release()
|
||||
ui.debug('imap', 'keepalive: connectionlock released')
|
||||
threads = []
|
||||
imapobjs = []
|
||||
|
||||
for i in range(numconnections):
|
||||
ui.debug('imap', 'keepalive: processing connection %d of %d' % (i, numconnections))
|
||||
imapobj = self.acquireconnection()
|
||||
ui.debug('imap', 'keepalive: connection %d acquired' % i)
|
||||
imapobjs.append(imapobj)
|
||||
thr = threadutil.ExitNotifyThread(target = imapobj.noop)
|
||||
thr.setDaemon(1)
|
||||
thr.start()
|
||||
threads.append(thr)
|
||||
ui.debug('imap', 'keepalive: thread started')
|
||||
|
||||
ui.debug('imap', 'keepalive: joining threads')
|
||||
|
||||
for thr in threads:
|
||||
# Make sure all the commands have completed.
|
||||
thr.join()
|
||||
|
||||
ui.debug('imap', 'keepalive: releasing connections')
|
||||
|
||||
for imapobj in imapobjs:
|
||||
self.releaseconnection(imapobj)
|
||||
|
||||
ui.debug('imap', 'keepalive: bottom of loop')
|
||||
|
||||
class ConfigedIMAPServer(IMAPServer):
|
||||
"""This class is designed for easier initialization given a ConfigParser
|
||||
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, repository, passwordhash = {}):
|
||||
"""Initialize the object. If the account is not a tunnel,
|
||||
the password is required."""
|
||||
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 repository.getname() in passwordhash:
|
||||
password = passwordhash[repository.getname()]
|
||||
|
||||
# Connect to the remote server.
|
||||
if usetunnel:
|
||||
IMAPServer.__init__(self, self.config, self.repos.getname(),
|
||||
tunnel = usetunnel,
|
||||
reference = reference,
|
||||
maxconnections = self.repos.getmaxconnections())
|
||||
else:
|
||||
if not password:
|
||||
password = self.repos.getpassword()
|
||||
IMAPServer.__init__(self, self.config, self.repos.getname(),
|
||||
user, password, host, port, ssl,
|
||||
self.repos.getmaxconnections(),
|
||||
reference = reference)
|
211
offlineimap/imaputil.py
Normal file
211
offlineimap/imaputil.py
Normal file
@ -0,0 +1,211 @@
|
||||
# IMAP utility module
|
||||
# Copyright (C) 2002 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
|
||||
|
||||
import re, string, types
|
||||
from offlineimap.ui import UIBase
|
||||
quotere = re.compile('^("(?:[^"]|\\\\")*")')
|
||||
|
||||
def debug(*args):
|
||||
msg = []
|
||||
for arg in args:
|
||||
msg.append(str(arg))
|
||||
UIBase.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.
|
||||
"""
|
||||
|
||||
debug("dequote() called with input:", string)
|
||||
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)
|
||||
return string
|
||||
|
||||
def flagsplit(string):
|
||||
if string[0] != '(' or string[-1] != ')':
|
||||
raise ValueError, "Passed string '%s' is not a flag list" % string
|
||||
return imapsplit(string[1:-1])
|
||||
|
||||
def options2hash(list):
|
||||
debug("options2hash called with input:", list)
|
||||
retval = {}
|
||||
counter = 0
|
||||
while (counter < len(list)):
|
||||
retval[list[counter]] = list[counter + 1]
|
||||
counter += 2
|
||||
debug("options2hash returning:", retval)
|
||||
return retval
|
||||
|
||||
def flags2hash(string):
|
||||
return options2hash(flagsplit(string))
|
||||
|
||||
def imapsplit(imapstring):
|
||||
"""Takes a string from an IMAP conversation and returns a list containing
|
||||
its components. One example string is:
|
||||
|
||||
(\\HasNoChildren) "." "INBOX.Sent"
|
||||
|
||||
The result from parsing this will be:
|
||||
|
||||
['(\\HasNoChildren)', '"."', '"INBOX.Sent"']"""
|
||||
|
||||
debug("imapsplit() called with input:", imapstring)
|
||||
if type(imapstring) != types.StringType:
|
||||
debug("imapsplit() got a non-string input; working around.")
|
||||
# Sometimes, imaplib will throw us a tuple if the input
|
||||
# contains a literal. See Python bug
|
||||
# #619732 at https://sourceforge.net/tracker/index.php?func=detail&aid=619732&group_id=5470&atid=105470
|
||||
# One example is:
|
||||
# result[0] = '() "\\\\" Admin'
|
||||
# result[1] = ('() "\\\\" {19}', 'Folder\\2')
|
||||
#
|
||||
# This function will effectively get result[0] or result[1], so
|
||||
# if we get the result[1] version, we need to parse apart the tuple
|
||||
# and figure out what to do with it. Each even-numbered
|
||||
# part of it should end with the {} number, and each odd-numbered
|
||||
# part should be directly a part of the result. We'll
|
||||
# artificially quote it to help out.
|
||||
retval = []
|
||||
for i in range(len(imapstring)):
|
||||
if i % 2: # Odd: quote then append.
|
||||
arg = imapstring[i]
|
||||
# Quote code lifted from imaplib
|
||||
arg = arg.replace('\\', '\\\\')
|
||||
arg = arg.replace('"', '\\"')
|
||||
arg = '"%s"' % arg
|
||||
debug("imapsplit() non-string [%d]: Appending %s" %\
|
||||
(i, arg))
|
||||
retval.append(arg)
|
||||
else:
|
||||
# Even -- we have a string that ends with a literal
|
||||
# size specifier. We need to strip off that, then run
|
||||
# what remains through the regular imapsplit parser.
|
||||
# Recursion to the rescue.
|
||||
arg = imapstring[i]
|
||||
arg = re.sub('\{\d+\}$', '', arg)
|
||||
debug("imapsplit() non-string [%d]: Feeding %s to recursion" %\
|
||||
(i, arg))
|
||||
retval.extend(imapsplit(arg))
|
||||
debug("imapsplit() non-string: returning %s" % str(retval))
|
||||
return retval
|
||||
|
||||
workstr = imapstring.strip()
|
||||
retval = []
|
||||
while len(workstr):
|
||||
if workstr[0] == '(':
|
||||
rparenc = 1 # count of right parenthesis to match
|
||||
rpareni = 1 # position to examine
|
||||
while rparenc: # Find the end of the group.
|
||||
if workstr[rpareni] == ')': # end of a group
|
||||
rparenc -= 1
|
||||
elif workstr[rpareni] == '(': # start of a group
|
||||
rparenc += 1
|
||||
rpareni += 1 # Move to next character.
|
||||
parenlist = workstr[0:rpareni]
|
||||
workstr = workstr[rpareni:].lstrip()
|
||||
retval.append(parenlist)
|
||||
elif workstr[0] == '"':
|
||||
quotelist = quotere.search(workstr).group(1)
|
||||
workstr = workstr[len(quotelist):].lstrip()
|
||||
retval.append(quotelist)
|
||||
else:
|
||||
splits = string.split(workstr, maxsplit = 1)
|
||||
splitslen = len(splits)
|
||||
# The unquoted word is splits[0]; the remainder is splits[1]
|
||||
if splitslen == 2:
|
||||
# There's an unquoted word, and more string follows.
|
||||
retval.append(splits[0])
|
||||
workstr = splits[1] # split will have already lstripped it
|
||||
continue
|
||||
elif splitslen == 1:
|
||||
# We got a last unquoted word, but nothing else
|
||||
retval.append(splits[0])
|
||||
# Nothing remains. workstr would be ''
|
||||
break
|
||||
elif splitslen == 0:
|
||||
# There was not even an unquoted word.
|
||||
break
|
||||
debug("imapsplit() returning:", retval)
|
||||
return retval
|
||||
|
||||
def flagsimap2maildir(flagstring):
|
||||
flagmap = {'\\seen': 'S',
|
||||
'\\answered': 'R',
|
||||
'\\flagged': 'F',
|
||||
'\\deleted': 'T',
|
||||
'\\draft': 'D'}
|
||||
retval = []
|
||||
imapflaglist = [x.lower() for x in flagstring[1:-1].split()]
|
||||
for imapflag in imapflaglist:
|
||||
if flagmap.has_key(imapflag):
|
||||
retval.append(flagmap[imapflag])
|
||||
retval.sort()
|
||||
return retval
|
||||
|
||||
def flagsmaildir2imap(list):
|
||||
flagmap = {'S': '\\Seen',
|
||||
'R': '\\Answered',
|
||||
'F': '\\Flagged',
|
||||
'T': '\\Deleted',
|
||||
'D': '\\Draft'}
|
||||
retval = []
|
||||
for mdflag in list:
|
||||
if flagmap.has_key(mdflag):
|
||||
retval.append(flagmap[mdflag])
|
||||
retval.sort()
|
||||
return '(' + ' '.join(retval) + ')'
|
||||
|
||||
def listjoin(list):
|
||||
start = None
|
||||
end = None
|
||||
retval = []
|
||||
|
||||
def getlist(start, end):
|
||||
if start == end:
|
||||
return(str(start))
|
||||
else:
|
||||
return(str(start) + ":" + str(end))
|
||||
|
||||
|
||||
for item in list:
|
||||
if start == None:
|
||||
# First item.
|
||||
start = item
|
||||
end = item
|
||||
elif item == end + 1:
|
||||
# An addition to the list.
|
||||
end = item
|
||||
else:
|
||||
# Here on: starting a new list.
|
||||
retval.append(getlist(start, end))
|
||||
start = item
|
||||
end = item
|
||||
|
||||
if start != None:
|
||||
retval.append(getlist(start, end))
|
||||
|
||||
return ",".join(retval)
|
||||
|
||||
|
||||
|
||||
|
||||
|
151
offlineimap/init.py
Normal file
151
offlineimap/init.py
Normal file
@ -0,0 +1,151 @@
|
||||
# OfflineIMAP initialization code
|
||||
# Copyright (C) 2002, 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 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
|
||||
from offlineimap.CustomConfig import CustomConfigParser
|
||||
from threading import *
|
||||
import threading
|
||||
from getopt import getopt
|
||||
|
||||
try:
|
||||
import fcntl
|
||||
hasfcntl = 1
|
||||
except:
|
||||
hasfcntl = 0
|
||||
|
||||
lockfd = None
|
||||
|
||||
def lock(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 startup(versionno):
|
||||
assert versionno == version.versionstr, "Revision of main program (%s) does not match that of library (%s). Please double-check your PYTHONPATH and installation locations." % (versionno, version.versionstr)
|
||||
options = {}
|
||||
if '--help' in sys.argv[1:]:
|
||||
sys.stdout.write(version.cmdhelp + "\n")
|
||||
sys.exit(0)
|
||||
|
||||
for optlist in getopt(sys.argv[1:], 'P:1oa:c:d:l:u:h')[0]:
|
||||
options[optlist[0]] = optlist[1]
|
||||
|
||||
if options.has_key('-h'):
|
||||
sys.stdout.write(version.cmdhelp)
|
||||
sys.stdout.write("\n")
|
||||
sys.exit(0)
|
||||
configfilename = os.path.expanduser("~/.offlineimaprc")
|
||||
if options.has_key('-c'):
|
||||
configfilename = options['-c']
|
||||
if options.has_key('-P'):
|
||||
if not options.has_key('-1'):
|
||||
sys.stderr.write("FATAL: profile mode REQUIRES -1\n")
|
||||
sys.exit(100)
|
||||
profiledir = options['-P']
|
||||
os.mkdir(profiledir)
|
||||
threadutil.setprofiledir(profiledir)
|
||||
sys.stderr.write("WARNING: profile mode engaged;\nPotentially large data will be created in " + profiledir + "\n")
|
||||
|
||||
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)
|
||||
|
||||
ui = offlineimap.ui.detector.findUI(config, options.get('-u'))
|
||||
UIBase.setglobalui(ui)
|
||||
|
||||
if options.has_key('-l'):
|
||||
ui.setlogfd(open(options['-l'], 'wt'))
|
||||
|
||||
ui.init_banner()
|
||||
|
||||
if options.has_key('-d'):
|
||||
for debugtype in options['-d'].split(','):
|
||||
ui.add_debug(debugtype.strip())
|
||||
if debugtype == 'imap':
|
||||
imaplib.Debug = 5
|
||||
if debugtype == 'thread':
|
||||
threading._VERBOSE = 1
|
||||
|
||||
if options.has_key('-o'):
|
||||
# FIXME: maybe need a better
|
||||
for section in accounts.getaccountlist(config):
|
||||
config.remove_option('Account ' + section, "autorefresh")
|
||||
|
||||
lock(config, ui)
|
||||
|
||||
if options.has_key('-l'):
|
||||
sys.stderr = ui.logfile
|
||||
|
||||
activeaccounts = config.get("general", "accounts")
|
||||
if options.has_key('-a'):
|
||||
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
|
||||
localrepos = None
|
||||
|
||||
if options.has_key('-1'):
|
||||
threadutil.initInstanceLimit("ACCOUNTLIMIT", 1)
|
||||
else:
|
||||
threadutil.initInstanceLimit("ACCOUNTLIMIT",
|
||||
config.getdefaultint("general", "maxsyncaccounts", 1))
|
||||
|
||||
for reposname in config.getsectionlist('Repository'):
|
||||
for instancename in ["FOLDER_" + reposname,
|
||||
"MSGCOPY_" + reposname]:
|
||||
if options.has_key('-1'):
|
||||
threadutil.initInstanceLimit(instancename, 1)
|
||||
else:
|
||||
threadutil.initInstanceLimit(instancename,
|
||||
config.getdefaultint('Repository ' + reposname, "maxconnections", 1))
|
||||
|
||||
threadutil.initexitnotify()
|
||||
t = ExitNotifyThread(target=syncmaster.syncitall,
|
||||
name='Sync Runner',
|
||||
kwargs = {'accounts': syncaccounts,
|
||||
'config': config})
|
||||
t.setDaemon(1)
|
||||
t.start()
|
||||
try:
|
||||
threadutil.exitnotifymonitorloop(threadutil.threadexited)
|
||||
except SystemExit:
|
||||
raise
|
||||
except:
|
||||
ui.mainException() # Also expected to terminate.
|
||||
|
||||
|
45
offlineimap/localeval.py
Normal file
45
offlineimap/localeval.py
Normal file
@ -0,0 +1,45 @@
|
||||
"""Eval python code with global namespace of a python source file."""
|
||||
|
||||
# Copyright (C) 2002 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
|
||||
|
||||
import imp
|
||||
try:
|
||||
import errno
|
||||
except:
|
||||
pass
|
||||
|
||||
class LocalEval:
|
||||
def __init__(self, path=None):
|
||||
self.namespace={}
|
||||
|
||||
if path is not None:
|
||||
file=open(path, 'r')
|
||||
module=imp.load_module(
|
||||
'<none>',
|
||||
file,
|
||||
path,
|
||||
('', 'r', imp.PY_SOURCE))
|
||||
for attr in dir(module):
|
||||
self.namespace[attr]=getattr(module, attr)
|
||||
|
||||
def eval(self, text, namespace=None):
|
||||
names = {}
|
||||
names.update(self.namespace)
|
||||
if namespace is not None:
|
||||
names.update(namespace)
|
||||
return eval(text, names)
|
74
offlineimap/mbnames.py
Normal file
74
offlineimap/mbnames.py
Normal file
@ -0,0 +1,74 @@
|
||||
# Mailbox name generator
|
||||
# Copyright (C) 2002 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
|
||||
|
||||
import os.path
|
||||
import re # for folderfilter
|
||||
from threading import *
|
||||
|
||||
boxes = {}
|
||||
config = None
|
||||
accounts = None
|
||||
mblock = Lock()
|
||||
|
||||
def init(conf, accts):
|
||||
global config, accounts
|
||||
config = conf
|
||||
accounts = accts
|
||||
|
||||
def add(accountname, foldername):
|
||||
if not accountname in boxes:
|
||||
boxes[accountname] = []
|
||||
if not foldername in boxes[accountname]:
|
||||
boxes[accountname].append(foldername)
|
||||
|
||||
def write():
|
||||
# See if we're ready to write it out.
|
||||
for account in accounts:
|
||||
if account not in boxes:
|
||||
return
|
||||
|
||||
genmbnames()
|
||||
|
||||
def genmbnames():
|
||||
"""Takes a configparser object and a boxlist, which is a list of hashes
|
||||
containing 'accountname' and 'foldername' keys."""
|
||||
mblock.acquire()
|
||||
try:
|
||||
localeval = config.getlocaleval()
|
||||
if not config.getdefaultboolean("mbnames", "enabled", 0):
|
||||
return
|
||||
file = open(os.path.expanduser(config.get("mbnames", "filename")), "wt")
|
||||
file.write(localeval.eval(config.get("mbnames", "header")))
|
||||
folderfilter = lambda accountname, foldername: 1
|
||||
if config.has_option("mbnames", "folderfilter"):
|
||||
folderfilter = localeval.eval(config.get("mbnames", "folderfilter"),
|
||||
{'re': re})
|
||||
itemlist = []
|
||||
for accountname in boxes.keys():
|
||||
for foldername in boxes[accountname]:
|
||||
if folderfilter(accountname, foldername):
|
||||
itemlist.append(config.get("mbnames", "peritem", raw=1) % \
|
||||
{'accountname': accountname,
|
||||
'foldername': foldername})
|
||||
file.write(localeval.eval(config.get("mbnames", "sep")).join(itemlist))
|
||||
file.write(localeval.eval(config.get("mbnames", "footer")))
|
||||
file.close()
|
||||
finally:
|
||||
mblock.release()
|
||||
|
||||
|
145
offlineimap/repository/Base.py
Normal file
145
offlineimap/repository/Base.py
Normal file
@ -0,0 +1,145 @@
|
||||
# Base repository support
|
||||
# Copyright (C) 2002, 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 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 []
|
||||
|
||||
def getsep(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def makefolder(self, foldername):
|
||||
raise NotImplementedError
|
||||
|
||||
def deletefolder(self, foldername):
|
||||
raise NotImplementedError
|
||||
|
||||
def getfolder(self, foldername):
|
||||
raise NotImplementedError
|
||||
|
||||
def syncfoldersto(self, dest):
|
||||
"""Syncs the folders in this repository to those in dest.
|
||||
It does NOT sync the contents of those folders."""
|
||||
src = self
|
||||
srcfolders = src.getfolders()
|
||||
destfolders = dest.getfolders()
|
||||
|
||||
# Create hashes with the names, but convert the source folders
|
||||
# to the dest folder's sep.
|
||||
|
||||
srchash = {}
|
||||
for folder in srcfolders:
|
||||
srchash[folder.getvisiblename().replace(src.getsep(), dest.getsep())] = \
|
||||
folder
|
||||
desthash = {}
|
||||
for folder in destfolders:
|
||||
desthash[folder.getvisiblename()] = folder
|
||||
|
||||
#
|
||||
# Find new folders.
|
||||
#
|
||||
|
||||
for key in srchash.keys():
|
||||
if not key in desthash:
|
||||
dest.makefolder(key)
|
||||
|
||||
#
|
||||
# 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
|
||||
|
||||
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
|
||||
|
191
offlineimap/repository/IMAP.py
Normal file
191
offlineimap/repository/IMAP.py
Normal file
@ -0,0 +1,191 @@
|
||||
# IMAP repository support
|
||||
# Copyright (C) 2002 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 Base import BaseRepository
|
||||
from offlineimap import folder, imaputil, imapserver
|
||||
from offlineimap.folder.UIDMaps import MappedIMAPFolder
|
||||
from offlineimap.threadutil import ExitNotifyThread
|
||||
import re, types, os
|
||||
from threading import *
|
||||
|
||||
class IMAPRepository(BaseRepository):
|
||||
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
|
||||
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 = fd.readline().strip()
|
||||
fd.close()
|
||||
return password
|
||||
return None
|
||||
|
||||
def getfolder(self, foldername):
|
||||
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:
|
||||
return self.folders
|
||||
retval = []
|
||||
imapobj = self.imapserver.acquireconnection()
|
||||
try:
|
||||
listresult = imapobj.list(directory = self.imapserver.reference)[1]
|
||||
finally:
|
||||
self.imapserver.releaseconnection(imapobj)
|
||||
for string in listresult:
|
||||
if string == None or \
|
||||
(type(string) == types.StringType and string == ''):
|
||||
# Bug in imaplib: empty strings in results from
|
||||
# literals.
|
||||
continue
|
||||
flags, delim, name = imaputil.imapsplit(string)
|
||||
flaglist = [x.lower() for x in imaputil.flagsplit(flags)]
|
||||
if '\\noselect' in flaglist:
|
||||
continue
|
||||
foldername = imaputil.dequote(name)
|
||||
if not self.folderfilter(foldername):
|
||||
continue
|
||||
retval.append(self.getfoldertype()(self.imapserver, foldername,
|
||||
self.nametrans(foldername),
|
||||
self.accountname, self))
|
||||
if len(self.folderincludes):
|
||||
imapobj = self.imapserver.acquireconnection()
|
||||
try:
|
||||
for foldername in self.folderincludes:
|
||||
try:
|
||||
imapobj.select(foldername, readonly = 1)
|
||||
except ValueError:
|
||||
continue
|
||||
retval.append(self.getfoldertype()(self.imapserver,
|
||||
foldername,
|
||||
self.nametrans(foldername),
|
||||
self.accountname, self))
|
||||
finally:
|
||||
self.imapserver.releaseconnection(imapobj)
|
||||
|
||||
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
|
60
offlineimap/repository/LocalStatus.py
Normal file
60
offlineimap/repository/LocalStatus.py
Normal file
@ -0,0 +1,60 @@
|
||||
# Local status cache repository support
|
||||
# Copyright (C) 2002 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 Base import BaseRepository
|
||||
from offlineimap import folder
|
||||
import os, re
|
||||
|
||||
class LocalStatusRepository(BaseRepository):
|
||||
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
|
||||
|
||||
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):
|
||||
# "touch" the file.
|
||||
file = open(self.getfolderfilename(foldername), "ab")
|
||||
file.close()
|
||||
# Invalidate the cache.
|
||||
self.folders = None
|
||||
|
||||
def getfolders(self):
|
||||
retval = []
|
||||
for folder in os.listdir(self.directory):
|
||||
retval.append(folder.LocalStatus.LocalStatusFolder(self.directory,
|
||||
folder, self, self.accountname))
|
||||
return retval
|
||||
|
||||
def getfolder(self, foldername):
|
||||
return folder.LocalStatus.LocalStatusFolder(self.directory, foldername,
|
||||
self, self.accountname)
|
||||
|
||||
|
||||
|
||||
|
||||
|
146
offlineimap/repository/Maildir.py
Normal file
146
offlineimap/repository/Maildir.py
Normal file
@ -0,0 +1,146 @@
|
||||
# Maildir repository support
|
||||
# Copyright (C) 2002 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 Base import BaseRepository
|
||||
from offlineimap import folder, imaputil
|
||||
from offlineimap.ui import UIBase
|
||||
from mailbox import Maildir
|
||||
import os
|
||||
|
||||
class MaildirRepository(BaseRepository):
|
||||
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 = self.getlocalroot()
|
||||
self.folders = None
|
||||
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):
|
||||
return self.getconf('sep', '.').strip()
|
||||
|
||||
def makefolder(self, foldername):
|
||||
self.debug("makefolder called with arg " + repr(foldername))
|
||||
# Do the chdir thing so the call to makedirs does not make the
|
||||
# self.root directory (we'd prefer to raise an error in that case),
|
||||
# but will make the (relative) paths underneath it. Need to use
|
||||
# makedirs to support a / separator.
|
||||
if self.getsep() == '/':
|
||||
for invalid in ['new', 'cur', 'tmp', 'offlineimap.uidvalidity']:
|
||||
for component in foldername.split('/'):
|
||||
assert component != invalid, "When using nested folders (/ as a separator in the account config), your folder names may not contain 'new', 'cur', 'tmp', or 'offlineimap.uidvalidity'."
|
||||
|
||||
assert foldername.find('./') == -1, "Folder names may not contain ../"
|
||||
assert not foldername.startswith('/'), "Folder names may not begin with /"
|
||||
|
||||
oldcwd = os.getcwd()
|
||||
os.chdir(self.root)
|
||||
|
||||
# If we're using hierarchical folders, it's possible that sub-folders
|
||||
# may be created before higher-up ones. If this is the case,
|
||||
# makedirs will fail because the higher-up dir already exists.
|
||||
# So, check to see if this is indeed the case.
|
||||
|
||||
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']:
|
||||
assert not os.path.isdir(os.path.join(foldername, subdir)), \
|
||||
"Tried to create folder %s but it already had dir %s" %\
|
||||
(foldername, subdir)
|
||||
else:
|
||||
self.debug("makefolder: calling makedirs %s" % foldername)
|
||||
os.makedirs(foldername, 0700)
|
||||
self.debug("makefolder: creating cur, new, tmp")
|
||||
for subdir in ['cur', 'new', 'tmp']:
|
||||
os.mkdir(os.path.join(foldername, subdir), 0700)
|
||||
# Invalidate the cache
|
||||
self.folders = None
|
||||
os.chdir(oldcwd)
|
||||
|
||||
def deletefolder(self, foldername):
|
||||
self.ui.warn("NOT YET IMPLEMENTED: DELETE FOLDER %s" % foldername)
|
||||
|
||||
def getfolder(self, foldername):
|
||||
return folder.Maildir.MaildirFolder(self.root, foldername,
|
||||
self.getsep(), self, self.accountname)
|
||||
|
||||
def _getfolders_scandir(self, root, extension = None):
|
||||
self.debug("_GETFOLDERS_SCANDIR STARTING. root = %s, extension = %s" \
|
||||
% (root, extension))
|
||||
# extension willl only be non-None when called recursively when
|
||||
# getsep() returns '/'.
|
||||
retval = []
|
||||
|
||||
# Configure the full path to this repository -- "toppath"
|
||||
|
||||
if extension == None:
|
||||
toppath = root
|
||||
else:
|
||||
toppath = os.path.join(root, extension)
|
||||
|
||||
self.debug(" toppath = %s" % toppath)
|
||||
|
||||
# Iterate over directories in top.
|
||||
for dirname in os.listdir(toppath) + ['.']:
|
||||
self.debug(" *** top of loop")
|
||||
self.debug(" dirname = %s" % dirname)
|
||||
if dirname in ['cur', 'new', 'tmp', 'offlineimap.uidvalidity']:
|
||||
self.debug(" skipping this dir (Maildir special)")
|
||||
# Bypass special files.
|
||||
continue
|
||||
fullname = os.path.join(toppath, dirname)
|
||||
self.debug(" fullname = %s" % fullname)
|
||||
if not os.path.isdir(fullname):
|
||||
self.debug(" skipping this entry (not a directory)")
|
||||
# Not a directory -- not a folder.
|
||||
continue
|
||||
foldername = dirname
|
||||
if extension != None:
|
||||
foldername = os.path.join(extension, dirname)
|
||||
if (os.path.isdir(os.path.join(fullname, 'cur')) and
|
||||
os.path.isdir(os.path.join(fullname, 'new')) and
|
||||
os.path.isdir(os.path.join(fullname, 'tmp'))):
|
||||
# This directory has maildir stuff -- process
|
||||
self.debug(" This is a maildir folder.")
|
||||
|
||||
self.debug(" foldername = %s" % foldername)
|
||||
|
||||
retval.append(folder.Maildir.MaildirFolder(self.root, foldername,
|
||||
self.getsep(), self, self.accountname))
|
||||
if self.getsep() == '/' and dirname != '.':
|
||||
# Check sub-directories for folders.
|
||||
retval.extend(self._getfolders_scandir(root, foldername))
|
||||
self.debug("_GETFOLDERS_SCANDIR RETURNING %s" % \
|
||||
repr([x.getname() for x in retval]))
|
||||
return retval
|
||||
|
||||
def getfolders(self):
|
||||
if self.folders == None:
|
||||
self.folders = self._getfolders_scandir(self.root)
|
||||
return self.folders
|
||||
|
1
offlineimap/repository/__init__.py
Normal file
1
offlineimap/repository/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
__all__ = ['IMAP', 'Base', 'Maildir', 'LocalStatus']
|
45
offlineimap/syncmaster.py
Normal file
45
offlineimap/syncmaster.py
Normal file
@ -0,0 +1,45 @@
|
||||
# OfflineIMAP synchronization master code
|
||||
# Copyright (C) 2002 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 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(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)
|
||||
|
||||
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()
|
291
offlineimap/threadutil.py
Normal file
291
offlineimap/threadutil.py
Normal file
@ -0,0 +1,291 @@
|
||||
# Copyright (C) 2002, 2003 John Goerzen
|
||||
# Thread support module
|
||||
# <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 threading import *
|
||||
from StringIO import StringIO
|
||||
import sys, traceback, thread, profile
|
||||
from offlineimap.ui import UIBase # for getglobalui()
|
||||
|
||||
profiledir = None
|
||||
|
||||
def setprofiledir(newdir):
|
||||
global profiledir
|
||||
profiledir = newdir
|
||||
|
||||
######################################################################
|
||||
# General utilities
|
||||
######################################################################
|
||||
|
||||
def semaphorereset(semaphore, originalstate):
|
||||
"""Wait until the semaphore gets back to its original state -- all acquired
|
||||
resources released."""
|
||||
for i in range(originalstate):
|
||||
semaphore.acquire()
|
||||
# Now release these.
|
||||
for i in range(originalstate):
|
||||
semaphore.release()
|
||||
|
||||
def semaphorewait(semaphore):
|
||||
semaphore.acquire()
|
||||
semaphore.release()
|
||||
|
||||
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
|
||||
######################################################################
|
||||
|
||||
exitcondition = Condition(Lock())
|
||||
exitthreads = []
|
||||
inited = 0
|
||||
|
||||
def initexitnotify():
|
||||
"""Initialize the exit notify system. This MUST be called from the
|
||||
SAME THREAD that will call monitorloop BEFORE it calls monitorloop.
|
||||
This SHOULD be called before the main thread starts any other
|
||||
ExitNotifyThreads, or else it may miss the ability to catch the exit
|
||||
status from them!"""
|
||||
pass
|
||||
|
||||
def exitnotifymonitorloop(callback):
|
||||
"""Enter an infinite "monitoring" loop. The argument, callback,
|
||||
defines the function to call when an ExitNotifyThread has terminated.
|
||||
That function is called with a single argument -- the ExitNotifyThread
|
||||
that has terminated. The monitor will not continue to monitor for
|
||||
other threads until the function returns, so if it intends to perform
|
||||
long calculations, it should start a new thread itself -- but NOT
|
||||
an ExitNotifyThread, or else an infinite loop may result. Furthermore,
|
||||
the monitor will hold the lock all the while the other thread is waiting.
|
||||
"""
|
||||
global exitcondition, exitthreads
|
||||
while 1: # Loop forever.
|
||||
exitcondition.acquire()
|
||||
try:
|
||||
while not len(exitthreads):
|
||||
exitcondition.wait(1)
|
||||
|
||||
while len(exitthreads):
|
||||
callback(exitthreads.pop(0)) # Pull off in order added!
|
||||
finally:
|
||||
exitcondition.release()
|
||||
|
||||
def threadexited(thread):
|
||||
"""Called when a thread exits."""
|
||||
ui = UIBase.getglobalui()
|
||||
if thread.getExitCause() == 'EXCEPTION':
|
||||
if isinstance(thread.getExitException(), SystemExit):
|
||||
# Bring a SystemExit into the main thread.
|
||||
# Do not send it back to UI layer right now.
|
||||
# Maybe later send it to ui.terminate?
|
||||
raise SystemExit
|
||||
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)
|
||||
|
||||
class ExitNotifyThread(Thread):
|
||||
"""This class is designed to alert a "monitor" to the fact that a thread has
|
||||
exited and to provide for the ability for it to find out why."""
|
||||
def run(self):
|
||||
global exitcondition, exitthreads, profiledir
|
||||
self.threadid = thread.get_ident()
|
||||
try:
|
||||
if not profiledir: # normal case
|
||||
Thread.run(self)
|
||||
else:
|
||||
prof = profile.Profile()
|
||||
try:
|
||||
prof = prof.runctx("Thread.run(self)", globals(), locals())
|
||||
except SystemExit:
|
||||
pass
|
||||
prof.dump_stats( \
|
||||
profiledir + "/" + str(self.threadid) + "_" + \
|
||||
self.getName() + ".prof")
|
||||
except:
|
||||
self.setExitCause('EXCEPTION')
|
||||
self.setExitException(sys.exc_info()[1])
|
||||
sbuf = StringIO()
|
||||
traceback.print_exc(file = sbuf)
|
||||
self.setExitStackTrace(sbuf.getvalue())
|
||||
else:
|
||||
self.setExitCause('NORMAL')
|
||||
if not hasattr(self, 'exitmessage'):
|
||||
self.setExitMessage(None)
|
||||
exitcondition.acquire()
|
||||
exitthreads.append(self)
|
||||
exitcondition.notify()
|
||||
exitcondition.release()
|
||||
|
||||
def setExitCause(self, cause):
|
||||
self.exitcause = cause
|
||||
def getExitCause(self):
|
||||
"""Returns the cause of the exit, one of:
|
||||
'EXCEPTION' -- the thread aborted because of an exception
|
||||
'NORMAL' -- normal termination."""
|
||||
return self.exitcause
|
||||
def setExitException(self, exc):
|
||||
self.exitexception = exc
|
||||
def getExitException(self):
|
||||
"""If getExitCause() is 'EXCEPTION', holds the value from
|
||||
sys.exc_info()[1] for this exception."""
|
||||
return self.exitexception
|
||||
def setExitStackTrace(self, st):
|
||||
self.exitstacktrace = st
|
||||
def getExitStackTrace(self):
|
||||
"""If getExitCause() is 'EXCEPTION', returns a string representing
|
||||
the stack trace for this exception."""
|
||||
return self.exitstacktrace
|
||||
def setExitMessage(self, msg):
|
||||
"""Sets the exit message to be fetched by a subsequent call to
|
||||
getExitMessage. This message may be any object or type except
|
||||
None."""
|
||||
self.exitmessage = msg
|
||||
def getExitMessage(self):
|
||||
"""For any exit cause, returns the message previously set by
|
||||
a call to setExitMessage(), or None if there was no such message
|
||||
set."""
|
||||
return self.exitmessage
|
||||
|
||||
|
||||
######################################################################
|
||||
# Instance-limited threads
|
||||
######################################################################
|
||||
|
||||
instancelimitedsems = {}
|
||||
instancelimitedlock = Lock()
|
||||
|
||||
def initInstanceLimit(instancename, instancemax):
|
||||
"""Initialize the instance-limited thread implementation to permit
|
||||
up to intancemax threads with the given instancename."""
|
||||
instancelimitedlock.acquire()
|
||||
if not instancelimitedsems.has_key(instancename):
|
||||
instancelimitedsems[instancename] = BoundedSemaphore(instancemax)
|
||||
instancelimitedlock.release()
|
||||
|
||||
class InstanceLimitedThread(ExitNotifyThread):
|
||||
def __init__(self, instancename, *args, **kwargs):
|
||||
self.instancename = instancename
|
||||
|
||||
apply(ExitNotifyThread.__init__, (self,) + args, kwargs)
|
||||
|
||||
def start(self):
|
||||
instancelimitedsems[self.instancename].acquire()
|
||||
ExitNotifyThread.start(self)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
ExitNotifyThread.run(self)
|
||||
finally:
|
||||
instancelimitedsems[self.instancename].release()
|
||||
|
||||
|
||||
######################################################################
|
||||
# Multi-lock -- capable of handling a single thread requesting a lock
|
||||
# multiple times
|
||||
######################################################################
|
||||
|
||||
class MultiLock:
|
||||
def __init__(self):
|
||||
self.lock = Lock()
|
||||
self.statuslock = Lock()
|
||||
self.locksheld = {}
|
||||
|
||||
def acquire(self):
|
||||
"""Obtain a lock. Provides nice support for a single
|
||||
thread trying to lock it several times -- as may be the case
|
||||
if one I/O-using object calls others, while wanting to make it all
|
||||
an atomic operation. Keeps a "lock request count" for the current
|
||||
thread, and acquires the lock when it goes above zero, releases when
|
||||
it goes below one.
|
||||
|
||||
This call is always blocking."""
|
||||
|
||||
# First, check to see if this thread already has a lock.
|
||||
# If so, increment the lock count and just return.
|
||||
self.statuslock.acquire()
|
||||
try:
|
||||
threadid = thread.get_ident()
|
||||
|
||||
if threadid in self.locksheld:
|
||||
self.locksheld[threadid] += 1
|
||||
return
|
||||
else:
|
||||
# This is safe because it is a per-thread structure
|
||||
self.locksheld[threadid] = 1
|
||||
finally:
|
||||
self.statuslock.release()
|
||||
self.lock.acquire()
|
||||
|
||||
def release(self):
|
||||
self.statuslock.acquire()
|
||||
try:
|
||||
threadid = thread.get_ident()
|
||||
if self.locksheld[threadid] > 1:
|
||||
self.locksheld[threadid] -= 1
|
||||
return
|
||||
else:
|
||||
del self.locksheld[threadid]
|
||||
self.lock.release()
|
||||
finally:
|
||||
self.statuslock.release()
|
||||
|
||||
|
132
offlineimap/ui/Blinkenlights.py
Normal file
132
offlineimap/ui/Blinkenlights.py
Normal file
@ -0,0 +1,132 @@
|
||||
# Blinkenlights base classes
|
||||
# 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 threading import *
|
||||
from offlineimap.ui.UIBase import UIBase
|
||||
import thread
|
||||
from offlineimap.threadutil import MultiLock
|
||||
|
||||
class BlinkenBase:
|
||||
"""This is a mix-in class that should be mixed in with either UIBase
|
||||
or another appropriate base class. The Tk interface, for instance,
|
||||
will probably mix it in with VerboseUI."""
|
||||
|
||||
def acct(s, accountname):
|
||||
s.gettf().setcolor('purple')
|
||||
s.__class__.__bases__[-1].acct(s, accountname)
|
||||
|
||||
def connecting(s, hostname, port):
|
||||
s.gettf().setcolor('gray')
|
||||
s.__class__.__bases__[-1].connecting(s, hostname, port)
|
||||
|
||||
def syncfolders(s, srcrepos, destrepos):
|
||||
s.gettf().setcolor('blue')
|
||||
s.__class__.__bases__[-1].syncfolders(s, srcrepos, destrepos)
|
||||
|
||||
def syncingfolder(s, srcrepos, srcfolder, destrepos, destfolder):
|
||||
s.gettf().setcolor('cyan')
|
||||
s.__class__.__bases__[-1].syncingfolder(s, srcrepos, srcfolder, destrepos, destfolder)
|
||||
|
||||
def loadmessagelist(s, repos, folder):
|
||||
s.gettf().setcolor('green')
|
||||
s._msg("Scanning folder [%s/%s]" % (s.getnicename(repos),
|
||||
folder.getvisiblename()))
|
||||
|
||||
def syncingmessages(s, sr, sf, dr, df):
|
||||
s.gettf().setcolor('blue')
|
||||
s.__class__.__bases__[-1].syncingmessages(s, sr, sf, dr, df)
|
||||
|
||||
def copyingmessage(s, uid, src, destlist):
|
||||
s.gettf().setcolor('orange')
|
||||
s.__class__.__bases__[-1].copyingmessage(s, uid, src, destlist)
|
||||
|
||||
def deletingmessages(s, uidlist, destlist):
|
||||
s.gettf().setcolor('red')
|
||||
s.__class__.__bases__[-1].deletingmessages(s, uidlist, destlist)
|
||||
|
||||
def deletingmessage(s, uid, destlist):
|
||||
s.gettf().setcolor('red')
|
||||
s.__class__.__bases__[-1].deletingmessage(s, uid, destlist)
|
||||
|
||||
def addingflags(s, uidlist, flags, destlist):
|
||||
s.gettf().setcolor('yellow')
|
||||
s.__class__.__bases__[-1].addingflags(s, uidlist, flags, destlist)
|
||||
|
||||
def deletingflags(s, uidlist, flags, destlist):
|
||||
s.gettf().setcolor('pink')
|
||||
s.__class__.__bases__[-1].deletingflags(s, uidlist, flags, destlist)
|
||||
|
||||
def init_banner(s):
|
||||
s.availablethreadframes = {}
|
||||
s.threadframes = {}
|
||||
s.tflock = MultiLock()
|
||||
|
||||
def threadExited(s, thread):
|
||||
threadid = thread.threadid
|
||||
accountname = s.getthreadaccount(thread)
|
||||
s.tflock.acquire()
|
||||
try:
|
||||
if threadid in s.threadframes[accountname]:
|
||||
tf = s.threadframes[accountname][threadid]
|
||||
del s.threadframes[accountname][threadid]
|
||||
s.availablethreadframes[accountname].append(tf)
|
||||
tf.setthread(None)
|
||||
finally:
|
||||
s.tflock.release()
|
||||
|
||||
UIBase.threadExited(s, thread)
|
||||
|
||||
def gettf(s):
|
||||
threadid = thread.get_ident()
|
||||
accountname = s.getthreadaccount()
|
||||
|
||||
s.tflock.acquire()
|
||||
|
||||
try:
|
||||
if not accountname in s.threadframes:
|
||||
s.threadframes[accountname] = {}
|
||||
|
||||
if threadid in s.threadframes[accountname]:
|
||||
return s.threadframes[accountname][threadid]
|
||||
|
||||
if not accountname in s.availablethreadframes:
|
||||
s.availablethreadframes[accountname] = []
|
||||
|
||||
if len(s.availablethreadframes[accountname]):
|
||||
tf = s.availablethreadframes[accountname].pop(0)
|
||||
tf.setthread(currentThread())
|
||||
else:
|
||||
tf = s.getaccountframe().getnewthreadframe()
|
||||
s.threadframes[accountname][threadid] = tf
|
||||
return tf
|
||||
finally:
|
||||
s.tflock.release()
|
||||
|
||||
def sleep(s, sleepsecs):
|
||||
s.gettf().setcolor('red')
|
||||
s.getaccountframe().startsleep(sleepsecs)
|
||||
UIBase.sleep(s, sleepsecs)
|
||||
|
||||
def sleeping(s, sleepsecs, remainingsecs):
|
||||
if remainingsecs and s.gettf().getcolor() == 'black':
|
||||
s.gettf().setcolor('red')
|
||||
else:
|
||||
s.gettf().setcolor('black')
|
||||
return s.getaccountframe().sleeping(sleepsecs, remainingsecs)
|
||||
|
||||
|
589
offlineimap/ui/Curses.py
Normal file
589
offlineimap/ui/Curses.py
Normal file
@ -0,0 +1,589 @@
|
||||
# Curses-based interfaces
|
||||
# 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 Blinkenlights import BlinkenBase
|
||||
from UIBase import UIBase
|
||||
from threading import *
|
||||
import thread, time, sys, os, signal, time
|
||||
from offlineimap import version, threadutil
|
||||
from offlineimap.threadutil import MultiLock
|
||||
|
||||
import curses, curses.panel, curses.textpad, curses.wrapper
|
||||
|
||||
acctkeys = '1234567890abcdefghijklmnoprstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-=;/.,'
|
||||
|
||||
class CursesUtil:
|
||||
def __init__(self):
|
||||
self.pairlock = Lock()
|
||||
self.iolock = MultiLock()
|
||||
self.start()
|
||||
|
||||
def initpairs(self):
|
||||
self.pairlock.acquire()
|
||||
try:
|
||||
self.pairs = {self._getpairindex(curses.COLOR_WHITE,
|
||||
curses.COLOR_BLACK): 0}
|
||||
self.nextpair = 1
|
||||
finally:
|
||||
self.pairlock.release()
|
||||
|
||||
def lock(self):
|
||||
self.iolock.acquire()
|
||||
|
||||
def unlock(self):
|
||||
self.iolock.release()
|
||||
|
||||
def locked(self, target, *args, **kwargs):
|
||||
"""Perform an operation with full locking."""
|
||||
self.lock()
|
||||
try:
|
||||
apply(target, args, kwargs)
|
||||
finally:
|
||||
self.unlock()
|
||||
|
||||
def refresh(self):
|
||||
def lockedstuff():
|
||||
curses.panel.update_panels()
|
||||
curses.doupdate()
|
||||
self.locked(lockedstuff)
|
||||
|
||||
def isactive(self):
|
||||
return hasattr(self, 'stdscr')
|
||||
|
||||
def _getpairindex(self, fg, bg):
|
||||
return '%d/%d' % (fg,bg)
|
||||
|
||||
def getpair(self, fg, bg):
|
||||
if not self.has_color:
|
||||
return 0
|
||||
pindex = self._getpairindex(fg, bg)
|
||||
self.pairlock.acquire()
|
||||
try:
|
||||
if self.pairs.has_key(pindex):
|
||||
return curses.color_pair(self.pairs[pindex])
|
||||
else:
|
||||
self.pairs[pindex] = self.nextpair
|
||||
curses.init_pair(self.nextpair, fg, bg)
|
||||
self.nextpair += 1
|
||||
return curses.color_pair(self.nextpair - 1)
|
||||
finally:
|
||||
self.pairlock.release()
|
||||
|
||||
def start(self):
|
||||
self.stdscr = curses.initscr()
|
||||
curses.noecho()
|
||||
curses.cbreak()
|
||||
self.stdscr.keypad(1)
|
||||
try:
|
||||
curses.start_color()
|
||||
self.has_color = curses.has_colors()
|
||||
except:
|
||||
self.has_color = 0
|
||||
|
||||
self.oldcursor = None
|
||||
try:
|
||||
self.oldcursor = curses.curs_set(0)
|
||||
except:
|
||||
pass
|
||||
|
||||
self.stdscr.clear()
|
||||
self.stdscr.refresh()
|
||||
(self.height, self.width) = self.stdscr.getmaxyx()
|
||||
self.initpairs()
|
||||
|
||||
def stop(self):
|
||||
if not hasattr(self, 'stdscr'):
|
||||
return
|
||||
#self.stdscr.addstr(self.height - 1, 0, "\n",
|
||||
# self.getpair(curses.COLOR_WHITE,
|
||||
# curses.COLOR_BLACK))
|
||||
if self.oldcursor != None:
|
||||
curses.curs_set(self.oldcursor)
|
||||
self.stdscr.refresh()
|
||||
self.stdscr.keypad(0)
|
||||
curses.nocbreak()
|
||||
curses.echo()
|
||||
curses.endwin()
|
||||
del self.stdscr
|
||||
|
||||
def reset(self):
|
||||
self.stop()
|
||||
self.start()
|
||||
|
||||
class CursesAccountFrame:
|
||||
def __init__(s, master, accountname):
|
||||
s.c = master
|
||||
s.children = []
|
||||
s.accountname = accountname
|
||||
|
||||
def drawleadstr(s, secs = None):
|
||||
if secs == None:
|
||||
acctstr = '%s: [active] %13.13s: ' % (s.key, s.accountname)
|
||||
else:
|
||||
acctstr = '%s: [%3d:%02d] %13.13s: ' % (s.key,
|
||||
secs / 60, secs % 60,
|
||||
s.accountname)
|
||||
s.c.locked(s.window.addstr, 0, 0, acctstr)
|
||||
s.location = len(acctstr)
|
||||
|
||||
def setwindow(s, window, key):
|
||||
s.window = window
|
||||
s.key = key
|
||||
s.drawleadstr()
|
||||
for child in s.children:
|
||||
child.update(window, 0, s.location)
|
||||
s.location += 1
|
||||
|
||||
def getnewthreadframe(s):
|
||||
tf = CursesThreadFrame(s.c, s.window, 0, s.location)
|
||||
s.location += 1
|
||||
s.children.append(tf)
|
||||
return tf
|
||||
|
||||
def startsleep(s, sleepsecs):
|
||||
s.sleeping_abort = 0
|
||||
|
||||
def sleeping(s, sleepsecs, remainingsecs):
|
||||
if remainingsecs:
|
||||
s.c.lock()
|
||||
try:
|
||||
s.drawleadstr(remainingsecs)
|
||||
s.window.refresh()
|
||||
finally:
|
||||
s.c.unlock()
|
||||
time.sleep(sleepsecs)
|
||||
else:
|
||||
s.c.lock()
|
||||
try:
|
||||
s.drawleadstr()
|
||||
s.window.refresh()
|
||||
finally:
|
||||
s.c.unlock()
|
||||
return s.sleeping_abort
|
||||
|
||||
def syncnow(s):
|
||||
s.sleeping_abort = 1
|
||||
|
||||
class CursesThreadFrame:
|
||||
def __init__(s, master, window, y, x):
|
||||
"""master should be a CursesUtil object."""
|
||||
s.c = master
|
||||
s.window = window
|
||||
s.x = x
|
||||
s.y = y
|
||||
s.colors = []
|
||||
bg = curses.COLOR_BLACK
|
||||
s.colormap = {'black': s.c.getpair(curses.COLOR_BLACK, bg),
|
||||
'gray': s.c.getpair(curses.COLOR_WHITE, bg),
|
||||
'white': curses.A_BOLD | s.c.getpair(curses.COLOR_WHITE, bg),
|
||||
'blue': s.c.getpair(curses.COLOR_BLUE, bg),
|
||||
'red': s.c.getpair(curses.COLOR_RED, bg),
|
||||
'purple': s.c.getpair(curses.COLOR_MAGENTA, bg),
|
||||
'cyan': s.c.getpair(curses.COLOR_CYAN, bg),
|
||||
'green': s.c.getpair(curses.COLOR_GREEN, bg),
|
||||
'orange': s.c.getpair(curses.COLOR_YELLOW, bg),
|
||||
'yellow': curses.A_BOLD | s.c.getpair(curses.COLOR_YELLOW, bg),
|
||||
'pink': curses.A_BOLD | s.c.getpair(curses.COLOR_RED, bg)}
|
||||
#s.setcolor('gray')
|
||||
s.setcolor('black')
|
||||
|
||||
def setcolor(self, color):
|
||||
self.color = self.colormap[color]
|
||||
self.colorname = color
|
||||
self.display()
|
||||
|
||||
def display(self):
|
||||
def lockedstuff():
|
||||
if self.getcolor() == 'black':
|
||||
self.window.addstr(self.y, self.x, ' ', self.color)
|
||||
else:
|
||||
self.window.addstr(self.y, self.x, '.', self.color)
|
||||
self.c.stdscr.move(self.c.height - 1, self.c.width - 1)
|
||||
self.window.refresh()
|
||||
self.c.locked(lockedstuff)
|
||||
|
||||
def getcolor(self):
|
||||
return self.colorname
|
||||
|
||||
def getcolorpair(self):
|
||||
return self.color
|
||||
|
||||
def update(self, window, y, x):
|
||||
self.window = window
|
||||
self.y = y
|
||||
self.x = x
|
||||
self.display()
|
||||
|
||||
def setthread(self, newthread):
|
||||
self.setcolor('black')
|
||||
#if newthread:
|
||||
# self.setcolor('gray')
|
||||
#else:
|
||||
# self.setcolor('black')
|
||||
|
||||
class InputHandler:
|
||||
def __init__(s, util):
|
||||
s.c = util
|
||||
s.bgchar = None
|
||||
s.inputlock = Lock()
|
||||
s.lockheld = 0
|
||||
s.statuslock = Lock()
|
||||
s.startup = Event()
|
||||
s.startthread()
|
||||
|
||||
def startthread(s):
|
||||
s.thread = threadutil.ExitNotifyThread(target = s.bgreaderloop,
|
||||
name = "InputHandler loop")
|
||||
s.thread.setDaemon(1)
|
||||
s.thread.start()
|
||||
|
||||
def bgreaderloop(s):
|
||||
while 1:
|
||||
s.statuslock.acquire()
|
||||
if s.lockheld or s.bgchar == None:
|
||||
s.statuslock.release()
|
||||
s.startup.wait()
|
||||
else:
|
||||
s.statuslock.release()
|
||||
ch = s.c.stdscr.getch()
|
||||
s.statuslock.acquire()
|
||||
try:
|
||||
if s.lockheld or s.bgchar == None:
|
||||
curses.ungetch(ch)
|
||||
else:
|
||||
s.bgchar(ch)
|
||||
finally:
|
||||
s.statuslock.release()
|
||||
|
||||
def set_bgchar(s, callback):
|
||||
"""Sets a "background" character handler. If a key is pressed
|
||||
while not doing anything else, it will be passed to this handler.
|
||||
|
||||
callback is a function taking a single arg -- the char pressed.
|
||||
|
||||
If callback is None, clears the request."""
|
||||
s.statuslock.acquire()
|
||||
oldhandler = s.bgchar
|
||||
newhandler = callback
|
||||
s.bgchar = callback
|
||||
|
||||
if oldhandler and not newhandler:
|
||||
pass
|
||||
if newhandler and not oldhandler:
|
||||
s.startup.set()
|
||||
|
||||
s.statuslock.release()
|
||||
|
||||
def input_acquire(s):
|
||||
"""Call this method when you want exclusive input control.
|
||||
Make sure to call input_release afterwards!
|
||||
"""
|
||||
|
||||
s.inputlock.acquire()
|
||||
s.statuslock.acquire()
|
||||
s.lockheld = 1
|
||||
s.statuslock.release()
|
||||
|
||||
def input_release(s):
|
||||
"""Call this method when you are done getting input."""
|
||||
s.statuslock.acquire()
|
||||
s.lockheld = 0
|
||||
s.statuslock.release()
|
||||
s.inputlock.release()
|
||||
s.startup.set()
|
||||
|
||||
class Blinkenlights(BlinkenBase, UIBase):
|
||||
def init_banner(s):
|
||||
s.af = {}
|
||||
s.aflock = Lock()
|
||||
s.c = CursesUtil()
|
||||
s.text = []
|
||||
BlinkenBase.init_banner(s)
|
||||
s.setupwindows()
|
||||
s.inputhandler = InputHandler(s.c)
|
||||
s.gettf().setcolor('red')
|
||||
s._msg(version.banner)
|
||||
s.inputhandler.set_bgchar(s.keypress)
|
||||
signal.signal(signal.SIGWINCH, s.resizehandler)
|
||||
s.resizelock = Lock()
|
||||
s.resizecount = 0
|
||||
|
||||
def resizehandler(s, signum, frame):
|
||||
s.resizeterm()
|
||||
|
||||
def resizeterm(s, dosleep = 1):
|
||||
if not s.resizelock.acquire(0):
|
||||
s.resizecount += 1
|
||||
return
|
||||
signal.signal(signal.SIGWINCH, signal.SIG_IGN)
|
||||
s.aflock.acquire()
|
||||
s.c.lock()
|
||||
s.resizecount += 1
|
||||
while s.resizecount:
|
||||
s.c.reset()
|
||||
s.setupwindows()
|
||||
s.resizecount -= 1
|
||||
s.c.unlock()
|
||||
s.aflock.release()
|
||||
s.resizelock.release()
|
||||
signal.signal(signal.SIGWINCH, s.resizehandler)
|
||||
if dosleep:
|
||||
time.sleep(1)
|
||||
s.resizeterm(0)
|
||||
|
||||
def isusable(s):
|
||||
# Not a terminal? Can't use curses.
|
||||
if not sys.stdout.isatty() and sys.stdin.isatty():
|
||||
return 0
|
||||
|
||||
# No TERM specified? Can't use curses.
|
||||
try:
|
||||
if not len(os.environ['TERM']):
|
||||
return 0
|
||||
except: return 0
|
||||
|
||||
# ncurses doesn't want to start? Can't use curses.
|
||||
# This test is nasty because initscr() actually EXITS on error.
|
||||
# grr.
|
||||
|
||||
pid = os.fork()
|
||||
if pid:
|
||||
# parent
|
||||
return not os.WEXITSTATUS(os.waitpid(pid, 0)[1])
|
||||
else:
|
||||
# child
|
||||
curses.initscr()
|
||||
curses.endwin()
|
||||
# If we didn't die by here, indicate success.
|
||||
sys.exit(0)
|
||||
|
||||
def keypress(s, key):
|
||||
if key > 255:
|
||||
return
|
||||
|
||||
if chr(key) == 'q':
|
||||
# Request to quit.
|
||||
s.terminate()
|
||||
|
||||
try:
|
||||
index = acctkeys.index(chr(key))
|
||||
except ValueError:
|
||||
# Key not a valid one: exit.
|
||||
return
|
||||
|
||||
if index >= len(s.hotkeys):
|
||||
# Not in our list of valid hotkeys.
|
||||
return
|
||||
|
||||
# Trying to end sleep somewhere.
|
||||
|
||||
s.getaccountframe(s.hotkeys[index]).syncnow()
|
||||
|
||||
def getpass(s, accountname, config, errmsg = None):
|
||||
s.inputhandler.input_acquire()
|
||||
|
||||
# See comment on _msg for info on why both locks are obtained.
|
||||
|
||||
s.tflock.acquire()
|
||||
s.c.lock()
|
||||
try:
|
||||
s.gettf().setcolor('white')
|
||||
s._addline(" *** Input Required", s.gettf().getcolorpair())
|
||||
s._addline(" *** Please enter password for account %s: " % accountname,
|
||||
s.gettf().getcolorpair())
|
||||
s.logwindow.refresh()
|
||||
password = s.logwindow.getstr()
|
||||
finally:
|
||||
s.tflock.release()
|
||||
s.c.unlock()
|
||||
s.inputhandler.input_release()
|
||||
return password
|
||||
|
||||
def setupwindows(s):
|
||||
s.c.lock()
|
||||
try:
|
||||
s.bannerwindow = curses.newwin(1, s.c.width, 0, 0)
|
||||
s.setupwindow_drawbanner()
|
||||
s.logheight = s.c.height - 1 - len(s.af.keys())
|
||||
s.logwindow = curses.newwin(s.logheight, s.c.width, 1, 0)
|
||||
s.logwindow.idlok(1)
|
||||
s.logwindow.scrollok(1)
|
||||
s.logwindow.move(s.logheight - 1, 0)
|
||||
s.setupwindow_drawlog()
|
||||
accounts = s.af.keys()
|
||||
accounts.sort()
|
||||
accounts.reverse()
|
||||
|
||||
pos = s.c.height - 1
|
||||
index = 0
|
||||
s.hotkeys = []
|
||||
for account in accounts:
|
||||
accountwindow = curses.newwin(1, s.c.width, pos, 0)
|
||||
s.af[account].setwindow(accountwindow, acctkeys[index])
|
||||
s.hotkeys.append(account)
|
||||
index += 1
|
||||
pos -= 1
|
||||
|
||||
curses.doupdate()
|
||||
finally:
|
||||
s.c.unlock()
|
||||
|
||||
def setupwindow_drawbanner(s):
|
||||
if s.c.has_color:
|
||||
color = s.c.getpair(curses.COLOR_WHITE, curses.COLOR_BLUE) | \
|
||||
curses.A_BOLD
|
||||
else:
|
||||
color = curses.A_REVERSE
|
||||
s.bannerwindow.bkgd(' ', color) # Fill background with that color
|
||||
s.bannerwindow.addstr("%s %s" % (version.productname,
|
||||
version.versionstr))
|
||||
s.bannerwindow.addstr(0, s.bannerwindow.getmaxyx()[1] - len(version.copyright) - 1,
|
||||
version.copyright)
|
||||
|
||||
s.bannerwindow.noutrefresh()
|
||||
|
||||
def setupwindow_drawlog(s):
|
||||
if s.c.has_color:
|
||||
color = s.c.getpair(curses.COLOR_WHITE, curses.COLOR_BLACK)
|
||||
else:
|
||||
color = curses.A_NORMAL
|
||||
s.logwindow.bkgd(' ', color)
|
||||
for line, color in s.text:
|
||||
s.logwindow.addstr("\n" + line, color)
|
||||
s.logwindow.noutrefresh()
|
||||
|
||||
def getaccountframe(s, accountname = None):
|
||||
if accountname == None:
|
||||
accountname = s.getthreadaccount()
|
||||
s.aflock.acquire()
|
||||
try:
|
||||
if accountname in s.af:
|
||||
return s.af[accountname]
|
||||
|
||||
# New one.
|
||||
s.af[accountname] = CursesAccountFrame(s.c, accountname)
|
||||
s.c.lock()
|
||||
try:
|
||||
s.c.reset()
|
||||
s.setupwindows()
|
||||
finally:
|
||||
s.c.unlock()
|
||||
finally:
|
||||
s.aflock.release()
|
||||
return s.af[accountname]
|
||||
|
||||
|
||||
def _display(s, msg, color = None):
|
||||
if "\n" in msg:
|
||||
for thisline in msg.split("\n"):
|
||||
s._msg(thisline)
|
||||
return
|
||||
|
||||
# We must acquire both locks. Otherwise, deadlock can result.
|
||||
# This can happen if one thread calls _msg (locking curses, then
|
||||
# tf) and another tries to set the color (locking tf, then curses)
|
||||
#
|
||||
# By locking both up-front here, in this order, we prevent deadlock.
|
||||
|
||||
s.tflock.acquire()
|
||||
s.c.lock()
|
||||
try:
|
||||
if not s.c.isactive():
|
||||
# For dumping out exceptions and stuff.
|
||||
print msg
|
||||
return
|
||||
if color:
|
||||
s.gettf().setcolor(color)
|
||||
s._addline(msg, s.gettf().getcolorpair())
|
||||
s.logwindow.refresh()
|
||||
finally:
|
||||
s.c.unlock()
|
||||
s.tflock.release()
|
||||
|
||||
def _addline(s, msg, color):
|
||||
s.c.lock()
|
||||
try:
|
||||
s.logwindow.addstr("\n" + msg, color)
|
||||
s.text.append((msg, color))
|
||||
while len(s.text) > s.logheight:
|
||||
s.text = s.text[1:]
|
||||
finally:
|
||||
s.c.unlock()
|
||||
|
||||
def terminate(s, exitstatus = 0):
|
||||
s.c.stop()
|
||||
UIBase.terminate(s, exitstatus)
|
||||
|
||||
def threadException(s, thread):
|
||||
s.c.stop()
|
||||
UIBase.threadException(s, thread)
|
||||
|
||||
def mainException(s):
|
||||
s.c.stop()
|
||||
UIBase.mainException(s)
|
||||
|
||||
def sleep(s, sleepsecs):
|
||||
s.gettf().setcolor('red')
|
||||
s._msg("Next sync in %d:%02d" % (sleepsecs / 60, sleepsecs % 60))
|
||||
BlinkenBase.sleep(s, sleepsecs)
|
||||
|
||||
if __name__ == '__main__':
|
||||
x = Blinkenlights(None)
|
||||
x.init_banner()
|
||||
import time
|
||||
time.sleep(5)
|
||||
x.c.stop()
|
||||
fgs = {'black': curses.COLOR_BLACK, 'red': curses.COLOR_RED,
|
||||
'green': curses.COLOR_GREEN, 'yellow': curses.COLOR_YELLOW,
|
||||
'blue': curses.COLOR_BLUE, 'magenta': curses.COLOR_MAGENTA,
|
||||
'cyan': curses.COLOR_CYAN, 'white': curses.COLOR_WHITE}
|
||||
|
||||
x = CursesUtil()
|
||||
win1 = curses.newwin(x.height, x.width / 4 - 1, 0, 0)
|
||||
win1.addstr("Black/normal\n")
|
||||
for name, fg in fgs.items():
|
||||
win1.addstr("%s\n" % name, x.getpair(fg, curses.COLOR_BLACK))
|
||||
win2 = curses.newwin(x.height, x.width / 4 - 1, 0, win1.getmaxyx()[1])
|
||||
win2.addstr("Blue/normal\n")
|
||||
for name, fg in fgs.items():
|
||||
win2.addstr("%s\n" % name, x.getpair(fg, curses.COLOR_BLUE))
|
||||
win3 = curses.newwin(x.height, x.width / 4 - 1, 0, win1.getmaxyx()[1] +
|
||||
win2.getmaxyx()[1])
|
||||
win3.addstr("Black/bright\n")
|
||||
for name, fg in fgs.items():
|
||||
win3.addstr("%s\n" % name, x.getpair(fg, curses.COLOR_BLACK) | \
|
||||
curses.A_BOLD)
|
||||
win4 = curses.newwin(x.height, x.width / 4 - 1, 0, win1.getmaxyx()[1] * 3)
|
||||
win4.addstr("Blue/bright\n")
|
||||
for name, fg in fgs.items():
|
||||
win4.addstr("%s\n" % name, x.getpair(fg, curses.COLOR_BLUE) | \
|
||||
curses.A_BOLD)
|
||||
|
||||
|
||||
win1.refresh()
|
||||
win2.refresh()
|
||||
win3.refresh()
|
||||
win4.refresh()
|
||||
x.stdscr.refresh()
|
||||
import time
|
||||
time.sleep(5)
|
||||
x.stop()
|
||||
print x.has_color
|
||||
print x.height
|
||||
print x.width
|
||||
|
48
offlineimap/ui/Noninteractive.py
Normal file
48
offlineimap/ui/Noninteractive.py
Normal file
@ -0,0 +1,48 @@
|
||||
# Noninteractive UI
|
||||
# Copyright (C) 2002 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
|
||||
|
||||
import sys, time
|
||||
from UIBase import UIBase
|
||||
|
||||
class Basic(UIBase):
|
||||
def getpass(s, accountname, config, errmsg = None):
|
||||
raise NotImplementedError, "Prompting for a password is not supported in noninteractive mode."
|
||||
|
||||
def _display(s, msg):
|
||||
print msg
|
||||
sys.stdout.flush()
|
||||
|
||||
def warn(s, msg, minor = 0):
|
||||
warntxt = 'WARNING'
|
||||
if minor:
|
||||
warntxt = 'warning'
|
||||
sys.stderr.write(warntxt + ": " + str(msg) + "\n")
|
||||
|
||||
def sleep(s, sleepsecs):
|
||||
if s.verbose >= 0:
|
||||
s._msg("Sleeping for %d:%02d" % (sleepsecs / 60, sleepsecs % 60))
|
||||
UIBase.sleep(s, sleepsecs)
|
||||
|
||||
def sleeping(s, sleepsecs, remainingsecs):
|
||||
if sleepsecs > 0:
|
||||
time.sleep(sleepsecs)
|
||||
return 0
|
||||
|
||||
class Quiet(Basic):
|
||||
def __init__(s, config, verbose = -1):
|
||||
Basic.__init__(s, config, verbose)
|
60
offlineimap/ui/TTY.py
Normal file
60
offlineimap/ui/TTY.py
Normal file
@ -0,0 +1,60 @@
|
||||
# TTY UI
|
||||
# Copyright (C) 2002 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 UIBase import UIBase
|
||||
from getpass import getpass
|
||||
import select, sys
|
||||
from threading import *
|
||||
|
||||
class TTYUI(UIBase):
|
||||
def __init__(s, config, verbose = 0):
|
||||
UIBase.__init__(s, config, verbose)
|
||||
s.iswaiting = 0
|
||||
s.outputlock = Lock()
|
||||
|
||||
def isusable(s):
|
||||
return sys.stdout.isatty() and sys.stdin.isatty()
|
||||
|
||||
def _display(s, msg):
|
||||
s.outputlock.acquire()
|
||||
try:
|
||||
if (currentThread().getName() == 'MainThread'):
|
||||
print msg
|
||||
else:
|
||||
print "%s:\n %s" % (currentThread().getName(), msg)
|
||||
sys.stdout.flush()
|
||||
finally:
|
||||
s.outputlock.release()
|
||||
|
||||
def getpass(s, accountname, config, errmsg = None):
|
||||
if errmsg:
|
||||
s._msg("%s: %s" % (accountname, errmsg))
|
||||
s.outputlock.acquire()
|
||||
try:
|
||||
return getpass("%s: Enter password: " % accountname)
|
||||
finally:
|
||||
s.outputlock.release()
|
||||
|
||||
def mainException(s):
|
||||
if isinstance(sys.exc_info()[1], KeyboardInterrupt) and \
|
||||
s.iswaiting:
|
||||
sys.stdout.write("Timer interrupted at user request; program terminating. \n")
|
||||
s.terminate()
|
||||
else:
|
||||
UIBase.mainException(s)
|
||||
|
536
offlineimap/ui/Tk.py
Normal file
536
offlineimap/ui/Tk.py
Normal file
@ -0,0 +1,536 @@
|
||||
# Tk UI
|
||||
# Copyright (C) 2002, 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 __future__ import nested_scopes
|
||||
|
||||
from Tkinter import *
|
||||
import tkFont
|
||||
from threading import *
|
||||
import thread, traceback, time, threading
|
||||
from StringIO import StringIO
|
||||
from ScrolledText import ScrolledText
|
||||
from offlineimap import threadutil, version
|
||||
from Queue import Queue
|
||||
from UIBase import UIBase
|
||||
from offlineimap.ui.Blinkenlights import BlinkenBase
|
||||
|
||||
usabletest = None
|
||||
|
||||
class PasswordDialog:
|
||||
def __init__(self, accountname, config, master=None, errmsg = None):
|
||||
self.top = Toplevel(master)
|
||||
self.top.title(version.productname + " Password Entry")
|
||||
text = ''
|
||||
if errmsg:
|
||||
text = '%s: %s\n' % (accountname, errmsg)
|
||||
text += "%s: Enter password: " % accountname
|
||||
self.label = Label(self.top, text = text)
|
||||
self.label.pack()
|
||||
|
||||
self.entry = Entry(self.top, show='*')
|
||||
self.entry.bind("<Return>", self.ok)
|
||||
self.entry.pack()
|
||||
self.entry.focus_force()
|
||||
|
||||
self.button = Button(self.top, text = "OK", command=self.ok)
|
||||
self.button.pack()
|
||||
|
||||
self.entry.focus_force()
|
||||
self.top.wait_window(self.label)
|
||||
|
||||
def ok(self, args = None):
|
||||
self.password = self.entry.get()
|
||||
self.top.destroy()
|
||||
|
||||
def getpassword(self):
|
||||
return self.password
|
||||
|
||||
class TextOKDialog:
|
||||
def __init__(self, title, message, blocking = 1, master = None):
|
||||
if not master:
|
||||
self.top = Tk()
|
||||
else:
|
||||
self.top = Toplevel(master)
|
||||
self.top.title(title)
|
||||
self.text = ScrolledText(self.top, font = "Courier 10")
|
||||
self.text.pack()
|
||||
self.text.insert(END, message)
|
||||
self.text['state'] = DISABLED
|
||||
self.button = Button(self.top, text = "OK", command=self.ok)
|
||||
self.button.pack()
|
||||
|
||||
if blocking:
|
||||
self.top.wait_window(self.button)
|
||||
|
||||
def ok(self):
|
||||
self.top.destroy()
|
||||
|
||||
|
||||
|
||||
class ThreadFrame(Frame):
|
||||
def __init__(self, master=None):
|
||||
self.threadextraframe = None
|
||||
self.thread = currentThread()
|
||||
self.threadid = thread.get_ident()
|
||||
Frame.__init__(self, master, relief = RIDGE, borderwidth = 2)
|
||||
self.pack(fill = 'x')
|
||||
self.threadlabel = Label(self, foreground = '#FF0000',
|
||||
text ="Thread %d (%s)" % (self.threadid,
|
||||
self.thread.getName()))
|
||||
self.threadlabel.pack()
|
||||
self.setthread(currentThread())
|
||||
|
||||
self.account = "Unknown"
|
||||
self.mailbox = "Unknown"
|
||||
self.loclabel = Label(self,
|
||||
text = "Account/mailbox information unknown")
|
||||
#self.loclabel.pack()
|
||||
|
||||
self.updateloclabel()
|
||||
|
||||
self.message = Label(self, text="Messages will appear here.\n",
|
||||
foreground = '#0000FF')
|
||||
self.message.pack(fill = 'x')
|
||||
|
||||
def setthread(self, newthread):
|
||||
if newthread:
|
||||
self.threadlabel['text'] = newthread.getName()
|
||||
else:
|
||||
self.threadlabel['text'] = "No thread"
|
||||
self.destroythreadextraframe()
|
||||
|
||||
def destroythreadextraframe(self):
|
||||
if self.threadextraframe:
|
||||
self.threadextraframe.destroy()
|
||||
self.threadextraframe = None
|
||||
|
||||
def getthreadextraframe(self):
|
||||
if self.threadextraframe:
|
||||
return self.threadextraframe
|
||||
self.threadextraframe = Frame(self)
|
||||
self.threadextraframe.pack(fill = 'x')
|
||||
return self.threadextraframe
|
||||
|
||||
def setaccount(self, account):
|
||||
self.account = account
|
||||
self.mailbox = "Unknown"
|
||||
self.updateloclabel()
|
||||
|
||||
def setmailbox(self, mailbox):
|
||||
self.mailbox = mailbox
|
||||
self.updateloclabel()
|
||||
|
||||
def updateloclabel(self):
|
||||
self.loclabel['text'] = "Processing %s: %s" % (self.account,
|
||||
self.mailbox)
|
||||
|
||||
def appendmessage(self, newtext):
|
||||
self.message['text'] += "\n" + newtext
|
||||
|
||||
def setmessage(self, newtext):
|
||||
self.message['text'] = newtext
|
||||
|
||||
|
||||
class VerboseUI(UIBase):
|
||||
def isusable(s):
|
||||
global usabletest
|
||||
if usabletest != None:
|
||||
return usabletest
|
||||
|
||||
try:
|
||||
Tk().destroy()
|
||||
usabletest = 1
|
||||
except TclError:
|
||||
usabletest = 0
|
||||
return usabletest
|
||||
|
||||
def _createTopWindow(self, doidlevac = 1):
|
||||
self.notdeleted = 1
|
||||
self.created = threading.Event()
|
||||
|
||||
self.af = {}
|
||||
self.aflock = Lock()
|
||||
|
||||
t = threadutil.ExitNotifyThread(target = self._runmainloop,
|
||||
name = "Tk Mainloop")
|
||||
t.setDaemon(1)
|
||||
t.start()
|
||||
|
||||
self.created.wait()
|
||||
del self.created
|
||||
|
||||
if doidlevac:
|
||||
t = threadutil.ExitNotifyThread(target = self.idlevacuum,
|
||||
name = "Tk idle vacuum")
|
||||
t.setDaemon(1)
|
||||
t.start()
|
||||
|
||||
def _runmainloop(s):
|
||||
s.top = Tk()
|
||||
s.top.title(version.productname + " " + version.versionstr)
|
||||
s.top.after_idle(s.created.set)
|
||||
s.top.mainloop()
|
||||
s.notdeleted = 0
|
||||
|
||||
def getaccountframe(s):
|
||||
accountname = s.getthreadaccount()
|
||||
s.aflock.acquire()
|
||||
try:
|
||||
if accountname in s.af:
|
||||
return s.af[accountname]
|
||||
|
||||
s.af[accountname] = LEDAccountFrame(s.top, accountname,
|
||||
s.fontfamily, s.fontsize)
|
||||
finally:
|
||||
s.aflock.release()
|
||||
return s.af[accountname]
|
||||
|
||||
def getpass(s, accountname, config, errmsg = None):
|
||||
pd = PasswordDialog(accountname, config, errmsg = errmsg)
|
||||
return pd.getpassword()
|
||||
|
||||
def gettf(s, newtype=ThreadFrame, master = None):
|
||||
if master == None:
|
||||
master = s.top
|
||||
threadid = thread.get_ident()
|
||||
s.tflock.acquire()
|
||||
try:
|
||||
if threadid in s.threadframes:
|
||||
return s.threadframes[threadid]
|
||||
if len(s.availablethreadframes):
|
||||
tf = s.availablethreadframes.pop(0)
|
||||
tf.setthread(currentThread())
|
||||
else:
|
||||
tf = newtype(master)
|
||||
s.threadframes[threadid] = tf
|
||||
return tf
|
||||
finally:
|
||||
s.tflock.release()
|
||||
|
||||
def _display(s, msg):
|
||||
s.gettf().setmessage(msg)
|
||||
|
||||
def threadExited(s, thread):
|
||||
threadid = thread.threadid
|
||||
s.tflock.acquire()
|
||||
if threadid in s.threadframes:
|
||||
tf = s.threadframes[threadid]
|
||||
tf.setthread(None)
|
||||
tf.setaccount("Unknown")
|
||||
tf.setmessage("Idle")
|
||||
s.availablethreadframes.append(tf)
|
||||
del s.threadframes[threadid]
|
||||
s.tflock.release()
|
||||
UIBase.threadExited(s, thread)
|
||||
|
||||
def idlevacuum(s):
|
||||
while s.notdeleted:
|
||||
time.sleep(10)
|
||||
s.tflock.acquire()
|
||||
while len(s.availablethreadframes):
|
||||
tf = s.availablethreadframes.pop()
|
||||
tf.destroy()
|
||||
s.tflock.release()
|
||||
|
||||
def threadException(s, thread):
|
||||
exceptionstr = s.getThreadExceptionString(thread)
|
||||
print exceptionstr
|
||||
|
||||
s.top.destroy()
|
||||
s.top = None
|
||||
TextOKDialog("Thread Exception", exceptionstr)
|
||||
s.delThreadDebugLog(thread)
|
||||
s.terminate(100)
|
||||
|
||||
def mainException(s):
|
||||
exceptionstr = s.getMainExceptionString()
|
||||
print exceptionstr
|
||||
|
||||
s.top.destroy()
|
||||
s.top = None
|
||||
TextOKDialog("Main Program Exception", exceptionstr)
|
||||
|
||||
def warn(s, msg, minor = 0):
|
||||
if minor:
|
||||
# Just let the default handler catch it
|
||||
UIBase.warn(s, msg, minor)
|
||||
else:
|
||||
TextOKDialog("OfflineIMAP Warning", msg)
|
||||
|
||||
def showlicense(s):
|
||||
TextOKDialog(version.productname + " License",
|
||||
version.bigcopyright + "\n" +
|
||||
version.homepage + "\n\n" + version.license,
|
||||
blocking = 0, master = s.top)
|
||||
|
||||
|
||||
def init_banner(s):
|
||||
s.threadframes = {}
|
||||
s.availablethreadframes = []
|
||||
s.tflock = Lock()
|
||||
s._createTopWindow()
|
||||
s._msg(version.productname + " " + version.versionstr + ", " +\
|
||||
version.copyright)
|
||||
tf = s.gettf().getthreadextraframe()
|
||||
|
||||
b = Button(tf, text = "About", command = s.showlicense)
|
||||
b.pack(side = LEFT)
|
||||
|
||||
b = Button(tf, text = "Exit", command = s.terminate)
|
||||
b.pack(side = RIGHT)
|
||||
s.sleeping_abort = {}
|
||||
|
||||
def deletingmessages(s, uidlist, destlist):
|
||||
ds = s.folderlist(destlist)
|
||||
s._msg("Deleting %d messages in %s" % (len(uidlist), ds))
|
||||
|
||||
def _sleep_cancel(s, args = None):
|
||||
s.sleeping_abort[thread.get_ident()] = 1
|
||||
|
||||
def sleep(s, sleepsecs):
|
||||
threadid = thread.get_ident()
|
||||
s.sleeping_abort[threadid] = 0
|
||||
tf = s.gettf().getthreadextraframe()
|
||||
|
||||
def sleep_cancel():
|
||||
s.sleeping_abort[threadid] = 1
|
||||
|
||||
sleepbut = Button(tf, text = 'Sync immediately',
|
||||
command = sleep_cancel)
|
||||
sleepbut.pack()
|
||||
UIBase.sleep(s, sleepsecs)
|
||||
|
||||
def sleeping(s, sleepsecs, remainingsecs):
|
||||
retval = s.sleeping_abort[thread.get_ident()]
|
||||
if remainingsecs:
|
||||
s._msg("Next sync in %d:%02d" % (remainingsecs / 60,
|
||||
remainingsecs % 60))
|
||||
else:
|
||||
s._msg("Wait done; synchronizing now.")
|
||||
s.gettf().destroythreadextraframe()
|
||||
del s.sleeping_abort[thread.get_ident()]
|
||||
time.sleep(sleepsecs)
|
||||
return retval
|
||||
|
||||
TkUI = VerboseUI
|
||||
|
||||
################################################## Blinkenlights
|
||||
|
||||
class LEDAccountFrame:
|
||||
def __init__(self, top, accountname, fontfamily, fontsize):
|
||||
self.top = top
|
||||
self.accountname = accountname
|
||||
self.fontfamily = fontfamily
|
||||
self.fontsize = fontsize
|
||||
self.frame = Frame(self.top, background = 'black')
|
||||
self.frame.pack(side = BOTTOM, expand = 1, fill = X)
|
||||
self._createcanvas(self.frame)
|
||||
|
||||
self.label = Label(self.frame, text = accountname,
|
||||
background = "black", foreground = "blue",
|
||||
font = (self.fontfamily, self.fontsize))
|
||||
self.label.grid(sticky = E, row = 0, column = 1)
|
||||
|
||||
def getnewthreadframe(s):
|
||||
return LEDThreadFrame(s.canvas)
|
||||
|
||||
def _createcanvas(self, parent):
|
||||
c = LEDFrame(parent)
|
||||
self.canvas = c
|
||||
c.grid(sticky = E, row = 0, column = 0)
|
||||
parent.grid_columnconfigure(1, weight = 1)
|
||||
#c.pack(side = LEFT, expand = 0, fill = X)
|
||||
|
||||
def startsleep(s, sleepsecs):
|
||||
s.sleeping_abort = 0
|
||||
s.button = Button(s.frame, text = "Sync now", command = s.syncnow,
|
||||
background = "black", activebackground = "black",
|
||||
activeforeground = "white",
|
||||
foreground = "blue", highlightthickness = 0,
|
||||
padx = 0, pady = 0,
|
||||
font = (s.fontfamily, s.fontsize), borderwidth = 0,
|
||||
relief = 'solid')
|
||||
s.button.grid(sticky = E, row = 0, column = 2)
|
||||
|
||||
def syncnow(s):
|
||||
s.sleeping_abort = 1
|
||||
|
||||
def sleeping(s, sleepsecs, remainingsecs):
|
||||
if remainingsecs:
|
||||
s.button.config(text = 'Sync now (%d:%02d remain)' % \
|
||||
(remainingsecs / 60, remainingsecs % 60))
|
||||
time.sleep(sleepsecs)
|
||||
else:
|
||||
s.button.destroy()
|
||||
del s.button
|
||||
return s.sleeping_abort
|
||||
|
||||
class LEDFrame(Frame):
|
||||
"""This holds the different lights."""
|
||||
def getnewobj(self):
|
||||
retval = Canvas(self, background = 'black', height = 20, bd = 0,
|
||||
highlightthickness = 0, width = 10)
|
||||
retval.pack(side = LEFT, padx = 0, pady = 0, ipadx = 0, ipady = 0)
|
||||
return retval
|
||||
|
||||
class LEDThreadFrame:
|
||||
"""There is one of these for each little light."""
|
||||
def __init__(self, master):
|
||||
self.canvas = master.getnewobj()
|
||||
self.color = ''
|
||||
self.ovalid = self.canvas.create_oval(4, 4, 9,
|
||||
9, fill = 'gray',
|
||||
outline = '#303030')
|
||||
|
||||
def setcolor(self, newcolor):
|
||||
if newcolor != self.color:
|
||||
self.canvas.itemconfigure(self.ovalid, fill = newcolor)
|
||||
self.color = newcolor
|
||||
|
||||
def getcolor(self):
|
||||
return self.color
|
||||
|
||||
def setthread(self, newthread):
|
||||
if newthread:
|
||||
self.setcolor('gray')
|
||||
else:
|
||||
self.setcolor('black')
|
||||
|
||||
|
||||
class Blinkenlights(BlinkenBase, VerboseUI):
|
||||
def __init__(s, config, verbose = 0):
|
||||
VerboseUI.__init__(s, config, verbose)
|
||||
s.fontfamily = 'Helvetica'
|
||||
s.fontsize = 8
|
||||
if config.has_option('ui.Tk.Blinkenlights', 'fontfamily'):
|
||||
s.fontfamily = config.get('ui.Tk.Blinkenlights', 'fontfamily')
|
||||
if config.has_option('ui.Tk.Blinkenlights', 'fontsize'):
|
||||
s.fontsize = config.getint('ui.Tk.Blinkenlights', 'fontsize')
|
||||
|
||||
def isusable(s):
|
||||
return VerboseUI.isusable(s)
|
||||
|
||||
def _createTopWindow(self):
|
||||
VerboseUI._createTopWindow(self, 0)
|
||||
#self.top.resizable(width = 0, height = 0)
|
||||
self.top.configure(background = 'black', bd = 0)
|
||||
|
||||
widthmetric = tkFont.Font(family = self.fontfamily, size = self.fontsize).measure("0")
|
||||
self.loglines = self.config.getdefaultint("ui.Tk.Blinkenlights",
|
||||
"loglines", 5)
|
||||
self.bufferlines = self.config.getdefaultint("ui.Tk.Blinkenlights",
|
||||
"bufferlines", 500)
|
||||
self.text = ScrolledText(self.top, bg = 'black', #scrollbar = 'y',
|
||||
font = (self.fontfamily, self.fontsize),
|
||||
bd = 0, highlightthickness = 0, setgrid = 0,
|
||||
state = DISABLED, height = self.loglines,
|
||||
wrap = NONE, width = 60)
|
||||
self.text.vbar.configure(background = '#000050',
|
||||
activebackground = 'blue',
|
||||
highlightbackground = 'black',
|
||||
troughcolor = "black", bd = 0,
|
||||
elementborderwidth = 2)
|
||||
|
||||
self.textenabled = 0
|
||||
self.tags = []
|
||||
self.textlock = Lock()
|
||||
|
||||
def init_banner(s):
|
||||
BlinkenBase.init_banner(s)
|
||||
s._createTopWindow()
|
||||
menubar = Menu(s.top, activebackground = "black",
|
||||
activeforeground = "white",
|
||||
activeborderwidth = 0,
|
||||
background = "black", foreground = "blue",
|
||||
font = (s.fontfamily, s.fontsize), bd = 0)
|
||||
menubar.add_command(label = "About", command = s.showlicense)
|
||||
menubar.add_command(label = "Show Log", command = s._togglelog)
|
||||
menubar.add_command(label = "Exit", command = s.terminate)
|
||||
s.top.config(menu = menubar)
|
||||
s.menubar = menubar
|
||||
s.text.see(END)
|
||||
if s.config.getdefaultboolean("ui.Tk.Blinkenlights", "showlog", 1):
|
||||
s._togglelog()
|
||||
s.gettf().setcolor('red')
|
||||
s.top.resizable(width = 0, height = 0)
|
||||
s._msg(version.banner)
|
||||
|
||||
def _togglelog(s):
|
||||
if s.textenabled:
|
||||
s.oldtextheight = s.text.winfo_height()
|
||||
s.text.pack_forget()
|
||||
s.textenabled = 0
|
||||
s.menubar.entryconfig('Hide Log', label = 'Show Log')
|
||||
s.top.update()
|
||||
s.top.geometry("")
|
||||
s.top.update()
|
||||
s.top.resizable(width = 0, height = 0)
|
||||
s.top.update()
|
||||
|
||||
else:
|
||||
s.text.pack(side = TOP, expand = 1, fill = BOTH)
|
||||
s.textenabled = 1
|
||||
s.top.update()
|
||||
s.top.geometry("")
|
||||
s.menubar.entryconfig('Show Log', label = 'Hide Log')
|
||||
s._rescroll()
|
||||
s.top.resizable(width = 1, height = 1)
|
||||
|
||||
def sleep(s, sleepsecs):
|
||||
s.gettf().setcolor('red')
|
||||
s._msg("Next sync in %d:%02d" % (sleepsecs / 60, sleepsecs % 60))
|
||||
BlinkenBase.sleep(s, sleepsecs)
|
||||
|
||||
def sleeping(s, sleepsecs, remainingsecs):
|
||||
return BlinkenBase.sleeping(s, sleepsecs, remainingsecs)
|
||||
|
||||
def _rescroll(s):
|
||||
s.text.see(END)
|
||||
lo, hi = s.text.vbar.get()
|
||||
s.text.vbar.set(1.0 - (hi - lo), 1.0)
|
||||
|
||||
def _display(s, msg):
|
||||
if "\n" in msg:
|
||||
for thisline in msg.split("\n"):
|
||||
s._msg(thisline)
|
||||
return
|
||||
#VerboseUI._msg(s, msg)
|
||||
color = s.gettf().getcolor()
|
||||
rescroll = 1
|
||||
s.textlock.acquire()
|
||||
try:
|
||||
if s.text.vbar.get()[1] != 1.0:
|
||||
rescroll = 0
|
||||
s.text.config(state = NORMAL)
|
||||
if not color in s.tags:
|
||||
s.text.tag_config(color, foreground = color)
|
||||
s.tags.append(color)
|
||||
s.text.insert(END, "\n" + msg, color)
|
||||
|
||||
# Trim down. Not quite sure why I have to say 7 instead of 5,
|
||||
# but so it is.
|
||||
while float(s.text.index(END)) > s.bufferlines + 2.0:
|
||||
s.text.delete(1.0, 2.0)
|
||||
|
||||
if rescroll:
|
||||
s._rescroll()
|
||||
finally:
|
||||
s.text.config(state = DISABLED)
|
||||
s.textlock.release()
|
||||
|
||||
|
341
offlineimap/ui/UIBase.py
Normal file
341
offlineimap/ui/UIBase.py
Normal file
@ -0,0 +1,341 @@
|
||||
# UI base class
|
||||
# Copyright (C) 2002 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
|
||||
|
||||
import offlineimap.version
|
||||
import re, time, sys, traceback, threading, thread
|
||||
from StringIO import StringIO
|
||||
|
||||
debugtypes = {'imap': 'IMAP protocol debugging',
|
||||
'maildir': 'Maildir repository debugging',
|
||||
'thread': 'Threading debugging'}
|
||||
|
||||
globalui = None
|
||||
def setglobalui(newui):
|
||||
global globalui
|
||||
globalui = newui
|
||||
def getglobalui():
|
||||
global globalui
|
||||
return globalui
|
||||
|
||||
class UIBase:
|
||||
def __init__(s, config, verbose = 0):
|
||||
s.verbose = verbose
|
||||
s.config = config
|
||||
s.debuglist = []
|
||||
s.debugmessages = {}
|
||||
s.debugmsglen = 50
|
||||
s.threadaccounts = {}
|
||||
s.logfile = None
|
||||
|
||||
################################################## UTILS
|
||||
def _msg(s, msg):
|
||||
"""Generic tool called when no other works."""
|
||||
s._log(msg)
|
||||
s._display(msg)
|
||||
|
||||
def _log(s, msg):
|
||||
"""Log it to disk. Returns true if it wrote something; false
|
||||
otherwise."""
|
||||
if s.logfile:
|
||||
s.logfile.write("%s: %s\n" % (threading.currentThread().getName(),
|
||||
msg))
|
||||
return 1
|
||||
return 0
|
||||
|
||||
def setlogfd(s, logfd):
|
||||
s.logfile = logfd
|
||||
logfd.write("This is %s %s %s\n" % \
|
||||
(offlineimap.version.productname,
|
||||
offlineimap.version.versionstr,
|
||||
offlineimap.version.revstr))
|
||||
logfd.write("Python: %s\n" % sys.version)
|
||||
logfd.write("Platform: %s\n" % sys.platform)
|
||||
logfd.write("Args: %s\n" % sys.argv)
|
||||
|
||||
def _display(s, msg):
|
||||
"""Display a message."""
|
||||
raise NotImplementedError
|
||||
|
||||
def warn(s, msg, minor = 0):
|
||||
if minor:
|
||||
s._msg("warning: " + msg)
|
||||
else:
|
||||
s._msg("WARNING: " + msg)
|
||||
|
||||
def registerthread(s, account):
|
||||
"""Provides a hint to UIs about which account this particular
|
||||
thread is processing."""
|
||||
if s.threadaccounts.has_key(threading.currentThread()):
|
||||
raise ValueError, "Thread %s already registered (old %s, new %s)" %\
|
||||
(threading.currentThread().getName(),
|
||||
s.getthreadaccount(s), account)
|
||||
s.threadaccounts[threading.currentThread()] = account
|
||||
|
||||
def unregisterthread(s, thr):
|
||||
"""Recognizes a thread has exited."""
|
||||
if s.threadaccounts.has_key(thr):
|
||||
del s.threadaccounts[thr]
|
||||
|
||||
def getthreadaccount(s, thr = None):
|
||||
if not thr:
|
||||
thr = threading.currentThread()
|
||||
if s.threadaccounts.has_key(thr):
|
||||
return s.threadaccounts[thr]
|
||||
return '*Control'
|
||||
|
||||
def debug(s, debugtype, msg):
|
||||
thisthread = threading.currentThread()
|
||||
if s.debugmessages.has_key(thisthread):
|
||||
s.debugmessages[thisthread].append("%s: %s" % (debugtype, msg))
|
||||
else:
|
||||
s.debugmessages[thisthread] = ["%s: %s" % (debugtype, msg)]
|
||||
|
||||
while len(s.debugmessages[thisthread]) > s.debugmsglen:
|
||||
s.debugmessages[thisthread] = s.debugmessages[thisthread][1:]
|
||||
|
||||
if debugtype in s.debuglist:
|
||||
if not s._log("DEBUG[%s]: %s" % (debugtype, msg)):
|
||||
s._display("DEBUG[%s]: %s" % (debugtype, msg))
|
||||
|
||||
def add_debug(s, debugtype):
|
||||
global debugtypes
|
||||
if debugtype in debugtypes:
|
||||
if not debugtype in s.debuglist:
|
||||
s.debuglist.append(debugtype)
|
||||
s.debugging(debugtype)
|
||||
else:
|
||||
s.invaliddebug(debugtype)
|
||||
|
||||
def debugging(s, debugtype):
|
||||
global debugtypes
|
||||
s._msg("Now debugging for %s: %s" % (debugtype, debugtypes[debugtype]))
|
||||
|
||||
def invaliddebug(s, debugtype):
|
||||
s.warn("Invalid debug type: %s" % debugtype)
|
||||
|
||||
def locked(s):
|
||||
raise Exception, "Another OfflineIMAP is running with the same metadatadir; exiting."
|
||||
|
||||
def getnicename(s, object):
|
||||
prelimname = str(object.__class__).split('.')[-1]
|
||||
# Strip off extra stuff.
|
||||
return re.sub('(Folder|Repository)', '', prelimname)
|
||||
|
||||
def isusable(s):
|
||||
"""Returns true if this UI object is usable in the current
|
||||
environment. For instance, an X GUI would return true if it's
|
||||
being run in X with a valid DISPLAY setting, and false otherwise."""
|
||||
return 1
|
||||
|
||||
################################################## INPUT
|
||||
|
||||
def getpass(s, accountname, config, errmsg = None):
|
||||
raise NotImplementedError
|
||||
|
||||
def folderlist(s, list):
|
||||
return ', '.join(["%s[%s]" % (s.getnicename(x), x.getname()) for x in list])
|
||||
|
||||
################################################## WARNINGS
|
||||
def msgtoreadonly(s, destfolder, uid, content, flags):
|
||||
if not (config.has_option('general', 'ignore-readonly') and config.getboolean("general", "ignore-readonly")):
|
||||
s.warn("Attempted to synchronize message %d to folder %s[%s], but that folder is read-only. The message will not be copied to that folder." % \
|
||||
(uid, s.getnicename(destfolder), destfolder.getname()))
|
||||
|
||||
def flagstoreadonly(s, destfolder, uidlist, flags):
|
||||
if not (config.has_option('general', 'ignore-readonly') and config.getboolean("general", "ignore-readonly")):
|
||||
s.warn("Attempted to modify flags for messages %s in folder %s[%s], but that folder is read-only. No flags have been modified for that message." % \
|
||||
(str(uidlist), s.getnicename(destfolder), destfolder.getname()))
|
||||
|
||||
def deletereadonly(s, destfolder, uidlist):
|
||||
if not (config.has_option('general', 'ignore-readonly') and config.getboolean("general", "ignore-readonly")):
|
||||
s.warn("Attempted to delete messages %s in folder %s[%s], but that folder is read-only. No messages have been deleted in that folder." % \
|
||||
(str(uidlist), s.getnicename(destfolder), destfolder.getname()))
|
||||
|
||||
################################################## MESSAGES
|
||||
|
||||
def init_banner(s):
|
||||
"""Called when the UI starts. Must be called before any other UI
|
||||
call except isusable(). Displays the copyright banner. This is
|
||||
where the UI should do its setup -- TK, for instance, would
|
||||
create the application window here."""
|
||||
if s.verbose >= 0:
|
||||
s._msg(offlineimap.version.banner)
|
||||
|
||||
def connecting(s, hostname, port):
|
||||
if s.verbose < 0:
|
||||
return
|
||||
if hostname == None:
|
||||
hostname = ''
|
||||
if port != None:
|
||||
port = ":%s" % str(port)
|
||||
displaystr = ' to %s%s.' % (hostname, port)
|
||||
if hostname == '' and port == None:
|
||||
displaystr = '.'
|
||||
s._msg("Establishing connection" + displaystr)
|
||||
|
||||
def acct(s, accountname):
|
||||
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" % \
|
||||
(s.getnicename(srcrepos), s.getnicename(destrepos)))
|
||||
|
||||
############################## Folder syncing
|
||||
def syncingfolder(s, srcrepos, srcfolder, destrepos, destfolder):
|
||||
"""Called when a folder sync operation is started."""
|
||||
if s.verbose >= 0:
|
||||
s._msg("Syncing %s: %s -> %s" % (srcfolder.getname(),
|
||||
s.getnicename(srcrepos),
|
||||
s.getnicename(destrepos)))
|
||||
|
||||
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:
|
||||
s._msg("Loading message list for %s[%s]" % (s.getnicename(repos),
|
||||
folder.getname()))
|
||||
|
||||
def messagelistloaded(s, repos, folder, count):
|
||||
if s.verbose > 0:
|
||||
s._msg("Message list for %s[%s] loaded: %d messages" % \
|
||||
(s.getnicename(repos), folder.getname(), count))
|
||||
|
||||
############################## Message syncing
|
||||
|
||||
def syncingmessages(s, sr, sf, dr, df):
|
||||
if s.verbose > 0:
|
||||
s._msg("Syncing messages %s[%s] -> %s[%s]" % (s.getnicename(sr),
|
||||
sf.getname(),
|
||||
s.getnicename(dr),
|
||||
df.getname()))
|
||||
|
||||
def copyingmessage(s, uid, src, destlist):
|
||||
if s.verbose >= 0:
|
||||
ds = s.folderlist(destlist)
|
||||
s._msg("Copy message %d %s[%s] -> %s" % (uid, s.getnicename(src),
|
||||
src.getname(), ds))
|
||||
|
||||
def deletingmessage(s, uid, destlist):
|
||||
if s.verbose >= 0:
|
||||
ds = s.folderlist(destlist)
|
||||
s._msg("Deleting message %d in %s" % (uid, ds))
|
||||
|
||||
def deletingmessages(s, uidlist, destlist):
|
||||
if s.verbose >= 0:
|
||||
ds = s.folderlist(destlist)
|
||||
s._msg("Deleting %d messages (%s) in %s" % \
|
||||
(len(uidlist),
|
||||
", ".join([str(u) for u in uidlist]),
|
||||
ds))
|
||||
|
||||
def addingflags(s, uidlist, flags, destlist):
|
||||
if s.verbose >= 0:
|
||||
ds = s.folderlist(destlist)
|
||||
s._msg("Adding flags %s to %d messages on %s" % \
|
||||
(", ".join(flags), len(uidlist), ds))
|
||||
|
||||
def deletingflags(s, uidlist, flags, destlist):
|
||||
if s.verbose >= 0:
|
||||
ds = s.folderlist(destlist)
|
||||
s._msg("Deleting flags %s to %d messages on %s" % \
|
||||
(", ".join(flags), len(uidlist), ds))
|
||||
|
||||
################################################## Threads
|
||||
|
||||
def getThreadDebugLog(s, thread):
|
||||
if s.debugmessages.has_key(thread):
|
||||
message = "\nLast %d debug messages logged for %s prior to exception:\n"\
|
||||
% (len(s.debugmessages[thread]), thread.getName())
|
||||
message += "\n".join(s.debugmessages[thread])
|
||||
else:
|
||||
message = "\nNo debug messages were logged for %s." % \
|
||||
thread.getName()
|
||||
return message
|
||||
|
||||
def delThreadDebugLog(s, thread):
|
||||
if s.debugmessages.has_key(thread):
|
||||
del s.debugmessages[thread]
|
||||
|
||||
def getThreadExceptionString(s, thread):
|
||||
message = "Thread '%s' terminated with exception:\n%s" % \
|
||||
(thread.getName(), thread.getExitStackTrace())
|
||||
message += "\n" + s.getThreadDebugLog(thread)
|
||||
return message
|
||||
|
||||
def threadException(s, thread):
|
||||
"""Called when a thread has terminated with an exception.
|
||||
The argument is the ExitNotifyThread that has so terminated."""
|
||||
s._msg(s.getThreadExceptionString(thread))
|
||||
s.delThreadDebugLog(thread)
|
||||
s.terminate(100)
|
||||
|
||||
def getMainExceptionString(s):
|
||||
sbuf = StringIO()
|
||||
traceback.print_exc(file = sbuf)
|
||||
return "Main program terminated with exception:\n" + \
|
||||
sbuf.getvalue() + "\n" + \
|
||||
s.getThreadDebugLog(threading.currentThread())
|
||||
|
||||
def mainException(s):
|
||||
s._msg(s.getMainExceptionString())
|
||||
|
||||
def terminate(s, exitstatus = 0):
|
||||
"""Called to terminate the application."""
|
||||
sys.exit(exitstatus)
|
||||
|
||||
def threadExited(s, thread):
|
||||
"""Called when a thread has exited normally. Many UIs will
|
||||
just ignore this."""
|
||||
s.delThreadDebugLog(thread)
|
||||
s.unregisterthread(thread)
|
||||
|
||||
################################################## Other
|
||||
|
||||
def sleep(s, sleepsecs):
|
||||
"""This function does not actually output anything, but handles
|
||||
the overall sleep, dealing with updates as necessary. It will,
|
||||
however, call sleeping() which DOES output something.
|
||||
|
||||
Returns 0 if timeout expired, 1 if there is a request to cancel
|
||||
the timer, and 2 if there is a request to abort the program."""
|
||||
|
||||
abortsleep = 0
|
||||
while sleepsecs > 0 and not abortsleep:
|
||||
abortsleep = s.sleeping(1, sleepsecs)
|
||||
sleepsecs -= 1
|
||||
s.sleeping(0, 0) # Done sleeping.
|
||||
return abortsleep
|
||||
|
||||
def sleeping(s, sleepsecs, remainingsecs):
|
||||
"""Sleep for sleepsecs, remainingsecs to go.
|
||||
If sleepsecs is 0, indicates we're done sleeping.
|
||||
|
||||
Return 0 for normal sleep, or 1 to indicate a request
|
||||
to sync immediately."""
|
||||
s._msg("Next refresh in %d seconds" % remainingsecs)
|
||||
if sleepsecs > 0:
|
||||
time.sleep(sleepsecs)
|
||||
return 0
|
44
offlineimap/ui/__init__.py
Normal file
44
offlineimap/ui/__init__.py
Normal file
@ -0,0 +1,44 @@
|
||||
# UI module directory
|
||||
# Copyright (C) 2002 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
|
||||
|
||||
|
||||
import UIBase, Blinkenlights
|
||||
try:
|
||||
import TTY
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import Tkinter
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
import Tk
|
||||
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
import Curses
|
||||
|
||||
import Noninteractive
|
||||
|
||||
# Must be last
|
||||
import detector
|
||||
|
49
offlineimap/ui/debuglock.py
Normal file
49
offlineimap/ui/debuglock.py
Normal file
@ -0,0 +1,49 @@
|
||||
# Locking debugging code -- temporary
|
||||
# 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 threading import *
|
||||
import traceback
|
||||
logfile = open("/tmp/logfile", "wt")
|
||||
loglock = Lock()
|
||||
|
||||
class DebuggingLock:
|
||||
def __init__(self, name):
|
||||
self.lock = Lock()
|
||||
self.name = name
|
||||
|
||||
def acquire(self, blocking = 1):
|
||||
self.print_tb("Acquire lock")
|
||||
self.lock.acquire(blocking)
|
||||
self.logmsg("===== %s: Thread %s acquired lock\n" % (self.name, currentThread().getName()))
|
||||
|
||||
def release(self):
|
||||
self.print_tb("Release lock")
|
||||
self.lock.release()
|
||||
|
||||
def logmsg(self, msg):
|
||||
loglock.acquire()
|
||||
logfile.write(msg + "\n")
|
||||
logfile.flush()
|
||||
loglock.release()
|
||||
|
||||
def print_tb(self, msg):
|
||||
self.logmsg(".... %s: Thread %s attempting to %s\n" % \
|
||||
(self.name, currentThread().getName(), msg) + \
|
||||
"\n".join(traceback.format_list(traceback.extract_stack())))
|
||||
|
||||
|
52
offlineimap/ui/detector.py
Normal file
52
offlineimap/ui/detector.py
Normal file
@ -0,0 +1,52 @@
|
||||
# UI base class
|
||||
# Copyright (C) 2002 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
|
||||
|
||||
import offlineimap.ui
|
||||
import sys
|
||||
|
||||
def findUI(config, chosenUI=None):
|
||||
uistrlist = ['Tk.Blinkenlights', 'Tk.VerboseUI',
|
||||
'Curses.Blinkenlights', 'TTY.TTYUI',
|
||||
'Noninteractive.Basic', 'Noninteractive.Quiet']
|
||||
namespace={}
|
||||
for ui in dir(offlineimap.ui):
|
||||
if ui.startswith('_') or ui=='detector':
|
||||
continue
|
||||
namespace[ui]=getattr(offlineimap.ui, ui)
|
||||
|
||||
if chosenUI is not None:
|
||||
uistrlist = [chosenUI]
|
||||
elif config.has_option("general", "ui"):
|
||||
uistrlist = config.get("general", "ui").replace(" ", "").split(",")
|
||||
|
||||
for uistr in uistrlist:
|
||||
uimod = getUImod(uistr, config.getlocaleval(), namespace)
|
||||
if uimod:
|
||||
uiinstance = uimod(config)
|
||||
if uiinstance.isusable():
|
||||
return uiinstance
|
||||
sys.stderr.write("ERROR: No UIs were found usable!\n")
|
||||
sys.exit(200)
|
||||
|
||||
def getUImod(uistr, localeval, namespace):
|
||||
try:
|
||||
uimod = localeval.eval(uistr, namespace)
|
||||
except (AttributeError, NameError), e:
|
||||
#raise
|
||||
return None
|
||||
return uimod
|
110
offlineimap/version.py
Normal file
110
offlineimap/version.py
Normal file
@ -0,0 +1,110 @@
|
||||
productname = 'OfflineIMAP'
|
||||
versionstr = "4.0.7"
|
||||
revno = long('$Rev: 592 $'[6:-2])
|
||||
revstr = "Rev %d" % revno
|
||||
datestr = '$Date: 2004-08-01 16:50:23 -0500 (Sun, 01 Aug 2004) $'
|
||||
|
||||
versionlist = versionstr.split(".")
|
||||
major = versionlist[0]
|
||||
minor = versionlist[1]
|
||||
patch = versionlist[2]
|
||||
copyright = "Copyright (C) 2002 - 2004 John Goerzen"
|
||||
author = "John Goerzen"
|
||||
author_email = "jgoerzen@complete.org"
|
||||
description = "Disconnected Universal IMAP Mail Synchronization/Reader Support"
|
||||
bigcopyright = """%(productname)s %(versionstr)s (%(revstr)s)
|
||||
%(copyright)s <%(author_email)s>""" % locals()
|
||||
|
||||
banner = bigcopyright + """
|
||||
This software comes with ABSOLUTELY NO WARRANTY; see the file
|
||||
COPYING for details. This is free software, and you are welcome
|
||||
to distribute it under the conditions laid out in COPYING."""
|
||||
|
||||
homepage = "http://www.quux.org/devel/offlineimap"
|
||||
homegopher = "gopher://quux.org/1/devel/offlineimap"
|
||||
license = """Copyright (C) 2002 - 2004 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"""
|
||||
|
||||
cmdhelp = """
|
||||
offlineimap [ -1 ] [ -P profiledir ] [ -a accountlist ] [
|
||||
-c configfile ] [ -d debugtype[,debugtype...] ] [ -o ] [
|
||||
-u interface ]
|
||||
|
||||
offlineimap -h | --help
|
||||
|
||||
-1 Disable all multithreading operations and use
|
||||
solely a single-thread sync. This effectively sets
|
||||
the maxsyncaccounts and all maxconnections configu-
|
||||
ration file variables to 1.
|
||||
|
||||
-P profiledir
|
||||
Sets OfflineIMAP into profile mode. The program
|
||||
will create profiledir (it must not already exist).
|
||||
As it runs, Python profiling information about each
|
||||
thread is logged into profiledir. Please note:
|
||||
This option is present for debugging and optimiza-
|
||||
tion only, and should NOT be used unless you have a
|
||||
specific reason to do so. It will significantly
|
||||
slow program performance, may reduce reliability,
|
||||
and can generate huge amounts of data. You must
|
||||
use the -1 option when you use -P.
|
||||
|
||||
|
||||
-a accountlist
|
||||
Overrides the accounts section in the config file.
|
||||
Lets you specify a particular account or set of
|
||||
accounts to sync without having to edit the config
|
||||
file. You might use this to exclude certain
|
||||
accounts, or to sync some accounts that you nor-
|
||||
mally prefer not to.
|
||||
|
||||
-c configfile
|
||||
Specifies a configuration file to use in lieu of
|
||||
the default, ~/.offlineimaprc.
|
||||
|
||||
-d debugtype[,debugtype...]
|
||||
Enables debugging for OfflineIMAP. This is useful
|
||||
if you are trying to track down a malfunction or
|
||||
figure out what is going on under the hood. I sug-
|
||||
gest that you use this with -1 in order to make the
|
||||
results more sensible.
|
||||
|
||||
-d now requires one or more debugtypes, separated
|
||||
by commas. These define what exactly will be
|
||||
debugged, and so far include two options: imap and
|
||||
maildir. 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 any-
|
||||
one else. The maildir option will enable debugging
|
||||
for certain Maildir operations.
|
||||
|
||||
-o Run only once, ignoring any autorefresh setting in
|
||||
the config file.
|
||||
|
||||
-h, --help
|
||||
Show summary of options.
|
||||
|
||||
-u interface
|
||||
Specifies an alternative user interface module to
|
||||
use. This overrides the default specified in the
|
||||
configuration file. The UI specified with -u will
|
||||
be forced to be used, even if its isuable() method
|
||||
states that it cannot be. Use this option with
|
||||
care. The pre-defined options are listed in the
|
||||
USER INTERFACES section.
|
||||
|
||||
"""
|
Reference in New Issue
Block a user