more style consistency
Signed-off-by: Nicolas Sebrecht <nicolas.s-dev@laposte.net>
This commit is contained in:
parent
5318af606e
commit
461554b7b1
@ -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.
|
||||||
@ -407,12 +414,12 @@ def syncfolder(account, remotefolder, quick):
|
|||||||
|
|
||||||
# Write the mailboxes
|
# Write the mailboxes
|
||||||
mbnames.add(account.name, localfolder.getname(),
|
mbnames.add(account.name, localfolder.getname(),
|
||||||
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():
|
||||||
|
@ -48,13 +48,11 @@ 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
|
||||||
# and check filtering status
|
# and check filtering status
|
||||||
@ -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,18 +742,16 @@ 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:
|
||||||
self.ui.info("[DRYRUN] Copy {0} messages from {1}[{2}] to {3}".format(
|
self.ui.info("[DRYRUN] Copy {0} messages from {1}[{2}] to {3}".format(
|
||||||
num_to_copy, self, self.repository, dstfolder.repository))
|
num_to_copy, self, self.repository, dstfolder.repository))
|
||||||
return
|
return
|
||||||
for num, uid in enumerate(copylist):
|
for num, uid in enumerate(copylist):
|
||||||
# bail out on CTRL-C or SIGTERM
|
# bail out on CTRL-C or SIGTERM
|
||||||
@ -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
|
||||||
|
@ -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. " % \
|
||||||
|
@ -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,21 +475,22 @@ 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',
|
||||||
|
@ -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])
|
||||||
|
@ -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,8 +154,8 @@ 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 '';
|
||||||
UPDATE metadata SET value='2' WHERE key='db_version';
|
UPDATE metadata SET value='2' WHERE key='db_version';
|
||||||
@ -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,8 +219,9 @@ 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 '
|
||||||
data, executemany=True)
|
'(id,flags,mtime,labels) VALUES (?,?,?,?)',
|
||||||
|
data, executemany=True)
|
||||||
|
|
||||||
|
|
||||||
# Following some pure SQLite functions, where we chose to use
|
# Following some pure SQLite functions, where we chose to use
|
||||||
@ -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):
|
||||||
|
@ -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,9 +257,9 @@ 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)))
|
||||||
|
|
||||||
|
|
||||||
def save_to_tmp_file(self, filename, content):
|
def save_to_tmp_file(self, filename, content):
|
||||||
@ -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']
|
||||||
|
@ -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:
|
||||||
@ -91,7 +91,7 @@ class MappedIMAPFolder(IMAPFolder):
|
|||||||
raise OfflineImapError("Could not find UID for msg '{0}' (f:'{1}'."
|
raise OfflineImapError("Could not find UID for msg '{0}' (f:'{1}'."
|
||||||
" This is usually a bad thing and should be reported on the ma"
|
" This is usually a bad thing and should be reported on the ma"
|
||||||
"iling list.".format(e.args[0], self),
|
"iling list.".format(e.args[0], self),
|
||||||
OfflineImapError.ERROR.MESSAGE), None, exc_info()[2]
|
OfflineImapError.ERROR.MESSAGE), None, exc_info()[2]
|
||||||
|
|
||||||
# Interface from BaseFolder
|
# Interface from BaseFolder
|
||||||
def cachemessagelist(self):
|
def cachemessagelist(self):
|
||||||
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -45,7 +45,8 @@ 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,16 +57,16 @@ 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)
|
||||||
self.tunnel = \
|
self.tunnel = \
|
||||||
self.preauth_tunnel if self.preauth_tunnel \
|
self.preauth_tunnel if self.preauth_tunnel \
|
||||||
else self.transport_tunnel
|
else self.transport_tunnel
|
||||||
|
|
||||||
self.username = \
|
self.username = \
|
||||||
None if self.preauth_tunnel else repos.getuser()
|
None if self.preauth_tunnel else repos.getuser()
|
||||||
self.user_identity = repos.get_remote_identity()
|
self.user_identity = repos.get_remote_identity()
|
||||||
self.authmechs = repos.get_auth_mechanisms()
|
self.authmechs = repos.get_auth_mechanisms()
|
||||||
self.password = None
|
self.password = None
|
||||||
@ -74,7 +75,7 @@ class IMAPServer:
|
|||||||
|
|
||||||
self.usessl = repos.getssl()
|
self.usessl = repos.getssl()
|
||||||
self.hostname = \
|
self.hostname = \
|
||||||
None if self.preauth_tunnel else repos.gethost()
|
None if self.preauth_tunnel else repos.gethost()
|
||||||
self.port = repos.getport()
|
self.port = repos.getport()
|
||||||
if self.port == None:
|
if self.port == None:
|
||||||
self.port = 993 if self.usessl else 143
|
self.port = 993 if self.usessl else 143
|
||||||
@ -110,8 +111,8 @@ class IMAPServer:
|
|||||||
|
|
||||||
# get 1) configured password first 2) fall back to asking via UI
|
# get 1) configured password first 2) fall back to asking via UI
|
||||||
self.password = self.repos.getpassword() or \
|
self.password = self.repos.getpassword() or \
|
||||||
self.ui.getpass(self.repos.getname(), self.config,
|
self.ui.getpass(self.repos.getname(), self.config,
|
||||||
self.passworderror)
|
self.passworderror)
|
||||||
self.passworderror = None
|
self.passworderror = None
|
||||||
return self.password
|
return self.password
|
||||||
|
|
||||||
@ -182,7 +183,7 @@ class IMAPServer:
|
|||||||
response = kerberos.authGSSClientResponse(self.gss_vc)
|
response = kerberos.authGSSClientResponse(self.gss_vc)
|
||||||
rc = kerberos.authGSSClientStep(self.gss_vc, data)
|
rc = kerberos.authGSSClientStep(self.gss_vc, data)
|
||||||
if rc != kerberos.AUTH_GSS_CONTINUE:
|
if rc != kerberos.AUTH_GSS_CONTINUE:
|
||||||
self.gss_step = self.GSS_STATE_WRAP
|
self.gss_step = self.GSS_STATE_WRAP
|
||||||
elif self.gss_step == self.GSS_STATE_WRAP:
|
elif self.gss_step == self.GSS_STATE_WRAP:
|
||||||
rc = kerberos.authGSSClientUnwrap(self.gss_vc, data)
|
rc = kerberos.authGSSClientUnwrap(self.gss_vc, data)
|
||||||
response = kerberos.authGSSClientResponse(self.gss_vc)
|
response = kerberos.authGSSClientResponse(self.gss_vc)
|
||||||
@ -207,8 +208,8 @@ 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])
|
||||||
|
|
||||||
|
|
||||||
## All __authn_* procedures are helpers that do authentication.
|
## All __authn_* procedures are helpers that do authentication.
|
||||||
@ -260,8 +261,8 @@ class IMAPServer:
|
|||||||
# (per RFC 2595)
|
# (per RFC 2595)
|
||||||
if 'LOGINDISABLED' in imapobj.capabilities:
|
if 'LOGINDISABLED' in imapobj.capabilities:
|
||||||
raise OfflineImapError("IMAP LOGIN is "
|
raise OfflineImapError("IMAP LOGIN is "
|
||||||
"disabled by server. Need to use SSL?",
|
"disabled by server. Need to use SSL?",
|
||||||
OfflineImapError.ERROR.REPO)
|
OfflineImapError.ERROR.REPO)
|
||||||
else:
|
else:
|
||||||
self.__loginauth(imapobj)
|
self.__loginauth(imapobj)
|
||||||
return True
|
return True
|
||||||
@ -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(
|
||||||
@ -443,7 +444,7 @@ class IMAPServer:
|
|||||||
self.ui.warn(err)
|
self.ui.warn(err)
|
||||||
raise Exception(err)
|
raise Exception(err)
|
||||||
self.delim, self.root = \
|
self.delim, self.root = \
|
||||||
imaputil.imapsplit(listres[0])[1:]
|
imaputil.imapsplit(listres[0])[1:]
|
||||||
self.delim = imaputil.dequote(self.delim)
|
self.delim = imaputil.dequote(self.delim)
|
||||||
self.root = imaputil.dequote(self.root)
|
self.root = imaputil.dequote(self.root)
|
||||||
|
|
||||||
@ -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,15 +710,15 @@ 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:
|
||||||
self.parent.releaseconnection(imapobj)
|
self.parent.releaseconnection(imapobj)
|
||||||
|
@ -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
|
||||||
|
@ -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')
|
||||||
|
@ -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,8 +196,8 @@ 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(),
|
||||||
status_repo.getsep()))
|
status_repo.getsep()))
|
||||||
@ -218,8 +214,8 @@ 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
|
||||||
# works fine TODO: getfolder() works only because we
|
# works fine TODO: getfolder() works only because we
|
||||||
|
@ -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,10 +101,9 @@ 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
|
||||||
return self._host
|
return self._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,9 +429,8 @@ 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)
|
||||||
|
|
||||||
|
@ -28,13 +28,13 @@ class LocalStatusRepository(BaseRepository):
|
|||||||
# class and root for all backends
|
# class and root for all backends
|
||||||
self.backends = {}
|
self.backends = {}
|
||||||
self.backends['sqlite'] = {
|
self.backends['sqlite'] = {
|
||||||
'class': LocalStatusSQLiteFolder,
|
'class': LocalStatusSQLiteFolder,
|
||||||
'root': os.path.join(account.getaccountmeta(), 'LocalStatus-sqlite')
|
'root': os.path.join(account.getaccountmeta(), 'LocalStatus-sqlite')
|
||||||
}
|
}
|
||||||
|
|
||||||
self.backends['plain'] = {
|
self.backends['plain'] = {
|
||||||
'class': LocalStatusFolder,
|
'class': LocalStatusFolder,
|
||||||
'root': os.path.join(account.getaccountmeta(), 'LocalStatus')
|
'root': os.path.join(account.getaccountmeta(), 'LocalStatus')
|
||||||
}
|
}
|
||||||
|
|
||||||
# Set class and root for the configured backend
|
# Set class and root for the configured backend
|
||||||
@ -54,7 +54,7 @@ class LocalStatusRepository(BaseRepository):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
raise SyntaxWarning("Unknown status_backend '%s' for account '%s'"%
|
raise SyntaxWarning("Unknown status_backend '%s' for account '%s'"%
|
||||||
(backend, self.account.name))
|
(backend, self.account.name))
|
||||||
|
|
||||||
def import_other_backend(self, folder):
|
def import_other_backend(self, folder):
|
||||||
for bk, dic in self.backends.items():
|
for bk, dic in self.backends.items():
|
||||||
@ -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)
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ class MaildirRepository(BaseRepository):
|
|||||||
except OSError as e:
|
except OSError as e:
|
||||||
if e.errno == 17 and os.path.isdir(full_path):
|
if e.errno == 17 and os.path.isdir(full_path):
|
||||||
self.debug("makefolder: '%s' already has subdir %s"%
|
self.debug("makefolder: '%s' already has subdir %s"%
|
||||||
(foldername, subdir))
|
(foldername, subdir))
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
@ -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]
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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,22 +594,24 @@ 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,
|
||||||
offlineimap.__copyright__, color)
|
offlineimap.__copyright__, color)
|
||||||
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]
|
||||||
|
@ -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,10 +90,11 @@ 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')
|
||||||
fh.setFormatter(file_formatter)
|
fh.setFormatter(file_formatter)
|
||||||
self.logger.addHandler(fh)
|
self.logger.addHandler(fh)
|
||||||
# write out more verbose initial info blurb on the log file
|
# write out more verbose initial info blurb on the log file
|
||||||
@ -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.
|
||||||
@ -134,7 +140,7 @@ class UIBase(object):
|
|||||||
One example of such a call might be:
|
One example of such a call might be:
|
||||||
|
|
||||||
ui.error(exc, sys.exc_info()[2], msg="While syncing Folder %s in "
|
ui.error(exc, sys.exc_info()[2], msg="While syncing Folder %s in "
|
||||||
"repo %s")
|
"repo %s")
|
||||||
"""
|
"""
|
||||||
if msg:
|
if msg:
|
||||||
self.logger.error("ERROR: %s\n %s" % (msg, exc))
|
self.logger.error("ERROR: %s\n %s" % (msg, exc))
|
||||||
@ -160,8 +166,8 @@ 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
|
||||||
|
|
||||||
def unregisterthread(self, thr):
|
def unregisterthread(self, thr):
|
||||||
@ -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,49 +240,49 @@ 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):
|
||||||
return ', '.join(["%s[%s]"% \
|
return ', '.join(["%s[%s]"% \
|
||||||
(self.getnicename(x), x.getname()) for x in folder_list])
|
(self.getnicename(x), x.getname()) for x in folder_list])
|
||||||
|
|
||||||
################################################## WARNINGS
|
################################################## WARNINGS
|
||||||
def msgtoreadonly(self, destfolder, uid, content, flags):
|
def msgtoreadonly(self, destfolder, uid, content, flags):
|
||||||
if self.config.has_option('general', 'ignore-readonly') and \
|
if self.config.has_option('general', 'ignore-readonly') and \
|
||||||
self.config.getboolean('general', 'ignore-readonly'):
|
self.config.getboolean('general', 'ignore-readonly'):
|
||||||
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):
|
||||||
if self.config.has_option('general', 'ignore-readonly') and \
|
if self.config.has_option('general', 'ignore-readonly') and \
|
||||||
self.config.getboolean('general', 'ignore-readonly'):
|
self.config.getboolean('general', 'ignore-readonly'):
|
||||||
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):
|
||||||
if self.config.has_option('general', 'ignore-readonly') and \
|
if self.config.has_option('general', 'ignore-readonly') and \
|
||||||
self.config.getboolean('general', 'ignore-readonly'):
|
self.config.getboolean('general', 'ignore-readonly'):
|
||||||
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):
|
||||||
if self.config.has_option('general', 'ignore-readonly') and \
|
if self.config.has_option('general', 'ignore-readonly') and \
|
||||||
self.config.getboolean('general', 'ignore-readonly'):
|
self.config.getboolean('general', 'ignore-readonly'):
|
||||||
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,8 +315,8 @@ 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):
|
||||||
"""Log 'Copying folder structure...'."""
|
"""Log 'Copying folder structure...'."""
|
||||||
@ -321,16 +327,18 @@ 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.getnicename(srcrepos),
|
self.logger.info("Syncing %s: %s -> %s"% (srcfolder,
|
||||||
self.getnicename(destrepos)))
|
self.getnicename(srcrepos),
|
||||||
|
self.getnicename(destrepos)))
|
||||||
|
|
||||||
def skippingfolder(self, folder):
|
def skippingfolder(self, folder):
|
||||||
"""Called when a folder sync operation is started."""
|
"""Called when a folder sync operation is started."""
|
||||||
@ -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))
|
||||||
|
|
||||||
@ -369,8 +377,8 @@ class UIBase(object):
|
|||||||
ds = self.folderlist(destlist)
|
ds = self.folderlist(destlist)
|
||||||
prefix = "[DRYRUN] " if self.dryrun else ""
|
prefix = "[DRYRUN] " if self.dryrun else ""
|
||||||
self.info("{0}Deleting {1} messages ({2}) in {3}".format(
|
self.info("{0}Deleting {1} messages ({2}) in {3}".format(
|
||||||
prefix, len(uidlist),
|
prefix, len(uidlist),
|
||||||
offlineimap.imaputil.uid_sequence(uidlist), ds))
|
offlineimap.imaputil.uid_sequence(uidlist), ds))
|
||||||
|
|
||||||
def addingflags(self, uidlist, flags, dest):
|
def addingflags(self, uidlist, flags, dest):
|
||||||
self.logger.info("Adding flag %s to %d messages on %s" % (
|
self.logger.info("Adding flag %s to %d messages on %s" % (
|
||||||
@ -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,8 +444,8 @@ 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:
|
||||||
syncstr = "" if sync_this else " (disabled)"
|
syncstr = "" if sync_this else " (disabled)"
|
||||||
@ -454,8 +461,8 @@ 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,8 +472,8 @@ 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
|
||||||
|
|
||||||
def delThreadDebugLog(self, thread):
|
def delThreadDebugLog(self, thread):
|
||||||
@ -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):
|
||||||
|
Loading…
Reference in New Issue
Block a user