more style consistency

Signed-off-by: Nicolas Sebrecht <nicolas.s-dev@laposte.net>
This commit is contained in:
Nicolas Sebrecht 2015-01-14 22:58:25 +01:00
parent 5318af606e
commit 461554b7b1
20 changed files with 343 additions and 297 deletions

View File

@ -55,7 +55,7 @@ def AccountHashGenerator(customconfig):
class Account(CustomConfig.ConfigHelperMixin): class Account(CustomConfig.ConfigHelperMixin):
"""Represents an account (ie. 2 repositories) to sync """Represents an account (ie. 2 repositories) to sync.
Most of the time you will actually want to use the derived Most of the time you will actually want to use the derived
:class:`accounts.SyncableAccount` which contains all functions used :class:`accounts.SyncableAccount` which contains all functions used
@ -71,8 +71,9 @@ class Account(CustomConfig.ConfigHelperMixin):
:param config: Representing the offlineimap configuration file. :param config: Representing the offlineimap configuration file.
:type config: :class:`offlineimap.CustomConfig.CustomConfigParser` :type config: :class:`offlineimap.CustomConfig.CustomConfigParser`
:param name: A string denoting the name of the Account :param name: A (str) string denoting the name of the Account
as configured""" as configured.
"""
self.config = config self.config = config
self.name = name self.name = name
@ -109,7 +110,7 @@ class Account(CustomConfig.ConfigHelperMixin):
@classmethod @classmethod
def set_abort_event(cls, config, signum): def set_abort_event(cls, config, signum):
"""Set skip sleep/abort event for all accounts """Set skip sleep/abort event for all accounts.
If we want to skip a current (or the next) sleep, or if we want If we want to skip a current (or the next) sleep, or if we want
to abort an autorefresh loop, the main thread can use to abort an autorefresh loop, the main thread can use
@ -121,6 +122,7 @@ class Account(CustomConfig.ConfigHelperMixin):
This is a class method, it will send the signal to all accounts. This is a class method, it will send the signal to all accounts.
""" """
if signum == 1: if signum == 1:
# resync signal, set config option for all accounts # resync signal, set config option for all accounts
for acctsection in getaccountlist(config): for acctsection in getaccountlist(config):
@ -133,7 +135,7 @@ class Account(CustomConfig.ConfigHelperMixin):
cls.abort_NOW_signal.set() cls.abort_NOW_signal.set()
def get_abort_event(self): def get_abort_event(self):
"""Checks if an abort signal had been sent """Checks if an abort signal had been sent.
If the 'skipsleep' config option for this account had been set, If the 'skipsleep' config option for this account had been set,
with `set_abort_event(config, 1)` it will get cleared in this with `set_abort_event(config, 1)` it will get cleared in this
@ -142,6 +144,7 @@ class Account(CustomConfig.ConfigHelperMixin):
:returns: True, if the main thread had called :returns: True, if the main thread had called
:meth:`set_abort_event` earlier, otherwise 'False'. :meth:`set_abort_event` earlier, otherwise 'False'.
""" """
skipsleep = self.getconfboolean("skipsleep", 0) skipsleep = self.getconfboolean("skipsleep", 0)
if skipsleep: if skipsleep:
self.config.set(self.getsection(), "skipsleep", '0') self.config.set(self.getsection(), "skipsleep", '0')
@ -149,12 +152,13 @@ class Account(CustomConfig.ConfigHelperMixin):
Account.abort_NOW_signal.is_set() Account.abort_NOW_signal.is_set()
def _sleeper(self): def _sleeper(self):
"""Sleep if the account is set to autorefresh """Sleep if the account is set to autorefresh.
:returns: 0:timeout expired, 1: canceled the timer, :returns: 0:timeout expired, 1: canceled the timer,
2:request to abort the program, 2:request to abort the program,
100: if configured to not sleep at all. 100: if configured to not sleep at all.
""" """
if not self.refreshperiod: if not self.refreshperiod:
return 100 return 100
@ -184,7 +188,8 @@ class Account(CustomConfig.ConfigHelperMixin):
return 0 return 0
def serverdiagnostics(self): def serverdiagnostics(self):
"""Output diagnostics for all involved repositories""" """Output diagnostics for all involved repositories."""
remote_repo = Repository(self, 'remote') remote_repo = Repository(self, 'remote')
local_repo = Repository(self, 'local') local_repo = Repository(self, 'local')
#status_repo = Repository(self, 'status') #status_repo = Repository(self, 'status')
@ -194,7 +199,7 @@ class Account(CustomConfig.ConfigHelperMixin):
class SyncableAccount(Account): class SyncableAccount(Account):
"""A syncable email account connecting 2 repositories """A syncable email account connecting 2 repositories.
Derives from :class:`accounts.Account` but contains the additional Derives from :class:`accounts.Account` but contains the additional
functions :meth:`syncrunner`, :meth:`sync`, :meth:`syncfolders`, functions :meth:`syncrunner`, :meth:`sync`, :meth:`syncfolders`,
@ -203,11 +208,12 @@ class SyncableAccount(Account):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
Account.__init__(self, *args, **kwargs) Account.__init__(self, *args, **kwargs)
self._lockfd = None self._lockfd = None
self._lockfilepath = os.path.join(self.config.getmetadatadir(), self._lockfilepath = os.path.join(
"%s.lock" % self) self.config.getmetadatadir(), "%s.lock"% self)
def __lock(self): def __lock(self):
"""Lock the account, throwing an exception if it is locked already""" """Lock the account, throwing an exception if it is locked already."""
self._lockfd = open(self._lockfilepath, 'w') self._lockfd = open(self._lockfilepath, 'w')
try: try:
fcntl.lockf(self._lockfd, fcntl.LOCK_EX|fcntl.LOCK_NB) fcntl.lockf(self._lockfd, fcntl.LOCK_EX|fcntl.LOCK_NB)
@ -217,19 +223,19 @@ class SyncableAccount(Account):
except IOError: except IOError:
self._lockfd.close() self._lockfd.close()
raise OfflineImapError("Could not lock account %s. Is another " raise OfflineImapError("Could not lock account %s. Is another "
"instance using this account?" % self, "instance using this account?"% self,
OfflineImapError.ERROR.REPO), \ OfflineImapError.ERROR.REPO), None, exc_info()[2]
None, exc_info()[2]
def _unlock(self): def _unlock(self):
"""Unlock the account, deleting the lock file""" """Unlock the account, deleting the lock file"""
#If we own the lock file, delete it #If we own the lock file, delete it
if self._lockfd and not self._lockfd.closed: if self._lockfd and not self._lockfd.closed:
self._lockfd.close() self._lockfd.close()
try: try:
os.unlink(self._lockfilepath) os.unlink(self._lockfilepath)
except OSError: except OSError:
pass #Failed to delete for some reason. pass # Failed to delete for some reason.
def syncrunner(self): def syncrunner(self):
self.ui.registerthread(self) self.ui.registerthread(self)
@ -265,8 +271,8 @@ class SyncableAccount(Account):
raise raise
self.ui.error(e, exc_info()[2]) self.ui.error(e, exc_info()[2])
except Exception as e: except Exception as e:
self.ui.error(e, exc_info()[2], msg="While attempting to sync" self.ui.error(e, exc_info()[2], msg=
" account '%s'"% self) "While attempting to sync account '%s'"% self)
else: else:
# after success sync, reset the looping counter to 3 # after success sync, reset the looping counter to 3
if self.refreshperiod: if self.refreshperiod:
@ -278,18 +284,19 @@ class SyncableAccount(Account):
looping = 0 looping = 0
def get_local_folder(self, remotefolder): def get_local_folder(self, remotefolder):
"""Return the corresponding local folder for a given remotefolder""" """Return the corresponding local folder for a given remotefolder."""
return self.localrepos.getfolder( return self.localrepos.getfolder(
remotefolder.getvisiblename(). remotefolder.getvisiblename().
replace(self.remoterepos.getsep(), self.localrepos.getsep())) replace(self.remoterepos.getsep(), self.localrepos.getsep()))
def __sync(self): def __sync(self):
"""Synchronize the account once, then return """Synchronize the account once, then return.
Assumes that `self.remoterepos`, `self.localrepos`, and Assumes that `self.remoterepos`, `self.localrepos`, and
`self.statusrepos` has already been populated, so it should only `self.statusrepos` has already been populated, so it should only
be called from the :meth:`syncrunner` function. be called from the :meth:`syncrunner` function."""
"""
folderthreads = [] folderthreads = []
hook = self.getconf('presynchook', '') hook = self.getconf('presynchook', '')
@ -383,12 +390,12 @@ class SyncableAccount(Account):
stdin=PIPE, stdout=PIPE, stderr=PIPE, stdin=PIPE, stdout=PIPE, stderr=PIPE,
close_fds=True) close_fds=True)
r = p.communicate() r = p.communicate()
self.ui.callhook("Hook stdout: %s\nHook stderr:%s\n" % r) self.ui.callhook("Hook stdout: %s\nHook stderr:%s\n"% r)
self.ui.callhook("Hook return code: %d" % p.returncode) self.ui.callhook("Hook return code: %d"% p.returncode)
except (KeyboardInterrupt, SystemExit): except (KeyboardInterrupt, SystemExit):
raise raise
except Exception as e: except Exception as e:
self.ui.error(e, exc_info()[2], msg = "Calling hook") self.ui.error(e, exc_info()[2], msg="Calling hook")
def syncfolder(account, remotefolder, quick): def syncfolder(account, remotefolder, quick):
"""Synchronizes given remote folder for the specified account. """Synchronizes given remote folder for the specified account.
@ -410,9 +417,9 @@ def syncfolder(account, remotefolder, quick):
localrepos.getlocalroot()) localrepos.getlocalroot())
# Load status folder. # Load status folder.
statusfolder = statusrepos.getfolder(remotefolder.getvisiblename().\ statusfolder = statusrepos.getfolder(remotefolder.getvisiblename().
replace(remoterepos.getsep(), replace(remoterepos.getsep(), statusrepos.getsep()))
statusrepos.getsep()))
if localfolder.get_uidvalidity() == None: if localfolder.get_uidvalidity() == None:
# This is a new folder, so delete the status cache to be # This is a new folder, so delete the status cache to be
# sure we don't have a conflict. # sure we don't have a conflict.
@ -423,13 +430,13 @@ def syncfolder(account, remotefolder, quick):
statusfolder.cachemessagelist() statusfolder.cachemessagelist()
if quick: if quick:
if not localfolder.quickchanged(statusfolder) \ if (not localfolder.quickchanged(statusfolder) and
and not remotefolder.quickchanged(statusfolder): not remotefolder.quickchanged(statusfolder)):
ui.skippingfolder(remotefolder) ui.skippingfolder(remotefolder)
localrepos.restore_atime() localrepos.restore_atime()
return return
# Load local folder # Load local folder.
ui.syncingfolder(remoterepos, remotefolder, localrepos, localfolder) ui.syncingfolder(remoterepos, remotefolder, localrepos, localfolder)
ui.loadmessagelist(localrepos, localfolder) ui.loadmessagelist(localrepos, localfolder)
localfolder.cachemessagelist() localfolder.cachemessagelist()
@ -488,9 +495,8 @@ def syncfolder(account, remotefolder, quick):
ui.error(e, exc_info()[2], msg = "Aborting sync, folder '%s' " ui.error(e, exc_info()[2], msg = "Aborting sync, folder '%s' "
"[acc: '%s']" % (localfolder, account)) "[acc: '%s']" % (localfolder, account))
except Exception as e: except Exception as e:
ui.error(e, msg = "ERROR in syncfolder for %s folder %s: %s" % \ ui.error(e, msg = "ERROR in syncfolder for %s folder %s: %s"%
(account, remotefolder.getvisiblename(), (account, remotefolder.getvisiblename(), traceback.format_exc()))
traceback.format_exc()))
finally: finally:
for folder in ["statusfolder", "localfolder", "remotefolder"]: for folder in ["statusfolder", "localfolder", "remotefolder"]:
if folder in locals(): if folder in locals():

View File

@ -48,12 +48,10 @@ class BaseFolder(object):
self.visiblename = '' self.visiblename = ''
self.config = repository.getconfig() self.config = repository.getconfig()
utime_from_message_global = \ utime_from_message_global = self.config.getdefaultboolean(
self.config.getdefaultboolean("general", "general", "utime_from_message", False)
"utime_from_message", False)
repo = "Repository " + repository.name repo = "Repository " + repository.name
self._utime_from_message = \ self._utime_from_message = self.config.getdefaultboolean(repo,
self.config.getdefaultboolean(repo,
"utime_from_message", utime_from_message_global) "utime_from_message", utime_from_message_global)
# Determine if we're running static or dynamic folder filtering # Determine if we're running static or dynamic folder filtering
@ -78,16 +76,19 @@ class BaseFolder(object):
return self.name return self.name
def __str__(self): def __str__(self):
# FIMXE: remove calls of this. We have getname().
return self.name return self.name
@property @property
def accountname(self): def accountname(self):
"""Account name as string""" """Account name as string"""
return self.repository.accountname return self.repository.accountname
@property @property
def sync_this(self): def sync_this(self):
"""Should this folder be synced or is it e.g. filtered out?""" """Should this folder be synced or is it e.g. filtered out?"""
if not self._dynamic_folderfilter: if not self._dynamic_folderfilter:
return self._sync_this return self._sync_this
else: else:
@ -144,7 +145,7 @@ class BaseFolder(object):
if self.name == self.visiblename: if self.name == self.visiblename:
return self.name return self.name
else: else:
return "%s [remote name %s]" % (self.visiblename, self.name) return "%s [remote name %s]"% (self.visiblename, self.name)
def getrepository(self): def getrepository(self):
"""Returns the repository object that this folder is within.""" """Returns the repository object that this folder is within."""
@ -172,9 +173,9 @@ class BaseFolder(object):
if not self.name: if not self.name:
basename = '.' basename = '.'
else: #avoid directory hierarchies and file names such as '/' else: # Avoid directory hierarchies and file names such as '/'.
basename = self.name.replace('/', '.') basename = self.name.replace('/', '.')
# replace with literal 'dot' if final path name is '.' as '.' is # Replace with literal 'dot' if final path name is '.' as '.' is
# an invalid file name. # an invalid file name.
basename = re.sub('(^|\/)\.$','\\1dot', basename) basename = re.sub('(^|\/)\.$','\\1dot', basename)
return basename return basename
@ -196,7 +197,7 @@ class BaseFolder(object):
return True return True
def _getuidfilename(self): def _getuidfilename(self):
"""provides UIDVALIDITY cache filename for class internal purposes""" """provides UIDVALIDITY cache filename for class internal purposes.
return os.path.join(self.repository.getuiddir(), return os.path.join(self.repository.getuiddir(),
self.getfolderbasename()) self.getfolderbasename())
@ -228,7 +229,7 @@ class BaseFolder(object):
uidfilename = self._getuidfilename() uidfilename = self._getuidfilename()
with open(uidfilename + ".tmp", "wt") as file: with open(uidfilename + ".tmp", "wt") as file:
file.write("%d\n" % newval) file.write("%d\n"% newval)
os.rename(uidfilename + ".tmp", uidfilename) os.rename(uidfilename + ".tmp", uidfilename)
self._base_saved_uidvalidity = newval self._base_saved_uidvalidity = newval
@ -252,6 +253,7 @@ class BaseFolder(object):
def getmessagelist(self): def getmessagelist(self):
"""Gets the current message list. """Gets the current message list.
You must call cachemessagelist() before calling this function!""" You must call cachemessagelist() before calling this function!"""
raise NotImplementedError raise NotImplementedError
@ -272,6 +274,7 @@ class BaseFolder(object):
def getmessageuidlist(self): def getmessageuidlist(self):
"""Gets a list of UIDs. """Gets a list of UIDs.
You may have to call cachemessagelist() before calling this function!""" You may have to call cachemessagelist() before calling this function!"""
return self.getmessagelist().keys() return self.getmessagelist().keys()
@ -377,6 +380,7 @@ class BaseFolder(object):
def getmessagelabels(self, uid): def getmessagelabels(self, uid):
"""Returns the labels for the specified message.""" """Returns the labels for the specified message."""
raise NotImplementedError raise NotImplementedError
def savemessagelabels(self, uid, labels, ignorelabels=set(), mtime=0): def savemessagelabels(self, uid, labels, ignorelabels=set(), mtime=0):
@ -691,10 +695,10 @@ class BaseFolder(object):
# load it up. # load it up.
if dstfolder.storesmessages(): if dstfolder.storesmessages():
message = self.getmessage(uid) message = self.getmessage(uid)
#Succeeded? -> IMAP actually assigned a UID. If newid # Succeeded? -> IMAP actually assigned a UID. If newid
#remained negative, no server was willing to assign us an # remained negative, no server was willing to assign us an
#UID. If newid is 0, saving succeeded, but we could not # UID. If newid is 0, saving succeeded, but we could not
#retrieve the new UID. Ignore message in this case. # retrieve the new UID. Ignore message in this case.
new_uid = dstfolder.savemessage(uid, message, flags, rtime) new_uid = dstfolder.savemessage(uid, message, flags, rtime)
if new_uid > 0: if new_uid > 0:
if new_uid != uid: if new_uid != uid:
@ -728,7 +732,7 @@ class BaseFolder(object):
raise #raise on unknown errors, so we can fix those raise #raise on unknown errors, so we can fix those
def __syncmessagesto_copy(self, dstfolder, statusfolder): def __syncmessagesto_copy(self, dstfolder, statusfolder):
"""Pass1: Copy locally existing messages not on the other side """Pass1: Copy locally existing messages not on the other side.
This will copy messages to dstfolder that exist locally but are This will copy messages to dstfolder that exist locally but are
not in the statusfolder yet. The strategy is: not in the statusfolder yet. The strategy is:
@ -738,13 +742,11 @@ class BaseFolder(object):
- If dstfolder doesn't have it yet, add them to dstfolder. - If dstfolder doesn't have it yet, add them to dstfolder.
- Update statusfolder - Update statusfolder
This function checks and protects us from action in ryrun mode. This function checks and protects us from action in dryrun mode."""
"""
threads = [] threads = []
copylist = filter(lambda uid: not \ copylist = filter(lambda uid: not statusfolder.uidexists(uid),
statusfolder.uidexists(uid),
self.getmessageuidlist()) self.getmessageuidlist())
num_to_copy = len(copylist) num_to_copy = len(copylist)
if num_to_copy and self.repository.account.dryrun: if num_to_copy and self.repository.account.dryrun:
@ -773,7 +775,7 @@ class BaseFolder(object):
thread.join() thread.join()
def __syncmessagesto_delete(self, dstfolder, statusfolder): def __syncmessagesto_delete(self, dstfolder, statusfolder):
"""Pass 2: Remove locally deleted messages on dst """Pass 2: Remove locally deleted messages on dst.
Get all UIDS in statusfolder but not self. These are messages Get all UIDS in statusfolder but not self. These are messages
that were deleted in 'self'. Delete those from dstfolder and that were deleted in 'self'. Delete those from dstfolder and
@ -782,9 +784,8 @@ class BaseFolder(object):
This function checks and protects us from action in ryrun mode. This function checks and protects us from action in ryrun mode.
""" """
deletelist = filter(lambda uid: uid>=0 \ deletelist = filter(lambda uid: uid >= 0 and not
and not self.uidexists(uid), self.uidexists(uid), statusfolder.getmessageuidlist())
statusfolder.getmessageuidlist())
if len(deletelist): if len(deletelist):
self.ui.deletingmessages(deletelist, [dstfolder]) self.ui.deletingmessages(deletelist, [dstfolder])
if self.repository.account.dryrun: if self.repository.account.dryrun:
@ -795,7 +796,7 @@ class BaseFolder(object):
folder.deletemessages(deletelist) folder.deletemessages(deletelist)
def __syncmessagesto_flags(self, dstfolder, statusfolder): def __syncmessagesto_flags(self, dstfolder, statusfolder):
"""Pass 3: Flag synchronization """Pass 3: Flag synchronization.
Compare flag mismatches in self with those in statusfolder. If Compare flag mismatches in self with those in statusfolder. If
msg has a valid UID and exists on dstfolder (has not e.g. been msg has a valid UID and exists on dstfolder (has not e.g. been
@ -904,7 +905,7 @@ class BaseFolder(object):
def __eq__(self, other): def __eq__(self, other):
"""Comparisons work either on string comparing folder names or """Comparisons work either on string comparing folder names or
on the same instance on the same instance.
MailDirFolder('foo') == 'foo' --> True MailDirFolder('foo') == 'foo' --> True
a = MailDirFolder('foo'); a == b --> True a = MailDirFolder('foo'); a == b --> True

View File

@ -22,11 +22,10 @@ from sys import exc_info
from offlineimap import imaputil, OfflineImapError from offlineimap import imaputil, OfflineImapError
from offlineimap import imaplibutil from offlineimap import imaplibutil
import offlineimap.accounts import offlineimap.accounts
"""Folder implementation to support features of the Gmail IMAP server.
"""
from .IMAP import IMAPFolder from .IMAP import IMAPFolder
"""Folder implementation to support features of the Gmail IMAP server."""
class GmailFolder(IMAPFolder): class GmailFolder(IMAPFolder):
"""Folder implementation to support features of the Gmail IMAP server. """Folder implementation to support features of the Gmail IMAP server.
@ -101,11 +100,11 @@ class GmailFolder(IMAPFolder):
body = self.addmessageheader(body, '\n', self.labelsheader, labels_str) body = self.addmessageheader(body, '\n', self.labelsheader, labels_str)
if len(body)>200: if len(body)>200:
dbg_output = "%s...%s" % (str(body)[:150], str(body)[-50:]) dbg_output = "%s...%s"% (str(body)[:150], str(body)[-50:])
else: else:
dbg_output = body dbg_output = body
self.ui.debug('imap', "Returned object from fetching %d: '%s'" % self.ui.debug('imap', "Returned object from fetching %d: '%s'"%
(uid, dbg_output)) (uid, dbg_output))
return body return body
@ -139,7 +138,7 @@ class GmailFolder(IMAPFolder):
# imaplib2 from quoting the sequence. # imaplib2 from quoting the sequence.
# #
# NB: msgsToFetch are sequential numbers, not UID's # NB: msgsToFetch are sequential numbers, not UID's
res_type, response = imapobj.fetch("'%s'" % msgsToFetch, res_type, response = imapobj.fetch("'%s'"% msgsToFetch,
'(FLAGS X-GM-LABELS UID)') '(FLAGS X-GM-LABELS UID)')
if res_type != 'OK': if res_type != 'OK':
raise OfflineImapError("FETCHING UIDs in folder [%s]%s failed. " % \ raise OfflineImapError("FETCHING UIDs in folder [%s]%s failed. " % \

View File

@ -164,10 +164,10 @@ class IMAPFolder(BaseFolder):
# By default examine all messages in this folder # By default examine all messages in this folder
msgsToFetch = '1:*' msgsToFetch = '1:*'
maxage = self.config.getdefaultint("Account %s"% self.accountname, maxage = self.config.getdefaultint(
"maxage", -1) "Account %s"% self.accountname, "maxage", -1)
maxsize = self.config.getdefaultint("Account %s"% self.accountname, maxsize = self.config.getdefaultint(
"maxsize", -1) "Account %s"% self.accountname, "maxsize", -1)
# Build search condition # Build search condition
if (maxage != -1) | (maxsize != -1): if (maxage != -1) | (maxsize != -1):
@ -178,9 +178,9 @@ class IMAPFolder(BaseFolder):
oldest_struct = time.gmtime(time.time() - (60*60*24*maxage)) oldest_struct = time.gmtime(time.time() - (60*60*24*maxage))
if oldest_struct[0] < 1900: if oldest_struct[0] < 1900:
raise OfflineImapError("maxage setting led to year %d. " raise OfflineImapError("maxage setting led to year %d. "
"Abort syncing." % oldest_struct[0], "Abort syncing."% oldest_struct[0],
OfflineImapError.ERROR.REPO) OfflineImapError.ERROR.REPO)
search_cond += "SINCE %02d-%s-%d" % ( search_cond += "SINCE %02d-%s-%d"% (
oldest_struct[2], oldest_struct[2],
MonthNames[oldest_struct[1]], MonthNames[oldest_struct[1]],
oldest_struct[0]) oldest_struct[0])
@ -188,7 +188,7 @@ class IMAPFolder(BaseFolder):
if(maxsize != -1): if(maxsize != -1):
if(maxage != -1): # There are two conditions, add space if(maxage != -1): # There are two conditions, add space
search_cond += " " search_cond += " "
search_cond += "SMALLER %d" % maxsize search_cond += "SMALLER %d"% maxsize
search_cond += ")" search_cond += ")"
@ -225,10 +225,8 @@ class IMAPFolder(BaseFolder):
msgsToFetch, '(FLAGS UID)') msgsToFetch, '(FLAGS UID)')
if res_type != 'OK': if res_type != 'OK':
raise OfflineImapError("FETCHING UIDs in folder [%s]%s failed. " raise OfflineImapError("FETCHING UIDs in folder [%s]%s failed. "
"Server responded '[%s] %s'"% ( "Server responded '[%s] %s'"% (self.getrepository(), self,
self.getrepository(), self, res_type, response), OfflineImapError.ERROR.FOLDER)
res_type, response),
OfflineImapError.ERROR.FOLDER)
finally: finally:
self.imapserver.releaseconnection(imapobj) self.imapserver.releaseconnection(imapobj)
@ -259,7 +257,7 @@ class IMAPFolder(BaseFolder):
# Interface from BaseFolder # Interface from BaseFolder
def getmessage(self, uid): def getmessage(self, uid):
"""Retrieve message with UID from the IMAP server (incl body) """Retrieve message with UID from the IMAP server (incl body).
After this function all CRLFs will be transformed to '\n'. After this function all CRLFs will be transformed to '\n'.
@ -280,7 +278,7 @@ class IMAPFolder(BaseFolder):
data = data[0][1].replace(CRLF, "\n") data = data[0][1].replace(CRLF, "\n")
if len(data)>200: if len(data)>200:
dbg_output = "%s...%s" % (str(data)[:150], str(data)[-50:]) dbg_output = "%s...%s"% (str(data)[:150], str(data)[-50:])
else: else:
dbg_output = data dbg_output = data
@ -331,7 +329,8 @@ class IMAPFolder(BaseFolder):
# Now find the UID it got. # Now find the UID it got.
headervalue = imapobj._quote(headervalue) headervalue = imapobj._quote(headervalue)
try: try:
matchinguids = imapobj.uid('search', 'HEADER', headername, headervalue)[1][0] matchinguids = imapobj.uid('search', 'HEADER',
headername, headervalue)[1][0]
except imapobj.error as err: except imapobj.error as err:
# IMAP server doesn't implement search or had a problem. # IMAP server doesn't implement search or had a problem.
self.ui.debug('imap', "__savemessage_searchforheader: got IMAP error '%s' while attempting to UID SEARCH for message with header %s"% (err, headername)) self.ui.debug('imap', "__savemessage_searchforheader: got IMAP error '%s' while attempting to UID SEARCH for message with header %s"% (err, headername))
@ -396,8 +395,8 @@ class IMAPFolder(BaseFolder):
result = imapobj.uid('FETCH', bytearray('%d:*'% start), 'rfc822.header') result = imapobj.uid('FETCH', bytearray('%d:*'% start), 'rfc822.header')
if result[0] != 'OK': if result[0] != 'OK':
raise OfflineImapError('Error fetching mail headers: ' + '. '.join(result[1]), raise OfflineImapError('Error fetching mail headers: %s'%
OfflineImapError.ERROR.MESSAGE) '. '.join(result[1]), OfflineImapError.ERROR.MESSAGE)
result = result[1] result = result[1]
@ -423,7 +422,8 @@ class IMAPFolder(BaseFolder):
def __getmessageinternaldate(self, content, rtime=None): def __getmessageinternaldate(self, content, rtime=None):
"""Parses mail and returns an INTERNALDATE string """Parses mail and returns an INTERNALDATE string
It will use information in the following order, falling back as an attempt fails: It will use information in the following order, falling back as an
attempt fails:
- rtime parameter - rtime parameter
- Date header of email - Date header of email
@ -475,20 +475,21 @@ class IMAPFolder(BaseFolder):
"Server will use local time."% datetuple) "Server will use local time."% datetuple)
return None return None
#produce a string representation of datetuple that works as # Produce a string representation of datetuple that works as
#INTERNALDATE # INTERNALDATE.
num2mon = {1:'Jan', 2:'Feb', 3:'Mar', 4:'Apr', 5:'May', 6:'Jun', num2mon = {1:'Jan', 2:'Feb', 3:'Mar', 4:'Apr', 5:'May', 6:'Jun',
7:'Jul', 8:'Aug', 9:'Sep', 10:'Oct', 11:'Nov', 12:'Dec'} 7:'Jul', 8:'Aug', 9:'Sep', 10:'Oct', 11:'Nov', 12:'Dec'}
#tm_isdst coming from email.parsedate is not usable, we still use it here, mhh # tm_isdst coming from email.parsedate is not usable, we still use it
# here, mhh.
if datetuple.tm_isdst == '1': if datetuple.tm_isdst == '1':
zone = -time.altzone zone = -time.altzone
else: else:
zone = -time.timezone zone = -time.timezone
offset_h, offset_m = divmod(zone//60, 60) offset_h, offset_m = divmod(zone//60, 60)
internaldate = '"%02d-%s-%04d %02d:%02d:%02d %+03d%02d"' \ internaldate = '"%02d-%s-%04d %02d:%02d:%02d %+03d%02d"'% \
% (datetuple.tm_mday, num2mon[datetuple.tm_mon], datetuple.tm_year, \ (datetuple.tm_mday, num2mon[datetuple.tm_mon], datetuple.tm_year, \
datetuple.tm_hour, datetuple.tm_min, datetuple.tm_sec, offset_h, offset_m) datetuple.tm_hour, datetuple.tm_min, datetuple.tm_sec, offset_h, offset_m)
return internaldate return internaldate
@ -554,7 +555,7 @@ class IMAPFolder(BaseFolder):
content = self.addmessageheader(content, CRLF, headername, headervalue) content = self.addmessageheader(content, CRLF, headername, headervalue)
if len(content)>200: if len(content)>200:
dbg_output = "%s...%s" % (content[:150], content[-50:]) dbg_output = "%s...%s"% (content[:150], content[-50:])
else: else:
dbg_output = content dbg_output = content
self.ui.debug('imap', "savemessage: date: %s, content: '%s'"% self.ui.debug('imap', "savemessage: date: %s, content: '%s'"%
@ -726,6 +727,7 @@ class IMAPFolder(BaseFolder):
Note that this function does not check against dryrun settings, Note that this function does not check against dryrun settings,
so you need to ensure that it is never called in a so you need to ensure that it is never called in a
dryrun mode.""" dryrun mode."""
imapobj = self.imapserver.acquireconnection() imapobj = self.imapserver.acquireconnection()
try: try:
result = self._store_to_imap(imapobj, str(uid), 'FLAGS', result = self._store_to_imap(imapobj, str(uid), 'FLAGS',

View File

@ -21,8 +21,9 @@ import threading
from .Base import BaseFolder from .Base import BaseFolder
class LocalStatusFolder(BaseFolder): class LocalStatusFolder(BaseFolder):
"""LocalStatus backend implemented as a plain text file""" """LocalStatus backend implemented as a plain text file."""
cur_version = 2 cur_version = 2
magicline = "OFFLINEIMAP LocalStatus CACHE DATA - DO NOT MODIFY - FORMAT %d" magicline = "OFFLINEIMAP LocalStatus CACHE DATA - DO NOT MODIFY - FORMAT %d"
@ -53,12 +54,10 @@ class LocalStatusFolder(BaseFolder):
if not self.isnewfolder(): if not self.isnewfolder():
os.unlink(self.filename) os.unlink(self.filename)
# Interface from BaseFolder # Interface from BaseFolder
def msglist_item_initializer(self, uid): def msglist_item_initializer(self, uid):
return {'uid': uid, 'flags': set(), 'labels': set(), 'time': 0, 'mtime': 0} return {'uid': uid, 'flags': set(), 'labels': set(), 'time': 0, 'mtime': 0}
def readstatus_v1(self, fp): def readstatus_v1(self, fp):
"""Read status folder in format version 1. """Read status folder in format version 1.
@ -80,7 +79,6 @@ class LocalStatusFolder(BaseFolder):
self.messagelist[uid] = self.msglist_item_initializer(uid) self.messagelist[uid] = self.msglist_item_initializer(uid)
self.messagelist[uid]['flags'] = flags self.messagelist[uid]['flags'] = flags
def readstatus(self, fp): def readstatus(self, fp):
"""Read status file in the current format. """Read status file in the current format.
@ -97,7 +95,7 @@ class LocalStatusFolder(BaseFolder):
mtime = long(mtime) mtime = long(mtime)
labels = set([lb.strip() for lb in labels.split(',') if len(lb.strip()) > 0]) labels = set([lb.strip() for lb in labels.split(',') if len(lb.strip()) > 0])
except ValueError as e: except ValueError as e:
errstr = "Corrupt line '%s' in cache file '%s'" % \ errstr = "Corrupt line '%s' in cache file '%s'"% \
(line, self.filename) (line, self.filename)
self.ui.warn(errstr) self.ui.warn(errstr)
raise ValueError(errstr), None, exc_info()[2] raise ValueError(errstr), None, exc_info()[2]
@ -227,7 +225,6 @@ class LocalStatusFolder(BaseFolder):
self.messagelist[uid]['flags'] = flags self.messagelist[uid]['flags'] = flags
self.save() self.save()
def savemessagelabels(self, uid, labels, mtime=None): def savemessagelabels(self, uid, labels, mtime=None):
self.messagelist[uid]['labels'] = labels self.messagelist[uid]['labels'] = labels
if mtime: self.messagelist[uid]['mtime'] = mtime if mtime: self.messagelist[uid]['mtime'] = mtime
@ -263,7 +260,6 @@ class LocalStatusFolder(BaseFolder):
def getmessagemtime(self, uid): def getmessagemtime(self, uid):
return self.messagelist[uid]['mtime'] return self.messagelist[uid]['mtime']
# Interface from BaseFolder # Interface from BaseFolder
def deletemessage(self, uid): def deletemessage(self, uid):
self.deletemessages([uid]) self.deletemessages([uid])

View File

@ -64,7 +64,7 @@ class LocalStatusSQLiteFolder(BaseFolder):
#Try to establish connection, no need for threadsafety in __init__ #Try to establish connection, no need for threadsafety in __init__
try: try:
self.connection = sqlite.connect(self.filename, check_same_thread = False) self.connection = sqlite.connect(self.filename, check_same_thread=False)
except NameError: except NameError:
# sqlite import had failed # sqlite import had failed
raise UserWarning('SQLite backend chosen, but no sqlite python ' raise UserWarning('SQLite backend chosen, but no sqlite python '
@ -93,7 +93,6 @@ class LocalStatusSQLiteFolder(BaseFolder):
def getfullname(self): def getfullname(self):
return self.filename return self.filename
# Interface from LocalStatusFolder # Interface from LocalStatusFolder
def isnewfolder(self): def isnewfolder(self):
return self._newfolder return self._newfolder
@ -101,7 +100,8 @@ class LocalStatusSQLiteFolder(BaseFolder):
# Interface from LocalStatusFolder # Interface from LocalStatusFolder
def deletemessagelist(self): def deletemessagelist(self):
"""delete all messages in the db""" """Delete all messages in the db."""
self.__sql_write('DELETE FROM status') self.__sql_write('DELETE FROM status')
@ -114,6 +114,7 @@ class LocalStatusSQLiteFolder(BaseFolder):
:param executemany: bool indicating whether we want to :param executemany: bool indicating whether we want to
perform conn.executemany() or conn.execute(). perform conn.executemany() or conn.execute().
:returns: the Cursor() or raises an Exception""" :returns: the Cursor() or raises an Exception"""
success = False success = False
while not success: while not success:
self._dblock.acquire() self._dblock.acquire()
@ -153,7 +154,7 @@ class LocalStatusSQLiteFolder(BaseFolder):
# Upgrade from database version 1 to version 2 # Upgrade from database version 1 to version 2
# This change adds labels and mtime columns, to be used by Gmail IMAP and Maildir folders. # This change adds labels and mtime columns, to be used by Gmail IMAP and Maildir folders.
if from_ver <= 1: if from_ver <= 1:
self.ui._msg('Upgrading LocalStatus cache from version 1 to version 2 for %s:%s' %\ self.ui._msg('Upgrading LocalStatus cache from version 1 to version 2 for %s:%s'%
(self.repository, self)) (self.repository, self))
self.connection.executescript("""ALTER TABLE status ADD mtime INTEGER DEFAULT 0; self.connection.executescript("""ALTER TABLE status ADD mtime INTEGER DEFAULT 0;
ALTER TABLE status ADD labels VARCHAR(256) DEFAULT ''; ALTER TABLE status ADD labels VARCHAR(256) DEFAULT '';
@ -167,12 +168,10 @@ class LocalStatusSQLiteFolder(BaseFolder):
def __create_db(self): def __create_db(self):
""" """Create a new db file.
Create a new db file.
self.connection must point to the opened and valid SQlite self.connection must point to the opened and valid SQlite
database connection. database connection."""
"""
self.ui._msg('Creating new Local Status db for %s:%s' \ self.ui._msg('Creating new Local Status db for %s:%s' \
% (self.repository, self)) % (self.repository, self))
self.connection.executescript(""" self.connection.executescript("""
@ -212,6 +211,7 @@ class LocalStatusSQLiteFolder(BaseFolder):
def saveall(self): def saveall(self):
"""Saves the entire messagelist to the database.""" """Saves the entire messagelist to the database."""
data = [] data = []
for uid, msg in self.messagelist.items(): for uid, msg in self.messagelist.items():
mtime = msg['mtime'] mtime = msg['mtime']
@ -219,7 +219,8 @@ class LocalStatusSQLiteFolder(BaseFolder):
labels = ', '.join(sorted(msg['labels'])) labels = ', '.join(sorted(msg['labels']))
data.append((uid, flags, mtime, labels)) data.append((uid, flags, mtime, labels))
self.__sql_write('INSERT OR REPLACE INTO status (id,flags,mtime,labels) VALUES (?,?,?,?)', self.__sql_write('INSERT OR REPLACE INTO status '
'(id,flags,mtime,labels) VALUES (?,?,?,?)',
data, executemany=True) data, executemany=True)
@ -267,14 +268,12 @@ class LocalStatusSQLiteFolder(BaseFolder):
# Interface from BaseFolder # Interface from BaseFolder
def savemessage(self, uid, content, flags, rtime, mtime=0, labels=set()): def savemessage(self, uid, content, flags, rtime, mtime=0, labels=set()):
""" """Writes a new message, with the specified uid.
Writes a new message, with the specified uid.
See folder/Base for detail. Note that savemessage() does not See folder/Base for detail. Note that savemessage() does not
check against dryrun settings, so you need to ensure that check against dryrun settings, so you need to ensure that
savemessage is never called in a dryrun mode. savemessage is never called in a dryrun mode."""
"""
if uid < 0: if uid < 0:
# We cannot assign a uid. # We cannot assign a uid.
return uid return uid
@ -352,6 +351,7 @@ class LocalStatusSQLiteFolder(BaseFolder):
def savemessagesmtimebulk(self, mtimes): def savemessagesmtimebulk(self, mtimes):
"""Saves mtimes from the mtimes dictionary in a single database operation.""" """Saves mtimes from the mtimes dictionary in a single database operation."""
data = [(mt, uid) for uid, mt in mtimes.items()] data = [(mt, uid) for uid, mt in mtimes.items()]
self.__sql_write('UPDATE status SET mtime=? WHERE id=?', data, executemany=True) self.__sql_write('UPDATE status SET mtime=? WHERE id=?', data, executemany=True)
for uid, mt in mtimes.items(): for uid, mt in mtimes.items():
@ -376,6 +376,7 @@ class LocalStatusSQLiteFolder(BaseFolder):
This function uses sqlites executemany() function which is This function uses sqlites executemany() function which is
much faster than iterating through deletemessage() when we have much faster than iterating through deletemessage() when we have
many messages to delete.""" many messages to delete."""
# Weed out ones not in self.messagelist # Weed out ones not in self.messagelist
uidlist = [uid for uid in uidlist if uid in self.messagelist] uidlist = [uid for uid in uidlist if uid in self.messagelist]
if not len(uidlist): if not len(uidlist):

View File

@ -69,7 +69,7 @@ class MaildirFolder(BaseFolder):
"Account "+self.accountname, "maildir-windows-compatible", False) "Account "+self.accountname, "maildir-windows-compatible", False)
self.infosep = '!' if self.wincompatible else ':' self.infosep = '!' if self.wincompatible else ':'
"""infosep is the separator between maildir name and flag appendix""" """infosep is the separator between maildir name and flag appendix"""
self.re_flagmatch = re.compile('%s2,(\w*)' % self.infosep) self.re_flagmatch = re.compile('%s2,(\w*)'% self.infosep)
#self.ui is set in BaseFolder.init() #self.ui is set in BaseFolder.init()
# Everything up to the first comma or colon (or ! if Windows): # Everything up to the first comma or colon (or ! if Windows):
self.re_prefixmatch = re.compile('([^'+ self.infosep + ',]*)') self.re_prefixmatch = re.compile('([^'+ self.infosep + ',]*)')
@ -128,13 +128,14 @@ class MaildirFolder(BaseFolder):
detected, we return an empty flags list. detected, we return an empty flags list.
:returns: (prefix, UID, FMD5, flags). UID is a numeric "long" :returns: (prefix, UID, FMD5, flags). UID is a numeric "long"
type. flags is a set() of Maildir flags""" type. flags is a set() of Maildir flags.
"""
prefix, uid, fmd5, flags = None, None, None, set() prefix, uid, fmd5, flags = None, None, None, set()
prefixmatch = self.re_prefixmatch.match(filename) prefixmatch = self.re_prefixmatch.match(filename)
if prefixmatch: if prefixmatch:
prefix = prefixmatch.group(1) prefix = prefixmatch.group(1)
folderstr = ',FMD5=%s' % self._foldermd5 folderstr = ',FMD5=%s'% self._foldermd5
foldermatch = folderstr in filename foldermatch = folderstr in filename
# If there was no folder MD5 specified, or if it mismatches, # If there was no folder MD5 specified, or if it mismatches,
# assume it is a foreign (new) message and ret: uid, fmd5 = None, None # assume it is a foreign (new) message and ret: uid, fmd5 = None, None
@ -154,7 +155,9 @@ class MaildirFolder(BaseFolder):
Maildir flags are: R (replied) S (seen) T (trashed) D (draft) F Maildir flags are: R (replied) S (seen) T (trashed) D (draft) F
(flagged). (flagged).
:returns: dict that can be used as self.messagelist""" :returns: dict that can be used as self.messagelist.
"""
maxage = self.config.getdefaultint("Account " + self.accountname, maxage = self.config.getdefaultint("Account " + self.accountname,
"maxage", None) "maxage", None)
maxsize = self.config.getdefaultint("Account " + self.accountname, maxsize = self.config.getdefaultint("Account " + self.accountname,
@ -254,7 +257,7 @@ class MaildirFolder(BaseFolder):
:returns: String containing unique message filename""" :returns: String containing unique message filename"""
timeval, timeseq = _gettimeseq() timeval, timeseq = _gettimeseq()
return '%d_%d.%d.%s,U=%d,FMD5=%s%s2,%s' % \ return '%d_%d.%d.%s,U=%d,FMD5=%s%s2,%s'% \
(timeval, timeseq, os.getpid(), socket.gethostname(), (timeval, timeseq, os.getpid(), socket.gethostname(),
uid, self._foldermd5, self.infosep, ''.join(sorted(flags))) uid, self._foldermd5, self.infosep, ''.join(sorted(flags)))
@ -393,7 +396,7 @@ class MaildirFolder(BaseFolder):
""" """
if not uid in self.messagelist: if not uid in self.messagelist:
raise OfflineImapError("Cannot change unknown Maildir UID %s" % uid) raise OfflineImapError("Cannot change unknown Maildir UID %s"% uid)
if uid == new_uid: return if uid == new_uid: return
oldfilename = self.messagelist[uid]['filename'] oldfilename = self.messagelist[uid]['filename']

View File

@ -78,7 +78,7 @@ class MappedIMAPFolder(IMAPFolder):
try: try:
file = open(mapfilename + ".tmp", 'wt') file = open(mapfilename + ".tmp", 'wt')
for (key, value) in self.diskl2r.iteritems(): for (key, value) in self.diskl2r.iteritems():
file.write("%d:%d\n" % (key, value)) file.write("%d:%d\n"% (key, value))
file.close() file.close()
os.rename(mapfilename + '.tmp', mapfilename) os.rename(mapfilename + '.tmp', mapfilename)
finally: finally:
@ -215,8 +215,8 @@ class MappedIMAPFolder(IMAPFolder):
newluid = self._mb.savemessage(-1, content, flags, rtime) newluid = self._mb.savemessage(-1, content, flags, rtime)
if newluid < 1: if newluid < 1:
raise ValueError("Backend could not find uid for message, returned " raise ValueError("Backend could not find uid for message, "
"%s" % newluid) "returned %s"% newluid)
self.maplock.acquire() self.maplock.acquire()
try: try:
self.diskl2r[newluid] = uid self.diskl2r[newluid] = uid
@ -262,8 +262,8 @@ class MappedIMAPFolder(IMAPFolder):
UID. The UIDMaps case handles this efficiently by simply UID. The UIDMaps case handles this efficiently by simply
changing the mappings file.""" changing the mappings file."""
if ruid not in self.r2l: if ruid not in self.r2l:
raise OfflineImapError("Cannot change unknown Maildir UID %s" % ruid, raise OfflineImapError("Cannot change unknown Maildir UID %s"%
OfflineImapError.ERROR.MESSAGE) ruid, OfflineImapError.ERROR.MESSAGE)
if ruid == new_ruid: return # sanity check shortcut if ruid == new_ruid: return # sanity check shortcut
self.maplock.acquire() self.maplock.acquire()
try: try:
@ -271,10 +271,10 @@ class MappedIMAPFolder(IMAPFolder):
self.l2r[luid] = new_ruid self.l2r[luid] = new_ruid
del self.r2l[ruid] del self.r2l[ruid]
self.r2l[new_ruid] = luid self.r2l[new_ruid] = luid
#TODO: diskl2r|r2l are a pain to sync and should be done away with # TODO: diskl2r|r2l are a pain to sync and should be done away with
#diskl2r only contains positive UIDs, so wrap in ifs # diskl2r only contains positive UIDs, so wrap in ifs.
if luid>0: self.diskl2r[luid] = new_ruid if luid > 0: self.diskl2r[luid] = new_ruid
if ruid>0: del self.diskr2l[ruid] if ruid > 0: del self.diskr2l[ruid]
if new_ruid > 0: self.diskr2l[new_ruid] = luid if new_ruid > 0: self.diskr2l[new_ruid] = luid
self._savemaps(dolock = 0) self._savemaps(dolock = 0)
finally: finally:

View File

@ -39,8 +39,9 @@ class UsefulIMAPMixIn(object):
:returns: 'OK' on success, nothing if the folder was already :returns: 'OK' on success, nothing if the folder was already
selected or raises an :exc:`OfflineImapError`.""" selected or raises an :exc:`OfflineImapError`."""
if self.__getselectedfolder() == mailbox and self.is_readonly == readonly \ if self.__getselectedfolder() == mailbox and \
and not force: self.is_readonly == readonly and \
not force:
# No change; return. # No change; return.
return return
# Wipe out all old responses, to maintain semantics with old imaplib2 # Wipe out all old responses, to maintain semantics with old imaplib2

View File

@ -46,6 +46,7 @@ class IMAPServer:
Public instance variables are: self.: Public instance variables are: self.:
delim The server's folder delimiter. Only valid after acquireconnection() delim The server's folder delimiter. Only valid after acquireconnection()
""" """
GSS_STATE_STEP = 0 GSS_STATE_STEP = 0
GSS_STATE_WRAP = 1 GSS_STATE_WRAP = 1
def __init__(self, repos): def __init__(self, repos):
@ -56,7 +57,7 @@ class IMAPServer:
self.preauth_tunnel = repos.getpreauthtunnel() self.preauth_tunnel = repos.getpreauthtunnel()
self.transport_tunnel = repos.gettransporttunnel() self.transport_tunnel = repos.gettransporttunnel()
if self.preauth_tunnel and self.transport_tunnel: if self.preauth_tunnel and self.transport_tunnel:
raise OfflineImapError('%s: '% repos + \ raise OfflineImapError('%s: '% repos +
'you must enable precisely one ' 'you must enable precisely one '
'type of tunnel (preauth or transport), ' 'type of tunnel (preauth or transport), '
'not both', OfflineImapError.ERROR.REPO) 'not both', OfflineImapError.ERROR.REPO)
@ -207,7 +208,7 @@ class IMAPServer:
imapobj.starttls() imapobj.starttls()
except imapobj.error as e: except imapobj.error as e:
raise OfflineImapError("Failed to start " raise OfflineImapError("Failed to start "
"TLS connection: %s" % str(e), "TLS connection: %s"% str(e),
OfflineImapError.ERROR.REPO, None, exc_info()[2]) OfflineImapError.ERROR.REPO, None, exc_info()[2])
@ -335,7 +336,7 @@ class IMAPServer:
exc_stack exc_stack
)) ))
raise OfflineImapError("All authentication types " raise OfflineImapError("All authentication types "
"failed:\n\t%s" % msg, OfflineImapError.ERROR.REPO) "failed:\n\t%s"% msg, OfflineImapError.ERROR.REPO)
if not tried_to_authn: if not tried_to_authn:
methods = ", ".join(map( methods = ", ".join(map(
@ -474,7 +475,7 @@ class IMAPServer:
if self.port != 993: if self.port != 993:
reason = "Could not connect via SSL to host '%s' and non-s"\ reason = "Could not connect via SSL to host '%s' and non-s"\
"tandard ssl port %d configured. Make sure you connect"\ "tandard ssl port %d configured. Make sure you connect"\
" to the correct port." % (self.hostname, self.port) " to the correct port."% (self.hostname, self.port)
else: else:
reason = "Unknown SSL protocol connecting to host '%s' for "\ reason = "Unknown SSL protocol connecting to host '%s' for "\
"repository '%s'. OpenSSL responded:\n%s"\ "repository '%s'. OpenSSL responded:\n%s"\
@ -487,7 +488,7 @@ class IMAPServer:
reason = "Connection to host '%s:%d' for repository '%s' was "\ reason = "Connection to host '%s:%d' for repository '%s' was "\
"refused. Make sure you have the right host and port "\ "refused. Make sure you have the right host and port "\
"configured and that you are actually able to access the "\ "configured and that you are actually able to access the "\
"network." % (self.hostname, self.port, self.repos) "network."% (self.hostname, self.port, self.repos)
raise OfflineImapError(reason, severity), None, exc_info()[2] raise OfflineImapError(reason, severity), None, exc_info()[2]
# Could not acquire connection to the remote; # Could not acquire connection to the remote;
# socket.error(last_error) raised # socket.error(last_error) raised
@ -709,14 +710,14 @@ class IdleThread(object):
imapobj.idle(callback=callback) imapobj.idle(callback=callback)
else: else:
self.ui.warn("IMAP IDLE not supported on server '%s'." self.ui.warn("IMAP IDLE not supported on server '%s'."
"Sleep until next refresh cycle." % imapobj.identifier) "Sleep until next refresh cycle."% imapobj.identifier)
imapobj.noop() imapobj.noop()
self.stop_sig.wait() # self.stop() or IDLE callback are invoked self.stop_sig.wait() # self.stop() or IDLE callback are invoked
try: try:
# End IDLE mode with noop, imapobj can point to a dropped conn. # End IDLE mode with noop, imapobj can point to a dropped conn.
imapobj.noop() imapobj.noop()
except imapobj.abort: except imapobj.abort:
self.ui.warn('Attempting NOOP on dropped connection %s' % \ self.ui.warn('Attempting NOOP on dropped connection %s'%
imapobj.identifier) imapobj.identifier)
self.parent.releaseconnection(imapobj, True) self.parent.releaseconnection(imapobj, True)
else: else:

View File

@ -211,7 +211,7 @@ def uid_sequence(uidlist):
def getrange(start, end): def getrange(start, end):
if start == end: if start == end:
return(str(start)) return(str(start))
return "%s:%s" % (start, end) return "%s:%s"% (start, end)
if not len(uidlist): return '' # Empty list, return if not len(uidlist): return '' # Empty list, return
start, end = None, None start, end = None, None

View File

@ -227,7 +227,7 @@ class OfflineImap:
'of %s'% ', '.join(UI_LIST.keys())) 'of %s'% ', '.join(UI_LIST.keys()))
if options.diagnostics: ui_type = 'basic' # enforce basic UI for --info if options.diagnostics: ui_type = 'basic' # enforce basic UI for --info
#dry-run? Set [general]dry-run=True # dry-run? Set [general]dry-run=True
if options.dryrun: if options.dryrun:
dryrun = config.set('general', 'dry-run', 'True') dryrun = config.set('general', 'dry-run', 'True')
config.set_if_not_exists('general', 'dry-run', 'False') config.set_if_not_exists('general', 'dry-run', 'False')

View File

@ -115,7 +115,6 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
@property @property
def readonly(self): def readonly(self):
"""Is the repository readonly?""" """Is the repository readonly?"""
return self._readonly return self._readonly
def getlocaleval(self): def getlocaleval(self):
@ -123,13 +122,11 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
def getfolders(self): def getfolders(self):
"""Returns a list of ALL folders on this server.""" """Returns a list of ALL folders on this server."""
return [] return []
def forgetfolders(self): def forgetfolders(self):
"""Forgets the cached list of folders, if any. Useful to run """Forgets the cached list of folders, if any. Useful to run
after a sync run.""" after a sync run."""
pass pass
def getsep(self): def getsep(self):
@ -150,8 +147,7 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
self.getconfboolean('createfolders', True) self.getconfboolean('createfolders', True)
def makefolder(self, foldername): def makefolder(self, foldername):
"""Create a new folder""" """Create a new folder."""
raise NotImplementedError raise NotImplementedError
def deletefolder(self, foldername): def deletefolder(self, foldername):
@ -200,7 +196,7 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
dst_haschanged = True # Need to refresh list dst_haschanged = True # Need to refresh list
except OfflineImapError as e: except OfflineImapError as e:
self.ui.error(e, exc_info()[2], self.ui.error(e, exc_info()[2],
"Creating folder %s on repository %s" %\ "Creating folder %s on repository %s"%
(src_name_t, dst_repo)) (src_name_t, dst_repo))
raise raise
status_repo.makefolder(src_name_t.replace(dst_repo.getsep(), status_repo.makefolder(src_name_t.replace(dst_repo.getsep(),
@ -218,7 +214,7 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
# case don't create it on it: # case don't create it on it:
if not self.should_sync_folder(dst_name_t): if not self.should_sync_folder(dst_name_t):
self.ui.debug('', "Not creating folder '%s' (repository '%s" self.ui.debug('', "Not creating folder '%s' (repository '%s"
"') as it would be filtered out on that repository." % "') as it would be filtered out on that repository."%
(dst_name_t, self)) (dst_name_t, self))
continue continue
# get IMAPFolder and see if the reverse nametrans # get IMAPFolder and see if the reverse nametrans

View File

@ -38,7 +38,7 @@ class IMAPRepository(BaseRepository):
self.folders = None self.folders = None
if self.getconf('sep', None): if self.getconf('sep', None):
self.ui.info("The 'sep' setting is being ignored for IMAP " self.ui.info("The 'sep' setting is being ignored for IMAP "
"repository '%s' (it's autodetected)" % self) "repository '%s' (it's autodetected)"% self)
def startkeepalive(self): def startkeepalive(self):
keepalivetime = self.getkeepalive() keepalivetime = self.getkeepalive()
@ -85,7 +85,7 @@ class IMAPRepository(BaseRepository):
acquireconnection() or it will still be `None`""" acquireconnection() or it will still be `None`"""
assert self.imapserver.delim != None, "'%s' " \ assert self.imapserver.delim != None, "'%s' " \
"repository called getsep() before the folder separator was " \ "repository called getsep() before the folder separator was " \
"queried from the server" % self "queried from the server"% self
return self.imapserver.delim return self.imapserver.delim
def gethost(self): def gethost(self):
@ -101,9 +101,8 @@ class IMAPRepository(BaseRepository):
try: try:
host = self.localeval.eval(host) host = self.localeval.eval(host)
except Exception as e: except Exception as e:
raise OfflineImapError("remotehosteval option for repository "\ raise OfflineImapError("remotehosteval option for repository "
"'%s' failed:\n%s" % (self, e), "'%s' failed:\n%s"% (self, e), OfflineImapError.ERROR.REPO), \
OfflineImapError.ERROR.REPO), \
None, exc_info()[2] None, exc_info()[2]
if host: if host:
self._host = host self._host = host
@ -115,9 +114,8 @@ class IMAPRepository(BaseRepository):
return self._host return self._host
# no success # no success
raise OfflineImapError("No remote host for repository "\ raise OfflineImapError("No remote host for repository "
"'%s' specified." % self, "'%s' specified."% self, OfflineImapError.ERROR.REPO)
OfflineImapError.ERROR.REPO)
def get_remote_identity(self): def get_remote_identity(self):
"""Remote identity is used for certain SASL mechanisms """Remote identity is used for certain SASL mechanisms
@ -139,8 +137,8 @@ class IMAPRepository(BaseRepository):
for m in mechs: for m in mechs:
if m not in supported: if m not in supported:
raise OfflineImapError("Repository %s: " % self + \ raise OfflineImapError("Repository %s: "% self + \
"unknown authentication mechanism '%s'" % m, "unknown authentication mechanism '%s'"% m,
OfflineImapError.ERROR.REPO) OfflineImapError.ERROR.REPO)
self.ui.debug('imap', "Using authentication mechanisms %s" % mechs) self.ui.debug('imap', "Using authentication mechanisms %s" % mechs)
@ -431,8 +429,7 @@ class IMAPRepository(BaseRepository):
result = imapobj.create(foldername) result = imapobj.create(foldername)
if result[0] != 'OK': if result[0] != 'OK':
raise OfflineImapError("Folder '%s'[%s] could not be created. " raise OfflineImapError("Folder '%s'[%s] could not be created. "
"Server responded: %s" % \ "Server responded: %s"% (foldername, self, str(result)),
(foldername, self, str(result)),
OfflineImapError.ERROR.FOLDER) OfflineImapError.ERROR.FOLDER)
finally: finally:
self.imapserver.releaseconnection(imapobj) self.imapserver.releaseconnection(imapobj)

View File

@ -101,7 +101,7 @@ class LocalStatusRepository(BaseRepository):
folder = self.LocalStatusFolderClass(foldername, self) folder = self.LocalStatusFolderClass(foldername, self)
# if folder is empty, try to import data from an other backend # If folder is empty, try to import data from an other backend.
if folder.isnewfolder(): if folder.isnewfolder():
self.import_other_backend(folder) self.import_other_backend(folder)

View File

@ -53,7 +53,7 @@ class Repository(object):
'GmailMaildir': GmailMaildirRepository} 'GmailMaildir': GmailMaildirRepository}
elif reqtype == 'status': elif reqtype == 'status':
# create and return a LocalStatusRepository # create and return a LocalStatusRepository.
name = account.getconf('localrepository') name = account.getconf('localrepository')
return LocalStatusRepository(name, account) return LocalStatusRepository(name, account)
@ -61,7 +61,7 @@ class Repository(object):
errstr = "Repository type %s not supported" % reqtype errstr = "Repository type %s not supported" % reqtype
raise OfflineImapError(errstr, OfflineImapError.ERROR.REPO) raise OfflineImapError(errstr, OfflineImapError.ERROR.REPO)
# Get repository type # Get repository type.
config = account.getconfig() config = account.getconfig()
try: try:
repostype = config.get('Repository ' + name, 'type').strip() repostype = config.get('Repository ' + name, 'type').strip()
@ -74,8 +74,8 @@ class Repository(object):
try: try:
repo = typemap[repostype] repo = typemap[repostype]
except KeyError: except KeyError:
errstr = "'%s' repository not supported for '%s' repositories." \ errstr = "'%s' repository not supported for '%s' repositories."% \
% (repostype, reqtype) (repostype, reqtype)
raise OfflineImapError(errstr, OfflineImapError.ERROR.REPO), \ raise OfflineImapError(errstr, OfflineImapError.ERROR.REPO), \
None, exc_info()[2] None, exc_info()[2]

View File

@ -32,6 +32,7 @@ from offlineimap.ui import getglobalui
def semaphorereset(semaphore, originalstate): def semaphorereset(semaphore, originalstate):
"""Block until `semaphore` gets back to its original state, ie all acquired """Block until `semaphore` gets back to its original state, ie all acquired
resources have been released.""" resources have been released."""
for i in range(originalstate): for i in range(originalstate):
semaphore.acquire() semaphore.acquire()
# Now release these. # Now release these.
@ -41,6 +42,7 @@ def semaphorereset(semaphore, originalstate):
class threadlist: class threadlist:
"""Store the list of all threads in the software so it can be used to find out """Store the list of all threads in the software so it can be used to find out
what's running and what's not.""" what's running and what's not."""
def __init__(self): def __init__(self):
self.lock = Lock() self.lock = Lock()
self.list = [] self.list = []
@ -98,6 +100,7 @@ def exitnotifymonitorloop(callback):
while the other thread is waiting. while the other thread is waiting.
:type callback: a callable function :type callback: a callable function
""" """
global exitthreads global exitthreads
do_loop = True do_loop = True
while do_loop: while do_loop:
@ -116,6 +119,7 @@ def threadexited(thread):
"""Called when a thread exits. """Called when a thread exits.
Main thread is aborted when this returns True.""" Main thread is aborted when this returns True."""
ui = getglobalui() ui = getglobalui()
if thread.exit_exception: if thread.exit_exception:
if isinstance(thread.exit_exception, SystemExit): if isinstance(thread.exit_exception, SystemExit):
@ -139,8 +143,9 @@ class ExitNotifyThread(Thread):
The thread can set instance variables self.exit_message for a human The thread can set instance variables self.exit_message for a human
readable reason of the thread exit.""" readable reason of the thread exit."""
profiledir = None profiledir = None
"""class variable that is set to the profile directory if required""" """Class variable that is set to the profile directory if required."""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(ExitNotifyThread, self).__init__(*args, **kwargs) super(ExitNotifyThread, self).__init__(*args, **kwargs)
@ -167,7 +172,7 @@ class ExitNotifyThread(Thread):
except SystemExit: except SystemExit:
pass pass
prof.dump_stats(os.path.join(ExitNotifyThread.profiledir, prof.dump_stats(os.path.join(ExitNotifyThread.profiledir,
"%s_%s.prof" % (self.ident, self.getName()))) "%s_%s.prof"% (self.ident, self.getName())))
except Exception as e: except Exception as e:
# Thread exited with Exception, store it # Thread exited with Exception, store it
tb = traceback.format_exc() tb = traceback.format_exc()
@ -179,6 +184,7 @@ class ExitNotifyThread(Thread):
def set_exit_exception(self, exc, st=None): def set_exit_exception(self, exc, st=None):
"""Sets Exception and stacktrace of a thread, so that other """Sets Exception and stacktrace of a thread, so that other
threads can query its exit status""" threads can query its exit status"""
self._exit_exc = exc self._exit_exc = exc
self._exit_stacktrace = st self._exit_stacktrace = st
@ -187,16 +193,19 @@ class ExitNotifyThread(Thread):
"""Returns the cause of the exit, one of: """Returns the cause of the exit, one of:
Exception() -- the thread aborted with this exception Exception() -- the thread aborted with this exception
None -- normal termination.""" None -- normal termination."""
return self._exit_exc return self._exit_exc
@property @property
def exit_stacktrace(self): def exit_stacktrace(self):
"""Returns a string representing the stack trace if set""" """Returns a string representing the stack trace if set"""
return self._exit_stacktrace return self._exit_stacktrace
@classmethod @classmethod
def set_profiledir(cls, directory): def set_profiledir(cls, directory):
"""If set, will output profile information to 'directory'""" """If set, will output profile information to 'directory'"""
cls.profiledir = directory cls.profiledir = directory
@ -210,6 +219,7 @@ instancelimitedlock = Lock()
def initInstanceLimit(instancename, instancemax): def initInstanceLimit(instancename, instancemax):
"""Initialize the instance-limited thread implementation to permit """Initialize the instance-limited thread implementation to permit
up to intancemax threads with the given instancename.""" up to intancemax threads with the given instancename."""
instancelimitedlock.acquire() instancelimitedlock.acquire()
if not instancename in instancelimitedsems: if not instancename in instancelimitedsems:
instancelimitedsems[instancename] = BoundedSemaphore(instancemax) instancelimitedsems[instancename] = BoundedSemaphore(instancemax)

View File

@ -33,17 +33,19 @@ class CursesUtil:
# iolock protects access to the # iolock protects access to the
self.iolock = RLock() self.iolock = RLock()
self.tframe_lock = RLock() self.tframe_lock = RLock()
"""tframe_lock protects the self.threadframes manipulation to # tframe_lock protects the self.threadframes manipulation to
only happen from 1 thread""" # only happen from 1 thread.
self.colormap = {} self.colormap = {}
"""dict, translating color string to curses color pair number""" """dict, translating color string to curses color pair number"""
def curses_colorpair(self, col_name): def curses_colorpair(self, col_name):
"""Return the curses color pair, that corresponds to the color""" """Return the curses color pair, that corresponds to the color."""
return curses.color_pair(self.colormap[col_name]) return curses.color_pair(self.colormap[col_name])
def init_colorpairs(self): def init_colorpairs(self):
"""initialize the curses color pairs available""" """Initialize the curses color pairs available."""
# set special colors 'gray' and 'banner' # set special colors 'gray' and 'banner'
self.colormap['white'] = 0 #hardcoded by curses self.colormap['white'] = 0 #hardcoded by curses
curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE) curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE)
@ -66,24 +68,27 @@ class CursesUtil:
curses.init_pair(i, fcol, bcol) curses.init_pair(i, fcol, bcol)
def lock(self, block=True): def lock(self, block=True):
"""Locks the Curses ui thread """Locks the Curses ui thread.
Can be invoked multiple times from the owning thread. Invoking Can be invoked multiple times from the owning thread. Invoking
from a non-owning thread blocks and waits until it has been from a non-owning thread blocks and waits until it has been
unlocked by the owning thread.""" unlocked by the owning thread."""
return self.iolock.acquire(block) return self.iolock.acquire(block)
def unlock(self): def unlock(self):
"""Unlocks the Curses ui thread """Unlocks the Curses ui thread.
Decrease the lock counter by one and unlock the ui thread if the Decrease the lock counter by one and unlock the ui thread if the
counter reaches 0. Only call this method when the calling counter reaches 0. Only call this method when the calling
thread owns the lock. A RuntimeError is raised if this method is thread owns the lock. A RuntimeError is raised if this method is
called when the lock is unlocked.""" called when the lock is unlocked."""
self.iolock.release() self.iolock.release()
def exec_locked(self, target, *args, **kwargs): def exec_locked(self, target, *args, **kwargs):
"""Perform an operation with full locking.""" """Perform an operation with full locking."""
self.lock() self.lock()
try: try:
target(*args, **kwargs) target(*args, **kwargs)
@ -113,31 +118,34 @@ class CursesAccountFrame:
def __init__(self, ui, account): def __init__(self, ui, account):
""" """
:param account: An Account() or None (for eg SyncrunnerThread)""" :param account: An Account() or None (for eg SyncrunnerThread)"""
self.children = [] self.children = []
self.account = account if account else '*Control' self.account = account if account else '*Control'
self.ui = ui self.ui = ui
self.window = None self.window = None
"""Curses window associated with this acc""" # Curses window associated with this acc.
self.acc_num = None self.acc_num = None
"""Account number (& hotkey) associated with this acc""" # Account number (& hotkey) associated with this acc.
self.location = 0 self.location = 0
"""length of the account prefix string""" # length of the account prefix string
def drawleadstr(self, secs = 0): def drawleadstr(self, secs = 0):
"""Draw the account status string """Draw the account status string.
secs tells us how long we are going to sleep.""" secs tells us how long we are going to sleep."""
sleepstr = '%3d:%02d' % (secs // 60, secs % 60) if secs else 'active'
accstr = '%s: [%s] %12.12s: ' % (self.acc_num, sleepstr, self.account) sleepstr = '%3d:%02d'% (secs // 60, secs % 60) if secs else 'active'
accstr = '%s: [%s] %12.12s: '% (self.acc_num, sleepstr, self.account)
self.ui.exec_locked(self.window.addstr, 0, 0, accstr) self.ui.exec_locked(self.window.addstr, 0, 0, accstr)
self.location = len(accstr) self.location = len(accstr)
def setwindow(self, curses_win, acc_num): def setwindow(self, curses_win, acc_num):
"""Register an curses win and a hotkey as Account window """Register an curses win and a hotkey as Account window.
:param curses_win: the curses window associated with an account :param curses_win: the curses window associated with an account
:param acc_num: int denoting the hotkey associated with this account.""" :param acc_num: int denoting the hotkey associated with this account."""
self.window = curses_win self.window = curses_win
self.acc_num = acc_num self.acc_num = acc_num
self.drawleadstr() self.drawleadstr()
@ -147,39 +155,43 @@ class CursesAccountFrame:
self.location += 1 self.location += 1
def get_new_tframe(self): def get_new_tframe(self):
"""Create a new ThreadFrame and append it to self.children """Create a new ThreadFrame and append it to self.children.
:returns: The new ThreadFrame""" :returns: The new ThreadFrame"""
tf = CursesThreadFrame(self.ui, self.window, self.location, 0) tf = CursesThreadFrame(self.ui, self.window, self.location, 0)
self.location += 1 self.location += 1
self.children.append(tf) self.children.append(tf)
return tf return tf
def sleeping(self, sleepsecs, remainingsecs): def sleeping(self, sleepsecs, remainingsecs):
"""show how long we are going to sleep and sleep """Show how long we are going to sleep and sleep.
:returns: Boolean, whether we want to abort the sleep""" :returns: Boolean, whether we want to abort the sleep"""
self.drawleadstr(remainingsecs) self.drawleadstr(remainingsecs)
self.ui.exec_locked(self.window.refresh) self.ui.exec_locked(self.window.refresh)
time.sleep(sleepsecs) time.sleep(sleepsecs)
return self.account.get_abort_event() return self.account.get_abort_event()
def syncnow(self): def syncnow(self):
"""Request that we stop sleeping asap and continue to sync""" """Request that we stop sleeping asap and continue to sync."""
# if this belongs to an Account (and not *Control), set the # if this belongs to an Account (and not *Control), set the
# skipsleep pref # skipsleep pref
if isinstance(self.account, offlineimap.accounts.Account): if isinstance(self.account, offlineimap.accounts.Account):
self.ui.info("Requested synchronization for acc: %s" % self.account) self.ui.info("Requested synchronization for acc: %s"% self.account)
self.account.config.set('Account %s' % self.account.name, self.account.config.set('Account %s'% self.account.name,
'skipsleep', '1') 'skipsleep', '1')
class CursesThreadFrame: class CursesThreadFrame:
""" """curses_color: current color pair for logging."""
curses_color: current color pair for logging"""
def __init__(self, ui, acc_win, x, y): def __init__(self, ui, acc_win, x, y):
""" """
:param ui: is a Blinkenlights() instance :param ui: is a Blinkenlights() instance
:param acc_win: curses Account window""" :param acc_win: curses Account window"""
self.ui = ui self.ui = ui
self.window = acc_win self.window = acc_win
self.x = x self.x = x
@ -188,7 +200,10 @@ class CursesThreadFrame:
def setcolor(self, color, modifier=0): def setcolor(self, color, modifier=0):
"""Draw the thread symbol '@' in the specified color """Draw the thread symbol '@' in the specified color
:param modifier: Curses modified, such as curses.A_BOLD"""
:param modifier: Curses modified, such as curses.A_BOLD
"""
self.curses_color = modifier | self.ui.curses_colorpair(color) self.curses_color = modifier | self.ui.curses_colorpair(color)
self.colorname = color self.colorname = color
self.display() self.display()
@ -201,7 +216,8 @@ class CursesThreadFrame:
self.ui.exec_locked(locked_display) self.ui.exec_locked(locked_display)
def update(self, acc_win, x, y): def update(self, acc_win, x, y):
"""Update the xy position of the '.' (and possibly the aframe)""" """Update the xy position of the '.' (and possibly the aframe)."""
self.window = acc_win self.window = acc_win
self.y = y self.y = y
self.x = x self.x = x
@ -213,6 +229,7 @@ class CursesThreadFrame:
class InputHandler(ExitNotifyThread): class InputHandler(ExitNotifyThread):
"""Listens for input via the curses interfaces""" """Listens for input via the curses interfaces"""
#TODO, we need to use the ugly exitnotifythread (rather than simply #TODO, we need to use the ugly exitnotifythread (rather than simply
#threading.Thread here, so exiting this thread via the callback #threading.Thread here, so exiting this thread via the callback
#handler, kills off all parents too. Otherwise, they would simply #handler, kills off all parents too. Otherwise, they would simply
@ -222,17 +239,18 @@ class InputHandler(ExitNotifyThread):
self.char_handler = None self.char_handler = None
self.ui = ui self.ui = ui
self.enabled = Event() self.enabled = Event()
"""We will only parse input if we are enabled""" # We will only parse input if we are enabled.
self.inputlock = RLock() self.inputlock = RLock()
"""denotes whether we should be handling the next char.""" # denotes whether we should be handling the next char.
self.start() #automatically start the thread self.start() #automatically start the thread
def get_next_char(self): def get_next_char(self):
"""return the key pressed or -1 """Return the key pressed or -1.
Wait until `enabled` and loop internally every stdscr.timeout() Wait until `enabled` and loop internally every stdscr.timeout()
msecs, releasing the inputlock. msecs, releasing the inputlock.
:returns: char or None if disabled while in here""" :returns: char or None if disabled while in here"""
self.enabled.wait() self.enabled.wait()
while self.enabled.is_set(): while self.enabled.is_set():
with self.inputlock: with self.inputlock:
@ -247,13 +265,14 @@ class InputHandler(ExitNotifyThread):
#curses.ungetch(char) #curses.ungetch(char)
def set_char_hdlr(self, callback): def set_char_hdlr(self, callback):
"""Sets a character callback handler """Sets a character callback handler.
If a key is pressed it will be passed to this handler. Keys If a key is pressed it will be passed to this handler. Keys
include the curses.KEY_RESIZE key. include the curses.KEY_RESIZE key.
callback is a function taking a single arg -- the char pressed. callback is a function taking a single arg -- the char pressed.
If callback is None, input will be ignored.""" If callback is None, input will be ignored."""
with self.inputlock: with self.inputlock:
self.char_handler = callback self.char_handler = callback
# start or stop the parsing of things # start or stop the parsing of things
@ -266,13 +285,14 @@ class InputHandler(ExitNotifyThread):
"""Call this method when you want exclusive input control. """Call this method when you want exclusive input control.
Make sure to call input_release afterwards! While this lockis Make sure to call input_release afterwards! While this lockis
held, input can go to e.g. the getpass input. held, input can go to e.g. the getpass input."""
"""
self.enabled.clear() self.enabled.clear()
self.inputlock.acquire() self.inputlock.acquire()
def input_release(self): def input_release(self):
"""Call this method when you are done getting input.""" """Call this method when you are done getting input."""
self.inputlock.release() self.inputlock.release()
self.enabled.set() self.enabled.set()
@ -301,7 +321,7 @@ class CursesLogHandler(logging.StreamHandler):
self.ui.stdscr.refresh() self.ui.stdscr.refresh()
class Blinkenlights(UIBase, CursesUtil): class Blinkenlights(UIBase, CursesUtil):
"""Curses-cased fancy UI """Curses-cased fancy UI.
Notable instance variables self. ....: Notable instance variables self. ....:
@ -319,7 +339,7 @@ class Blinkenlights(UIBase, CursesUtil):
################################################## UTILS ################################################## UTILS
def setup_consolehandler(self): def setup_consolehandler(self):
"""Backend specific console handler """Backend specific console handler.
Sets up things and adds them to self.logger. Sets up things and adds them to self.logger.
:returns: The logging.Handler() for console output""" :returns: The logging.Handler() for console output"""
@ -337,7 +357,7 @@ class Blinkenlights(UIBase, CursesUtil):
return ch return ch
def isusable(s): def isusable(s):
"""Returns true if the backend is usable ie Curses works""" """Returns true if the backend is usable ie Curses works."""
# Not a terminal? Can't use curses. # Not a terminal? Can't use curses.
if not sys.stdout.isatty() and sys.stdin.isatty(): if not sys.stdout.isatty() and sys.stdin.isatty():
@ -393,7 +413,7 @@ class Blinkenlights(UIBase, CursesUtil):
self.info(offlineimap.banner) self.info(offlineimap.banner)
def acct(self, *args): def acct(self, *args):
"""Output that we start syncing an account (and start counting)""" """Output that we start syncing an account (and start counting)."""
self.gettf().setcolor('purple') self.gettf().setcolor('purple')
super(Blinkenlights, self).acct(*args) super(Blinkenlights, self).acct(*args)
@ -458,7 +478,8 @@ class Blinkenlights(UIBase, CursesUtil):
super(Blinkenlights, self).threadExited(thread) super(Blinkenlights, self).threadExited(thread)
def gettf(self): def gettf(self):
"""Return the ThreadFrame() of the current thread""" """Return the ThreadFrame() of the current thread."""
cur_thread = currentThread() cur_thread = currentThread()
acc = self.getthreadaccount() #Account() or None acc = self.getthreadaccount() #Account() or None
@ -504,7 +525,7 @@ class Blinkenlights(UIBase, CursesUtil):
def sleep(self, sleepsecs, account): def sleep(self, sleepsecs, account):
self.gettf().setcolor('red') self.gettf().setcolor('red')
self.info("Next sync in %d:%02d" % (sleepsecs / 60, sleepsecs % 60)) self.info("Next sync in %d:%02d"% (sleepsecs / 60, sleepsecs % 60))
return super(Blinkenlights, self).sleep(sleepsecs, account) return super(Blinkenlights, self).sleep(sleepsecs, account)
def sleeping(self, sleepsecs, remainingsecs): def sleeping(self, sleepsecs, remainingsecs):
@ -515,13 +536,14 @@ class Blinkenlights(UIBase, CursesUtil):
return accframe.sleeping(sleepsecs, remainingsecs) return accframe.sleeping(sleepsecs, remainingsecs)
def resizeterm(self): def resizeterm(self):
"""Resize the current windows""" """Resize the current windows."""
self.exec_locked(self.setupwindows, True) self.exec_locked(self.setupwindows, True)
def mainException(self): def mainException(self):
UIBase.mainException(self) UIBase.mainException(self)
def getpass(self, accountname, config, errmsg = None): def getpass(self, accountname, config, errmsg=None):
# disable the hotkeys inputhandler # disable the hotkeys inputhandler
self.inputhandler.input_acquire() self.inputhandler.input_acquire()
@ -540,10 +562,11 @@ class Blinkenlights(UIBase, CursesUtil):
return password return password
def setupwindows(self, resize=False): def setupwindows(self, resize=False):
"""Setup and draw bannerwin and logwin """Setup and draw bannerwin and logwin.
If `resize`, don't create new windows, just adapt size. This If `resize`, don't create new windows, just adapt size. This
function should be invoked with CursesUtils.locked().""" function should be invoked with CursesUtils.locked()."""
self.height, self.width = self.stdscr.getmaxyx() self.height, self.width = self.stdscr.getmaxyx()
self.logheight = self.height - len(self.accframes) - 1 self.logheight = self.height - len(self.accframes) - 1
if resize: if resize:
@ -571,14 +594,15 @@ class Blinkenlights(UIBase, CursesUtil):
curses.doupdate() curses.doupdate()
def draw_bannerwin(self): def draw_bannerwin(self):
"""Draw the top-line banner line""" """Draw the top-line banner line."""
if curses.has_colors(): if curses.has_colors():
color = curses.A_BOLD | self.curses_colorpair('banner') color = curses.A_BOLD | self.curses_colorpair('banner')
else: else:
color = curses.A_REVERSE color = curses.A_REVERSE
self.bannerwin.clear() # Delete old content (eg before resizes) self.bannerwin.clear() # Delete old content (eg before resizes)
self.bannerwin.bkgd(' ', color) # Fill background with that color self.bannerwin.bkgd(' ', color) # Fill background with that color
string = "%s %s" % (offlineimap.__productname__, string = "%s %s"% (offlineimap.__productname__,
offlineimap.__bigversion__) offlineimap.__bigversion__)
self.bannerwin.addstr(0, 0, string, color) self.bannerwin.addstr(0, 0, string, color)
self.bannerwin.addstr(0, self.width -len(offlineimap.__copyright__) -1, self.bannerwin.addstr(0, self.width -len(offlineimap.__copyright__) -1,
@ -586,7 +610,8 @@ class Blinkenlights(UIBase, CursesUtil):
self.bannerwin.noutrefresh() self.bannerwin.noutrefresh()
def draw_logwin(self): def draw_logwin(self):
"""(Re)draw the current logwindow""" """(Re)draw the current logwindow."""
if curses.has_colors(): if curses.has_colors():
color = curses.color_pair(0) #default colors color = curses.color_pair(0) #default colors
else: else:
@ -596,9 +621,10 @@ class Blinkenlights(UIBase, CursesUtil):
self.logwin.bkgd(' ', color) self.logwin.bkgd(' ', color)
def getaccountframe(self, acc_name): def getaccountframe(self, acc_name):
"""Return an AccountFrame() corresponding to acc_name """Return an AccountFrame() corresponding to acc_name.
Note that the *control thread uses acc_name `None`.""" Note that the *control thread uses acc_name `None`."""
with self.aflock: with self.aflock:
# 1) Return existing or 2) create a new CursesAccountFrame. # 1) Return existing or 2) create a new CursesAccountFrame.
if acc_name in self.accframes: return self.accframes[acc_name] if acc_name in self.accframes: return self.accframes[acc_name]

View File

@ -36,14 +36,18 @@ debugtypes = {'':'Other offlineimap related sync messages',
globalui = None globalui = None
def setglobalui(newui): def setglobalui(newui):
"""Set the global ui object to be used for logging""" """Set the global ui object to be used for logging."""
global globalui global globalui
globalui = newui globalui = newui
def getglobalui(): def getglobalui():
"""Return the current ui object""" """Return the current ui object."""
global globalui global globalui
return globalui return globalui
class UIBase(object): class UIBase(object):
def __init__(self, config, loglevel=logging.INFO): def __init__(self, config, loglevel=logging.INFO):
self.config = config self.config = config
@ -65,11 +69,11 @@ class UIBase(object):
self.logger = logging.getLogger('OfflineImap') self.logger = logging.getLogger('OfflineImap')
self.logger.setLevel(loglevel) self.logger.setLevel(loglevel)
self._log_con_handler = self.setup_consolehandler() self._log_con_handler = self.setup_consolehandler()
"""The console handler (we need access to be able to lock it)""" """The console handler (we need access to be able to lock it)."""
################################################## UTILS ################################################## UTILS
def setup_consolehandler(self): def setup_consolehandler(self):
"""Backend specific console handler """Backend specific console handler.
Sets up things and adds them to self.logger. Sets up things and adds them to self.logger.
:returns: The logging.Handler() for console output""" :returns: The logging.Handler() for console output"""
@ -86,7 +90,8 @@ class UIBase(object):
return ch return ch
def setlogfile(self, logfile): def setlogfile(self, logfile):
"""Create file handler which logs to file""" """Create file handler which logs to file."""
fh = logging.FileHandler(logfile, 'at') fh = logging.FileHandler(logfile, 'at')
file_formatter = logging.Formatter("%(asctime)s %(levelname)s: " file_formatter = logging.Formatter("%(asctime)s %(levelname)s: "
"%(message)s", '%Y-%m-%d %H:%M:%S') "%(message)s", '%Y-%m-%d %H:%M:%S')
@ -107,13 +112,14 @@ class UIBase(object):
def info(self, msg): def info(self, msg):
"""Display a message.""" """Display a message."""
self.logger.info(msg) self.logger.info(msg)
def warn(self, msg, minor = 0): def warn(self, msg, minor=0):
self.logger.warning(msg) self.logger.warning(msg)
def error(self, exc, exc_traceback=None, msg=None): def error(self, exc, exc_traceback=None, msg=None):
"""Log a message at severity level ERROR """Log a message at severity level ERROR.
Log Exception 'exc' to error log, possibly prepended by a preceding Log Exception 'exc' to error log, possibly prepended by a preceding
error "msg", detailing at what point the error occurred. error "msg", detailing at what point the error occurred.
@ -160,7 +166,7 @@ class UIBase(object):
"'%s')" % (cur_thread.getName(), "'%s')" % (cur_thread.getName(),
self.getthreadaccount(cur_thread), account)) self.getthreadaccount(cur_thread), account))
else: else:
self.debug('thread', "Register new thread '%s' (account '%s')" %\ self.debug('thread', "Register new thread '%s' (account '%s')"%
(cur_thread.getName(), account)) (cur_thread.getName(), account))
self.threadaccounts[cur_thread] = account self.threadaccounts[cur_thread] = account
@ -216,7 +222,7 @@ class UIBase(object):
self.warn("Invalid debug type: %s" % debugtype) self.warn("Invalid debug type: %s" % debugtype)
def getnicename(self, object): def getnicename(self, object):
"""Return the type of a repository or Folder as string """Return the type of a repository or Folder as string.
(IMAP, Gmail, Maildir, etc...)""" (IMAP, Gmail, Maildir, etc...)"""
@ -234,7 +240,7 @@ class UIBase(object):
################################################## INPUT ################################################## INPUT
def getpass(self, accountname, config, errmsg = None): def getpass(self, accountname, config, errmsg = None):
raise NotImplementedError("Prompting for a password is not supported"\ raise NotImplementedError("Prompting for a password is not supported"
" in this UI backend.") " in this UI backend.")
def folderlist(self, folder_list): def folderlist(self, folder_list):
@ -248,7 +254,7 @@ class UIBase(object):
return return
self.warn("Attempted to synchronize message %d to folder %s[%s], " self.warn("Attempted to synchronize message %d to folder %s[%s], "
"but that folder is read-only. The message will not be " "but that folder is read-only. The message will not be "
"copied to that folder." % ( "copied to that folder."% (
uid, self.getnicename(destfolder), destfolder)) uid, self.getnicename(destfolder), destfolder))
def flagstoreadonly(self, destfolder, uidlist, flags): def flagstoreadonly(self, destfolder, uidlist, flags):
@ -257,7 +263,7 @@ class UIBase(object):
return return
self.warn("Attempted to modify flags for messages %s in folder %s[%s], " self.warn("Attempted to modify flags for messages %s in folder %s[%s], "
"but that folder is read-only. No flags have been modified " "but that folder is read-only. No flags have been modified "
"for that message." % ( "for that message."% (
str(uidlist), self.getnicename(destfolder), destfolder)) str(uidlist), self.getnicename(destfolder), destfolder))
def labelstoreadonly(self, destfolder, uidlist, labels): def labelstoreadonly(self, destfolder, uidlist, labels):
@ -266,7 +272,7 @@ class UIBase(object):
return return
self.warn("Attempted to modify labels for messages %s in folder %s[%s], " self.warn("Attempted to modify labels for messages %s in folder %s[%s], "
"but that folder is read-only. No labels have been modified " "but that folder is read-only. No labels have been modified "
"for that message." % ( "for that message."% (
str(uidlist), self.getnicename(destfolder), destfolder)) str(uidlist), self.getnicename(destfolder), destfolder))
def deletereadonly(self, destfolder, uidlist): def deletereadonly(self, destfolder, uidlist):
@ -275,7 +281,7 @@ class UIBase(object):
return return
self.warn("Attempted to delete messages %s in folder %s[%s], but that " self.warn("Attempted to delete messages %s in folder %s[%s], but that "
"folder is read-only. No messages have been deleted in that " "folder is read-only. No messages have been deleted in that "
"folder." % (str(uidlist), self.getnicename(destfolder), "folder."% (str(uidlist), self.getnicename(destfolder),
destfolder)) destfolder))
################################################## MESSAGES ################################################## MESSAGES
@ -293,7 +299,7 @@ class UIBase(object):
if not self.logger.isEnabledFor(logging.INFO): return if not self.logger.isEnabledFor(logging.INFO): return
displaystr = '' displaystr = ''
hostname = hostname if hostname else '' hostname = hostname if hostname else ''
port = "%s" % port if port else '' port = "%s"% port if port else ''
if hostname: if hostname:
displaystr = ' to %s:%s' % (hostname, port) displaystr = ' to %s:%s' % (hostname, port)
self.logger.info("Establishing connection%s" % displaystr) self.logger.info("Establishing connection%s" % displaystr)
@ -309,7 +315,7 @@ class UIBase(object):
sec = time.time() - self.acct_startimes[account] sec = time.time() - self.acct_startimes[account]
del self.acct_startimes[account] del self.acct_startimes[account]
self.logger.info("*** Finished account '%s' in %d:%02d" % self.logger.info("*** Finished account '%s' in %d:%02d"%
(account, sec // 60, sec % 60)) (account, sec // 60, sec % 60))
def syncfolders(self, src_repo, dst_repo): def syncfolders(self, src_repo, dst_repo):
@ -321,14 +327,16 @@ class UIBase(object):
############################## Folder syncing ############################## Folder syncing
def makefolder(self, repo, foldername): def makefolder(self, repo, foldername):
"""Called when a folder is created""" """Called when a folder is created."""
prefix = "[DRYRUN] " if self.dryrun else "" prefix = "[DRYRUN] " if self.dryrun else ""
self.info("{0}Creating folder {1}[{2}]".format( self.info(("{0}Creating folder {1}[{2}]".format(
prefix, foldername, repo)) prefix, foldername, repo)))
def syncingfolder(self, srcrepos, srcfolder, destrepos, destfolder): def syncingfolder(self, srcrepos, srcfolder, destrepos, destfolder):
"""Called when a folder sync operation is started.""" """Called when a folder sync operation is started."""
self.logger.info("Syncing %s: %s -> %s" % (srcfolder,
self.logger.info("Syncing %s: %s -> %s"% (srcfolder,
self.getnicename(srcrepos), self.getnicename(srcrepos),
self.getnicename(destrepos))) self.getnicename(destrepos)))
@ -344,7 +352,7 @@ class UIBase(object):
folder.get_saveduidvalidity(), folder.get_uidvalidity())) folder.get_saveduidvalidity(), folder.get_uidvalidity()))
def loadmessagelist(self, repos, folder): def loadmessagelist(self, repos, folder):
self.logger.debug(u"Loading message list for %s[%s]"% ( self.logger.debug("Loading message list for %s[%s]"% (
self.getnicename(repos), self.getnicename(repos),
folder)) folder))
@ -407,9 +415,8 @@ class UIBase(object):
self.getnicename(repository))) self.getnicename(repository)))
try: try:
if hasattr(repository, 'gethost'): # IMAP if hasattr(repository, 'gethost'): # IMAP
self._msg("Host: %s Port: %s SSL: %s" % (repository.gethost(), self._msg("Host: %s Port: %s SSL: %s"% (repository.gethost(),
repository.getport(), repository.getport(), repository.getssl()))
repository.getssl()))
try: try:
conn = repository.imapserver.acquireconnection() conn = repository.imapserver.acquireconnection()
except OfflineImapError as e: except OfflineImapError as e:
@ -437,7 +444,7 @@ class UIBase(object):
self._msg("nametrans= %s\n" % nametrans) self._msg("nametrans= %s\n" % nametrans)
folders = repository.getfolders() folders = repository.getfolders()
foldernames = [(f.name, f.getvisiblename(), f.sync_this) \ foldernames = [(f.name, f.getvisiblename(), f.sync_this)
for f in folders] for f in folders]
folders = [] folders = []
for name, visiblename, sync_this in foldernames: for name, visiblename, sync_this in foldernames:
@ -454,7 +461,7 @@ class UIBase(object):
def savemessage(self, debugtype, uid, flags, folder): def savemessage(self, debugtype, uid, flags, folder):
"""Output a log line stating that we save a msg.""" """Output a log line stating that we save a msg."""
self.debug(debugtype, u"Write mail '%s:%d' with flags %s"% self.debug(debugtype, "Write mail '%s:%d' with flags %s"%
(folder, uid, repr(flags))) (folder, uid, repr(flags)))
################################################## Threads ################################################## Threads
@ -465,7 +472,7 @@ class UIBase(object):
% (len(self.debugmessages[thread]), thread.getName()) % (len(self.debugmessages[thread]), thread.getName())
message += "\n".join(self.debugmessages[thread]) message += "\n".join(self.debugmessages[thread])
else: else:
message = "\nNo debug messages were logged for %s." % \ message = "\nNo debug messages were logged for %s."% \
thread.getName() thread.getName()
return message return message
@ -474,9 +481,9 @@ class UIBase(object):
del self.debugmessages[thread] del self.debugmessages[thread]
def getThreadExceptionString(self, thread): def getThreadExceptionString(self, thread):
message = u"Thread '%s' terminated with exception:\n%s"% \ message = "Thread '%s' terminated with exception:\n%s"% \
(thread.getName(), thread.exit_stacktrace) (thread.getName(), thread.exit_stacktrace)
message += u"\n" + self.getThreadDebugLog(thread) message += "\n" + self.getThreadDebugLog(thread)
return message return message
def threadException(self, thread): def threadException(self, thread):
@ -492,21 +499,21 @@ class UIBase(object):
#print any exceptions that have occurred over the run #print any exceptions that have occurred over the run
if not self.exc_queue.empty(): if not self.exc_queue.empty():
self.warn(u"ERROR: Exceptions occurred during the run!") self.warn("ERROR: Exceptions occurred during the run!")
while not self.exc_queue.empty(): while not self.exc_queue.empty():
msg, exc, exc_traceback = self.exc_queue.get() msg, exc, exc_traceback = self.exc_queue.get()
if msg: if msg:
self.warn(u"ERROR: %s\n %s"% (msg, exc)) self.warn("ERROR: %s\n %s"% (msg, exc))
else: else:
self.warn(u"ERROR: %s"% (exc)) self.warn("ERROR: %s"% (exc))
if exc_traceback: if exc_traceback:
self.warn(u"\nTraceback:\n%s"% "".join( self.warn("\nTraceback:\n%s"% "".join(
traceback.format_tb(exc_traceback))) traceback.format_tb(exc_traceback)))
if errormsg and errortitle: if errormsg and errortitle:
self.warn(u'ERROR: %s\n\n%s\n'% (errortitle, errormsg)) self.warn('ERROR: %s\n\n%s\n'% (errortitle, errormsg))
elif errormsg: elif errormsg:
self.warn(u'%s\n' % errormsg) self.warn('%s\n'% errormsg)
sys.exit(exitstatus) sys.exit(exitstatus)
def threadExited(self, thread): def threadExited(self, thread):