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):
|
||||
"""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
|
||||
:class:`accounts.SyncableAccount` which contains all functions used
|
||||
@ -71,8 +71,9 @@ class Account(CustomConfig.ConfigHelperMixin):
|
||||
:param config: Representing the offlineimap configuration file.
|
||||
:type config: :class:`offlineimap.CustomConfig.CustomConfigParser`
|
||||
|
||||
:param name: A string denoting the name of the Account
|
||||
as configured"""
|
||||
:param name: A (str) string denoting the name of the Account
|
||||
as configured.
|
||||
"""
|
||||
|
||||
self.config = config
|
||||
self.name = name
|
||||
@ -109,7 +110,7 @@ class Account(CustomConfig.ConfigHelperMixin):
|
||||
|
||||
@classmethod
|
||||
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
|
||||
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.
|
||||
"""
|
||||
|
||||
if signum == 1:
|
||||
# resync signal, set config option for all accounts
|
||||
for acctsection in getaccountlist(config):
|
||||
@ -133,7 +135,7 @@ class Account(CustomConfig.ConfigHelperMixin):
|
||||
cls.abort_NOW_signal.set()
|
||||
|
||||
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,
|
||||
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
|
||||
:meth:`set_abort_event` earlier, otherwise 'False'.
|
||||
"""
|
||||
|
||||
skipsleep = self.getconfboolean("skipsleep", 0)
|
||||
if skipsleep:
|
||||
self.config.set(self.getsection(), "skipsleep", '0')
|
||||
@ -149,12 +152,13 @@ class Account(CustomConfig.ConfigHelperMixin):
|
||||
Account.abort_NOW_signal.is_set()
|
||||
|
||||
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,
|
||||
2:request to abort the program,
|
||||
100: if configured to not sleep at all.
|
||||
"""
|
||||
|
||||
if not self.refreshperiod:
|
||||
return 100
|
||||
|
||||
@ -184,7 +188,8 @@ class Account(CustomConfig.ConfigHelperMixin):
|
||||
return 0
|
||||
|
||||
def serverdiagnostics(self):
|
||||
"""Output diagnostics for all involved repositories"""
|
||||
"""Output diagnostics for all involved repositories."""
|
||||
|
||||
remote_repo = Repository(self, 'remote')
|
||||
local_repo = Repository(self, 'local')
|
||||
#status_repo = Repository(self, 'status')
|
||||
@ -194,7 +199,7 @@ class Account(CustomConfig.ConfigHelperMixin):
|
||||
|
||||
|
||||
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
|
||||
functions :meth:`syncrunner`, :meth:`sync`, :meth:`syncfolders`,
|
||||
@ -203,11 +208,12 @@ class SyncableAccount(Account):
|
||||
def __init__(self, *args, **kwargs):
|
||||
Account.__init__(self, *args, **kwargs)
|
||||
self._lockfd = None
|
||||
self._lockfilepath = os.path.join(self.config.getmetadatadir(),
|
||||
"%s.lock" % self)
|
||||
self._lockfilepath = os.path.join(
|
||||
self.config.getmetadatadir(), "%s.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')
|
||||
try:
|
||||
fcntl.lockf(self._lockfd, fcntl.LOCK_EX|fcntl.LOCK_NB)
|
||||
@ -217,19 +223,19 @@ class SyncableAccount(Account):
|
||||
except IOError:
|
||||
self._lockfd.close()
|
||||
raise OfflineImapError("Could not lock account %s. Is another "
|
||||
"instance using this account?" % self,
|
||||
OfflineImapError.ERROR.REPO), \
|
||||
None, exc_info()[2]
|
||||
"instance using this account?"% self,
|
||||
OfflineImapError.ERROR.REPO), None, exc_info()[2]
|
||||
|
||||
def _unlock(self):
|
||||
"""Unlock the account, deleting the lock file"""
|
||||
|
||||
#If we own the lock file, delete it
|
||||
if self._lockfd and not self._lockfd.closed:
|
||||
self._lockfd.close()
|
||||
try:
|
||||
os.unlink(self._lockfilepath)
|
||||
except OSError:
|
||||
pass #Failed to delete for some reason.
|
||||
pass # Failed to delete for some reason.
|
||||
|
||||
def syncrunner(self):
|
||||
self.ui.registerthread(self)
|
||||
@ -265,8 +271,8 @@ class SyncableAccount(Account):
|
||||
raise
|
||||
self.ui.error(e, exc_info()[2])
|
||||
except Exception as e:
|
||||
self.ui.error(e, exc_info()[2], msg="While attempting to sync"
|
||||
" account '%s'"% self)
|
||||
self.ui.error(e, exc_info()[2], msg=
|
||||
"While attempting to sync account '%s'"% self)
|
||||
else:
|
||||
# after success sync, reset the looping counter to 3
|
||||
if self.refreshperiod:
|
||||
@ -278,18 +284,19 @@ class SyncableAccount(Account):
|
||||
looping = 0
|
||||
|
||||
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(
|
||||
remotefolder.getvisiblename().
|
||||
replace(self.remoterepos.getsep(), self.localrepos.getsep()))
|
||||
|
||||
def __sync(self):
|
||||
"""Synchronize the account once, then return
|
||||
"""Synchronize the account once, then return.
|
||||
|
||||
Assumes that `self.remoterepos`, `self.localrepos`, and
|
||||
`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 = []
|
||||
|
||||
hook = self.getconf('presynchook', '')
|
||||
@ -383,12 +390,12 @@ class SyncableAccount(Account):
|
||||
stdin=PIPE, stdout=PIPE, stderr=PIPE,
|
||||
close_fds=True)
|
||||
r = p.communicate()
|
||||
self.ui.callhook("Hook stdout: %s\nHook stderr:%s\n" % r)
|
||||
self.ui.callhook("Hook return code: %d" % p.returncode)
|
||||
self.ui.callhook("Hook stdout: %s\nHook stderr:%s\n"% r)
|
||||
self.ui.callhook("Hook return code: %d"% p.returncode)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
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):
|
||||
"""Synchronizes given remote folder for the specified account.
|
||||
@ -407,12 +414,12 @@ def syncfolder(account, remotefolder, quick):
|
||||
|
||||
# Write the mailboxes
|
||||
mbnames.add(account.name, localfolder.getname(),
|
||||
localrepos.getlocalroot())
|
||||
localrepos.getlocalroot())
|
||||
|
||||
# Load status folder.
|
||||
statusfolder = statusrepos.getfolder(remotefolder.getvisiblename().\
|
||||
replace(remoterepos.getsep(),
|
||||
statusrepos.getsep()))
|
||||
statusfolder = statusrepos.getfolder(remotefolder.getvisiblename().
|
||||
replace(remoterepos.getsep(), statusrepos.getsep()))
|
||||
|
||||
if localfolder.get_uidvalidity() == None:
|
||||
# This is a new folder, so delete the status cache to be
|
||||
# sure we don't have a conflict.
|
||||
@ -423,13 +430,13 @@ def syncfolder(account, remotefolder, quick):
|
||||
statusfolder.cachemessagelist()
|
||||
|
||||
if quick:
|
||||
if not localfolder.quickchanged(statusfolder) \
|
||||
and not remotefolder.quickchanged(statusfolder):
|
||||
if (not localfolder.quickchanged(statusfolder) and
|
||||
not remotefolder.quickchanged(statusfolder)):
|
||||
ui.skippingfolder(remotefolder)
|
||||
localrepos.restore_atime()
|
||||
return
|
||||
|
||||
# Load local folder
|
||||
# Load local folder.
|
||||
ui.syncingfolder(remoterepos, remotefolder, localrepos, localfolder)
|
||||
ui.loadmessagelist(localrepos, localfolder)
|
||||
localfolder.cachemessagelist()
|
||||
@ -488,9 +495,8 @@ def syncfolder(account, remotefolder, quick):
|
||||
ui.error(e, exc_info()[2], msg = "Aborting sync, folder '%s' "
|
||||
"[acc: '%s']" % (localfolder, account))
|
||||
except Exception as e:
|
||||
ui.error(e, msg = "ERROR in syncfolder for %s folder %s: %s" % \
|
||||
(account, remotefolder.getvisiblename(),
|
||||
traceback.format_exc()))
|
||||
ui.error(e, msg = "ERROR in syncfolder for %s folder %s: %s"%
|
||||
(account, remotefolder.getvisiblename(), traceback.format_exc()))
|
||||
finally:
|
||||
for folder in ["statusfolder", "localfolder", "remotefolder"]:
|
||||
if folder in locals():
|
||||
|
@ -48,13 +48,11 @@ class BaseFolder(object):
|
||||
self.visiblename = ''
|
||||
|
||||
self.config = repository.getconfig()
|
||||
utime_from_message_global = \
|
||||
self.config.getdefaultboolean("general",
|
||||
"utime_from_message", False)
|
||||
utime_from_message_global = self.config.getdefaultboolean(
|
||||
"general", "utime_from_message", False)
|
||||
repo = "Repository " + repository.name
|
||||
self._utime_from_message = \
|
||||
self.config.getdefaultboolean(repo,
|
||||
"utime_from_message", utime_from_message_global)
|
||||
self._utime_from_message = self.config.getdefaultboolean(repo,
|
||||
"utime_from_message", utime_from_message_global)
|
||||
|
||||
# Determine if we're running static or dynamic folder filtering
|
||||
# and check filtering status
|
||||
@ -78,16 +76,19 @@ class BaseFolder(object):
|
||||
return self.name
|
||||
|
||||
def __str__(self):
|
||||
# FIMXE: remove calls of this. We have getname().
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def accountname(self):
|
||||
"""Account name as string"""
|
||||
|
||||
return self.repository.accountname
|
||||
|
||||
@property
|
||||
def sync_this(self):
|
||||
"""Should this folder be synced or is it e.g. filtered out?"""
|
||||
|
||||
if not self._dynamic_folderfilter:
|
||||
return self._sync_this
|
||||
else:
|
||||
@ -144,7 +145,7 @@ class BaseFolder(object):
|
||||
if self.name == self.visiblename:
|
||||
return self.name
|
||||
else:
|
||||
return "%s [remote name %s]" % (self.visiblename, self.name)
|
||||
return "%s [remote name %s]"% (self.visiblename, self.name)
|
||||
|
||||
def getrepository(self):
|
||||
"""Returns the repository object that this folder is within."""
|
||||
@ -172,9 +173,9 @@ class BaseFolder(object):
|
||||
|
||||
if not self.name:
|
||||
basename = '.'
|
||||
else: #avoid directory hierarchies and file names such as '/'
|
||||
else: # Avoid directory hierarchies and file names such as '/'.
|
||||
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.
|
||||
basename = re.sub('(^|\/)\.$','\\1dot', basename)
|
||||
return basename
|
||||
@ -196,7 +197,7 @@ class BaseFolder(object):
|
||||
return True
|
||||
|
||||
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(),
|
||||
self.getfolderbasename())
|
||||
@ -228,7 +229,7 @@ class BaseFolder(object):
|
||||
uidfilename = self._getuidfilename()
|
||||
|
||||
with open(uidfilename + ".tmp", "wt") as file:
|
||||
file.write("%d\n" % newval)
|
||||
file.write("%d\n"% newval)
|
||||
os.rename(uidfilename + ".tmp", uidfilename)
|
||||
self._base_saved_uidvalidity = newval
|
||||
|
||||
@ -252,6 +253,7 @@ class BaseFolder(object):
|
||||
|
||||
def getmessagelist(self):
|
||||
"""Gets the current message list.
|
||||
|
||||
You must call cachemessagelist() before calling this function!"""
|
||||
|
||||
raise NotImplementedError
|
||||
@ -272,6 +274,7 @@ class BaseFolder(object):
|
||||
|
||||
def getmessageuidlist(self):
|
||||
"""Gets a list of UIDs.
|
||||
|
||||
You may have to call cachemessagelist() before calling this function!"""
|
||||
|
||||
return self.getmessagelist().keys()
|
||||
@ -377,6 +380,7 @@ class BaseFolder(object):
|
||||
|
||||
def getmessagelabels(self, uid):
|
||||
"""Returns the labels for the specified message."""
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
def savemessagelabels(self, uid, labels, ignorelabels=set(), mtime=0):
|
||||
@ -691,10 +695,10 @@ class BaseFolder(object):
|
||||
# load it up.
|
||||
if dstfolder.storesmessages():
|
||||
message = self.getmessage(uid)
|
||||
#Succeeded? -> IMAP actually assigned a UID. If newid
|
||||
#remained negative, no server was willing to assign us an
|
||||
#UID. If newid is 0, saving succeeded, but we could not
|
||||
#retrieve the new UID. Ignore message in this case.
|
||||
# Succeeded? -> IMAP actually assigned a UID. If newid
|
||||
# remained negative, no server was willing to assign us an
|
||||
# UID. If newid is 0, saving succeeded, but we could not
|
||||
# retrieve the new UID. Ignore message in this case.
|
||||
new_uid = dstfolder.savemessage(uid, message, flags, rtime)
|
||||
if new_uid > 0:
|
||||
if new_uid != uid:
|
||||
@ -728,7 +732,7 @@ class BaseFolder(object):
|
||||
raise #raise on unknown errors, so we can fix those
|
||||
|
||||
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
|
||||
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.
|
||||
- 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 = []
|
||||
|
||||
copylist = filter(lambda uid: not \
|
||||
statusfolder.uidexists(uid),
|
||||
self.getmessageuidlist())
|
||||
copylist = filter(lambda uid: not statusfolder.uidexists(uid),
|
||||
self.getmessageuidlist())
|
||||
num_to_copy = len(copylist)
|
||||
if num_to_copy and self.repository.account.dryrun:
|
||||
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
|
||||
for num, uid in enumerate(copylist):
|
||||
# bail out on CTRL-C or SIGTERM
|
||||
@ -773,7 +775,7 @@ class BaseFolder(object):
|
||||
thread.join()
|
||||
|
||||
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
|
||||
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.
|
||||
"""
|
||||
|
||||
deletelist = filter(lambda uid: uid>=0 \
|
||||
and not self.uidexists(uid),
|
||||
statusfolder.getmessageuidlist())
|
||||
deletelist = filter(lambda uid: uid >= 0 and not
|
||||
self.uidexists(uid), statusfolder.getmessageuidlist())
|
||||
if len(deletelist):
|
||||
self.ui.deletingmessages(deletelist, [dstfolder])
|
||||
if self.repository.account.dryrun:
|
||||
@ -795,7 +796,7 @@ class BaseFolder(object):
|
||||
folder.deletemessages(deletelist)
|
||||
|
||||
def __syncmessagesto_flags(self, dstfolder, statusfolder):
|
||||
"""Pass 3: Flag synchronization
|
||||
"""Pass 3: Flag synchronization.
|
||||
|
||||
Compare flag mismatches in self with those in statusfolder. If
|
||||
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):
|
||||
"""Comparisons work either on string comparing folder names or
|
||||
on the same instance
|
||||
on the same instance.
|
||||
|
||||
MailDirFolder('foo') == 'foo' --> True
|
||||
a = MailDirFolder('foo'); a == b --> True
|
||||
|
@ -22,11 +22,10 @@ from sys import exc_info
|
||||
from offlineimap import imaputil, OfflineImapError
|
||||
from offlineimap import imaplibutil
|
||||
import offlineimap.accounts
|
||||
|
||||
"""Folder implementation to support features of the Gmail IMAP server.
|
||||
"""
|
||||
from .IMAP import IMAPFolder
|
||||
|
||||
"""Folder implementation to support features of the Gmail IMAP server."""
|
||||
|
||||
class GmailFolder(IMAPFolder):
|
||||
"""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)
|
||||
|
||||
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:
|
||||
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))
|
||||
return body
|
||||
|
||||
@ -139,7 +138,7 @@ class GmailFolder(IMAPFolder):
|
||||
# imaplib2 from quoting the sequence.
|
||||
#
|
||||
# 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)')
|
||||
if res_type != 'OK':
|
||||
raise OfflineImapError("FETCHING UIDs in folder [%s]%s failed. " % \
|
||||
|
@ -164,10 +164,10 @@ class IMAPFolder(BaseFolder):
|
||||
# By default examine all messages in this folder
|
||||
msgsToFetch = '1:*'
|
||||
|
||||
maxage = self.config.getdefaultint("Account %s"% self.accountname,
|
||||
"maxage", -1)
|
||||
maxsize = self.config.getdefaultint("Account %s"% self.accountname,
|
||||
"maxsize", -1)
|
||||
maxage = self.config.getdefaultint(
|
||||
"Account %s"% self.accountname, "maxage", -1)
|
||||
maxsize = self.config.getdefaultint(
|
||||
"Account %s"% self.accountname, "maxsize", -1)
|
||||
|
||||
# Build search condition
|
||||
if (maxage != -1) | (maxsize != -1):
|
||||
@ -178,9 +178,9 @@ class IMAPFolder(BaseFolder):
|
||||
oldest_struct = time.gmtime(time.time() - (60*60*24*maxage))
|
||||
if oldest_struct[0] < 1900:
|
||||
raise OfflineImapError("maxage setting led to year %d. "
|
||||
"Abort syncing." % oldest_struct[0],
|
||||
OfflineImapError.ERROR.REPO)
|
||||
search_cond += "SINCE %02d-%s-%d" % (
|
||||
"Abort syncing."% oldest_struct[0],
|
||||
OfflineImapError.ERROR.REPO)
|
||||
search_cond += "SINCE %02d-%s-%d"% (
|
||||
oldest_struct[2],
|
||||
MonthNames[oldest_struct[1]],
|
||||
oldest_struct[0])
|
||||
@ -188,7 +188,7 @@ class IMAPFolder(BaseFolder):
|
||||
if(maxsize != -1):
|
||||
if(maxage != -1): # There are two conditions, add space
|
||||
search_cond += " "
|
||||
search_cond += "SMALLER %d" % maxsize
|
||||
search_cond += "SMALLER %d"% maxsize
|
||||
|
||||
search_cond += ")"
|
||||
|
||||
@ -225,10 +225,8 @@ class IMAPFolder(BaseFolder):
|
||||
msgsToFetch, '(FLAGS UID)')
|
||||
if res_type != 'OK':
|
||||
raise OfflineImapError("FETCHING UIDs in folder [%s]%s failed. "
|
||||
"Server responded '[%s] %s'"% (
|
||||
self.getrepository(), self,
|
||||
res_type, response),
|
||||
OfflineImapError.ERROR.FOLDER)
|
||||
"Server responded '[%s] %s'"% (self.getrepository(), self,
|
||||
res_type, response), OfflineImapError.ERROR.FOLDER)
|
||||
finally:
|
||||
self.imapserver.releaseconnection(imapobj)
|
||||
|
||||
@ -259,7 +257,7 @@ class IMAPFolder(BaseFolder):
|
||||
|
||||
# Interface from BaseFolder
|
||||
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'.
|
||||
|
||||
@ -280,7 +278,7 @@ class IMAPFolder(BaseFolder):
|
||||
data = data[0][1].replace(CRLF, "\n")
|
||||
|
||||
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:
|
||||
dbg_output = data
|
||||
|
||||
@ -331,7 +329,8 @@ class IMAPFolder(BaseFolder):
|
||||
# Now find the UID it got.
|
||||
headervalue = imapobj._quote(headervalue)
|
||||
try:
|
||||
matchinguids = imapobj.uid('search', 'HEADER', headername, headervalue)[1][0]
|
||||
matchinguids = imapobj.uid('search', 'HEADER',
|
||||
headername, headervalue)[1][0]
|
||||
except imapobj.error as err:
|
||||
# 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))
|
||||
@ -396,8 +395,8 @@ class IMAPFolder(BaseFolder):
|
||||
|
||||
result = imapobj.uid('FETCH', bytearray('%d:*'% start), 'rfc822.header')
|
||||
if result[0] != 'OK':
|
||||
raise OfflineImapError('Error fetching mail headers: ' + '. '.join(result[1]),
|
||||
OfflineImapError.ERROR.MESSAGE)
|
||||
raise OfflineImapError('Error fetching mail headers: %s'%
|
||||
'. '.join(result[1]), OfflineImapError.ERROR.MESSAGE)
|
||||
|
||||
result = result[1]
|
||||
|
||||
@ -423,7 +422,8 @@ class IMAPFolder(BaseFolder):
|
||||
def __getmessageinternaldate(self, content, rtime=None):
|
||||
"""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
|
||||
- Date header of email
|
||||
|
||||
@ -475,21 +475,22 @@ class IMAPFolder(BaseFolder):
|
||||
"Server will use local time."% datetuple)
|
||||
return None
|
||||
|
||||
#produce a string representation of datetuple that works as
|
||||
#INTERNALDATE
|
||||
# Produce a string representation of datetuple that works as
|
||||
# INTERNALDATE.
|
||||
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'}
|
||||
|
||||
#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':
|
||||
zone = -time.altzone
|
||||
else:
|
||||
zone = -time.timezone
|
||||
offset_h, offset_m = divmod(zone//60, 60)
|
||||
|
||||
internaldate = '"%02d-%s-%04d %02d:%02d:%02d %+03d%02d"' \
|
||||
% (datetuple.tm_mday, num2mon[datetuple.tm_mon], datetuple.tm_year, \
|
||||
datetuple.tm_hour, datetuple.tm_min, datetuple.tm_sec, offset_h, offset_m)
|
||||
internaldate = '"%02d-%s-%04d %02d:%02d:%02d %+03d%02d"'% \
|
||||
(datetuple.tm_mday, num2mon[datetuple.tm_mon], datetuple.tm_year, \
|
||||
datetuple.tm_hour, datetuple.tm_min, datetuple.tm_sec, offset_h, offset_m)
|
||||
|
||||
return internaldate
|
||||
|
||||
@ -554,7 +555,7 @@ class IMAPFolder(BaseFolder):
|
||||
content = self.addmessageheader(content, CRLF, headername, headervalue)
|
||||
|
||||
if len(content)>200:
|
||||
dbg_output = "%s...%s" % (content[:150], content[-50:])
|
||||
dbg_output = "%s...%s"% (content[:150], content[-50:])
|
||||
else:
|
||||
dbg_output = content
|
||||
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,
|
||||
so you need to ensure that it is never called in a
|
||||
dryrun mode."""
|
||||
|
||||
imapobj = self.imapserver.acquireconnection()
|
||||
try:
|
||||
result = self._store_to_imap(imapobj, str(uid), 'FLAGS',
|
||||
|
@ -21,8 +21,9 @@ import threading
|
||||
|
||||
from .Base import BaseFolder
|
||||
|
||||
|
||||
class LocalStatusFolder(BaseFolder):
|
||||
"""LocalStatus backend implemented as a plain text file"""
|
||||
"""LocalStatus backend implemented as a plain text file."""
|
||||
|
||||
cur_version = 2
|
||||
magicline = "OFFLINEIMAP LocalStatus CACHE DATA - DO NOT MODIFY - FORMAT %d"
|
||||
@ -53,12 +54,10 @@ class LocalStatusFolder(BaseFolder):
|
||||
if not self.isnewfolder():
|
||||
os.unlink(self.filename)
|
||||
|
||||
|
||||
# Interface from BaseFolder
|
||||
def msglist_item_initializer(self, uid):
|
||||
return {'uid': uid, 'flags': set(), 'labels': set(), 'time': 0, 'mtime': 0}
|
||||
|
||||
|
||||
def readstatus_v1(self, fp):
|
||||
"""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]['flags'] = flags
|
||||
|
||||
|
||||
def readstatus(self, fp):
|
||||
"""Read status file in the current format.
|
||||
|
||||
@ -97,7 +95,7 @@ class LocalStatusFolder(BaseFolder):
|
||||
mtime = long(mtime)
|
||||
labels = set([lb.strip() for lb in labels.split(',') if len(lb.strip()) > 0])
|
||||
except ValueError as e:
|
||||
errstr = "Corrupt line '%s' in cache file '%s'" % \
|
||||
errstr = "Corrupt line '%s' in cache file '%s'"% \
|
||||
(line, self.filename)
|
||||
self.ui.warn(errstr)
|
||||
raise ValueError(errstr), None, exc_info()[2]
|
||||
@ -227,7 +225,6 @@ class LocalStatusFolder(BaseFolder):
|
||||
self.messagelist[uid]['flags'] = flags
|
||||
self.save()
|
||||
|
||||
|
||||
def savemessagelabels(self, uid, labels, mtime=None):
|
||||
self.messagelist[uid]['labels'] = labels
|
||||
if mtime: self.messagelist[uid]['mtime'] = mtime
|
||||
@ -263,7 +260,6 @@ class LocalStatusFolder(BaseFolder):
|
||||
def getmessagemtime(self, uid):
|
||||
return self.messagelist[uid]['mtime']
|
||||
|
||||
|
||||
# Interface from BaseFolder
|
||||
def deletemessage(self, uid):
|
||||
self.deletemessages([uid])
|
||||
|
@ -64,7 +64,7 @@ class LocalStatusSQLiteFolder(BaseFolder):
|
||||
|
||||
#Try to establish connection, no need for threadsafety in __init__
|
||||
try:
|
||||
self.connection = sqlite.connect(self.filename, check_same_thread = False)
|
||||
self.connection = sqlite.connect(self.filename, check_same_thread=False)
|
||||
except NameError:
|
||||
# sqlite import had failed
|
||||
raise UserWarning('SQLite backend chosen, but no sqlite python '
|
||||
@ -93,7 +93,6 @@ class LocalStatusSQLiteFolder(BaseFolder):
|
||||
def getfullname(self):
|
||||
return self.filename
|
||||
|
||||
|
||||
# Interface from LocalStatusFolder
|
||||
def isnewfolder(self):
|
||||
return self._newfolder
|
||||
@ -101,7 +100,8 @@ class LocalStatusSQLiteFolder(BaseFolder):
|
||||
|
||||
# Interface from LocalStatusFolder
|
||||
def deletemessagelist(self):
|
||||
"""delete all messages in the db"""
|
||||
"""Delete all messages in the db."""
|
||||
|
||||
self.__sql_write('DELETE FROM status')
|
||||
|
||||
|
||||
@ -114,6 +114,7 @@ class LocalStatusSQLiteFolder(BaseFolder):
|
||||
:param executemany: bool indicating whether we want to
|
||||
perform conn.executemany() or conn.execute().
|
||||
:returns: the Cursor() or raises an Exception"""
|
||||
|
||||
success = False
|
||||
while not success:
|
||||
self._dblock.acquire()
|
||||
@ -153,8 +154,8 @@ class LocalStatusSQLiteFolder(BaseFolder):
|
||||
# Upgrade from database version 1 to version 2
|
||||
# This change adds labels and mtime columns, to be used by Gmail IMAP and Maildir folders.
|
||||
if from_ver <= 1:
|
||||
self.ui._msg('Upgrading LocalStatus cache from version 1 to version 2 for %s:%s' %\
|
||||
(self.repository, self))
|
||||
self.ui._msg('Upgrading LocalStatus cache from version 1 to version 2 for %s:%s'%
|
||||
(self.repository, self))
|
||||
self.connection.executescript("""ALTER TABLE status ADD mtime INTEGER DEFAULT 0;
|
||||
ALTER TABLE status ADD labels VARCHAR(256) DEFAULT '';
|
||||
UPDATE metadata SET value='2' WHERE key='db_version';
|
||||
@ -167,12 +168,10 @@ class LocalStatusSQLiteFolder(BaseFolder):
|
||||
|
||||
|
||||
def __create_db(self):
|
||||
"""
|
||||
Create a new db file.
|
||||
"""Create a new db file.
|
||||
|
||||
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.repository, self))
|
||||
self.connection.executescript("""
|
||||
@ -212,6 +211,7 @@ class LocalStatusSQLiteFolder(BaseFolder):
|
||||
|
||||
def saveall(self):
|
||||
"""Saves the entire messagelist to the database."""
|
||||
|
||||
data = []
|
||||
for uid, msg in self.messagelist.items():
|
||||
mtime = msg['mtime']
|
||||
@ -219,8 +219,9 @@ class LocalStatusSQLiteFolder(BaseFolder):
|
||||
labels = ', '.join(sorted(msg['labels']))
|
||||
data.append((uid, flags, mtime, labels))
|
||||
|
||||
self.__sql_write('INSERT OR REPLACE INTO status (id,flags,mtime,labels) VALUES (?,?,?,?)',
|
||||
data, executemany=True)
|
||||
self.__sql_write('INSERT OR REPLACE INTO status '
|
||||
'(id,flags,mtime,labels) VALUES (?,?,?,?)',
|
||||
data, executemany=True)
|
||||
|
||||
|
||||
# Following some pure SQLite functions, where we chose to use
|
||||
@ -267,14 +268,12 @@ class LocalStatusSQLiteFolder(BaseFolder):
|
||||
|
||||
# Interface from BaseFolder
|
||||
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
|
||||
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:
|
||||
# We cannot assign a uid.
|
||||
return uid
|
||||
@ -352,6 +351,7 @@ class LocalStatusSQLiteFolder(BaseFolder):
|
||||
|
||||
def savemessagesmtimebulk(self, mtimes):
|
||||
"""Saves mtimes from the mtimes dictionary in a single database operation."""
|
||||
|
||||
data = [(mt, uid) for uid, mt in mtimes.items()]
|
||||
self.__sql_write('UPDATE status SET mtime=? WHERE id=?', data, executemany=True)
|
||||
for uid, mt in mtimes.items():
|
||||
@ -376,6 +376,7 @@ class LocalStatusSQLiteFolder(BaseFolder):
|
||||
This function uses sqlites executemany() function which is
|
||||
much faster than iterating through deletemessage() when we have
|
||||
many messages to delete."""
|
||||
|
||||
# Weed out ones not in self.messagelist
|
||||
uidlist = [uid for uid in uidlist if uid in self.messagelist]
|
||||
if not len(uidlist):
|
||||
|
@ -69,7 +69,7 @@ class MaildirFolder(BaseFolder):
|
||||
"Account "+self.accountname, "maildir-windows-compatible", False)
|
||||
self.infosep = '!' if self.wincompatible else ':'
|
||||
"""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()
|
||||
# Everything up to the first comma or colon (or ! if Windows):
|
||||
self.re_prefixmatch = re.compile('([^'+ self.infosep + ',]*)')
|
||||
@ -128,13 +128,14 @@ class MaildirFolder(BaseFolder):
|
||||
detected, we return an empty flags list.
|
||||
|
||||
: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()
|
||||
prefixmatch = self.re_prefixmatch.match(filename)
|
||||
if prefixmatch:
|
||||
prefix = prefixmatch.group(1)
|
||||
folderstr = ',FMD5=%s' % self._foldermd5
|
||||
folderstr = ',FMD5=%s'% self._foldermd5
|
||||
foldermatch = folderstr in filename
|
||||
# If there was no folder MD5 specified, or if it mismatches,
|
||||
# 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
|
||||
(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", None)
|
||||
maxsize = self.config.getdefaultint("Account " + self.accountname,
|
||||
@ -254,9 +257,9 @@ class MaildirFolder(BaseFolder):
|
||||
:returns: String containing unique message filename"""
|
||||
|
||||
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(),
|
||||
uid, self._foldermd5, self.infosep, ''.join(sorted(flags)))
|
||||
uid, self._foldermd5, self.infosep, ''.join(sorted(flags)))
|
||||
|
||||
|
||||
def save_to_tmp_file(self, filename, content):
|
||||
@ -393,7 +396,7 @@ class MaildirFolder(BaseFolder):
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
oldfilename = self.messagelist[uid]['filename']
|
||||
|
@ -78,7 +78,7 @@ class MappedIMAPFolder(IMAPFolder):
|
||||
try:
|
||||
file = open(mapfilename + ".tmp", 'wt')
|
||||
for (key, value) in self.diskl2r.iteritems():
|
||||
file.write("%d:%d\n" % (key, value))
|
||||
file.write("%d:%d\n"% (key, value))
|
||||
file.close()
|
||||
os.rename(mapfilename + '.tmp', mapfilename)
|
||||
finally:
|
||||
@ -91,7 +91,7 @@ class MappedIMAPFolder(IMAPFolder):
|
||||
raise OfflineImapError("Could not find UID for msg '{0}' (f:'{1}'."
|
||||
" This is usually a bad thing and should be reported on the ma"
|
||||
"iling list.".format(e.args[0], self),
|
||||
OfflineImapError.ERROR.MESSAGE), None, exc_info()[2]
|
||||
OfflineImapError.ERROR.MESSAGE), None, exc_info()[2]
|
||||
|
||||
# Interface from BaseFolder
|
||||
def cachemessagelist(self):
|
||||
@ -215,8 +215,8 @@ class MappedIMAPFolder(IMAPFolder):
|
||||
|
||||
newluid = self._mb.savemessage(-1, content, flags, rtime)
|
||||
if newluid < 1:
|
||||
raise ValueError("Backend could not find uid for message, returned "
|
||||
"%s" % newluid)
|
||||
raise ValueError("Backend could not find uid for message, "
|
||||
"returned %s"% newluid)
|
||||
self.maplock.acquire()
|
||||
try:
|
||||
self.diskl2r[newluid] = uid
|
||||
@ -262,8 +262,8 @@ class MappedIMAPFolder(IMAPFolder):
|
||||
UID. The UIDMaps case handles this efficiently by simply
|
||||
changing the mappings file."""
|
||||
if ruid not in self.r2l:
|
||||
raise OfflineImapError("Cannot change unknown Maildir UID %s" % ruid,
|
||||
OfflineImapError.ERROR.MESSAGE)
|
||||
raise OfflineImapError("Cannot change unknown Maildir UID %s"%
|
||||
ruid, OfflineImapError.ERROR.MESSAGE)
|
||||
if ruid == new_ruid: return # sanity check shortcut
|
||||
self.maplock.acquire()
|
||||
try:
|
||||
@ -271,10 +271,10 @@ class MappedIMAPFolder(IMAPFolder):
|
||||
self.l2r[luid] = new_ruid
|
||||
del self.r2l[ruid]
|
||||
self.r2l[new_ruid] = luid
|
||||
#TODO: diskl2r|r2l are a pain to sync and should be done away with
|
||||
#diskl2r only contains positive UIDs, so wrap in ifs
|
||||
if luid>0: self.diskl2r[luid] = new_ruid
|
||||
if ruid>0: del self.diskr2l[ruid]
|
||||
# TODO: diskl2r|r2l are a pain to sync and should be done away with
|
||||
# diskl2r only contains positive UIDs, so wrap in ifs.
|
||||
if luid > 0: self.diskl2r[luid] = new_ruid
|
||||
if ruid > 0: del self.diskr2l[ruid]
|
||||
if new_ruid > 0: self.diskr2l[new_ruid] = luid
|
||||
self._savemaps(dolock = 0)
|
||||
finally:
|
||||
|
@ -39,8 +39,9 @@ class UsefulIMAPMixIn(object):
|
||||
:returns: 'OK' on success, nothing if the folder was already
|
||||
selected or raises an :exc:`OfflineImapError`."""
|
||||
|
||||
if self.__getselectedfolder() == mailbox and self.is_readonly == readonly \
|
||||
and not force:
|
||||
if self.__getselectedfolder() == mailbox and \
|
||||
self.is_readonly == readonly and \
|
||||
not force:
|
||||
# No change; return.
|
||||
return
|
||||
# Wipe out all old responses, to maintain semantics with old imaplib2
|
||||
|
@ -45,7 +45,8 @@ class IMAPServer:
|
||||
|
||||
Public instance variables are: self.:
|
||||
delim The server's folder delimiter. Only valid after acquireconnection()
|
||||
"""
|
||||
"""
|
||||
|
||||
GSS_STATE_STEP = 0
|
||||
GSS_STATE_WRAP = 1
|
||||
def __init__(self, repos):
|
||||
@ -56,16 +57,16 @@ class IMAPServer:
|
||||
self.preauth_tunnel = repos.getpreauthtunnel()
|
||||
self.transport_tunnel = repos.gettransporttunnel()
|
||||
if self.preauth_tunnel and self.transport_tunnel:
|
||||
raise OfflineImapError('%s: '% repos + \
|
||||
'you must enable precisely one '
|
||||
'type of tunnel (preauth or transport), '
|
||||
'not both', OfflineImapError.ERROR.REPO)
|
||||
raise OfflineImapError('%s: '% repos +
|
||||
'you must enable precisely one '
|
||||
'type of tunnel (preauth or transport), '
|
||||
'not both', OfflineImapError.ERROR.REPO)
|
||||
self.tunnel = \
|
||||
self.preauth_tunnel if self.preauth_tunnel \
|
||||
else self.transport_tunnel
|
||||
self.preauth_tunnel if self.preauth_tunnel \
|
||||
else self.transport_tunnel
|
||||
|
||||
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.authmechs = repos.get_auth_mechanisms()
|
||||
self.password = None
|
||||
@ -74,7 +75,7 @@ class IMAPServer:
|
||||
|
||||
self.usessl = repos.getssl()
|
||||
self.hostname = \
|
||||
None if self.preauth_tunnel else repos.gethost()
|
||||
None if self.preauth_tunnel else repos.gethost()
|
||||
self.port = repos.getport()
|
||||
if self.port == None:
|
||||
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
|
||||
self.password = self.repos.getpassword() or \
|
||||
self.ui.getpass(self.repos.getname(), self.config,
|
||||
self.passworderror)
|
||||
self.ui.getpass(self.repos.getname(), self.config,
|
||||
self.passworderror)
|
||||
self.passworderror = None
|
||||
return self.password
|
||||
|
||||
@ -182,7 +183,7 @@ class IMAPServer:
|
||||
response = kerberos.authGSSClientResponse(self.gss_vc)
|
||||
rc = kerberos.authGSSClientStep(self.gss_vc, data)
|
||||
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:
|
||||
rc = kerberos.authGSSClientUnwrap(self.gss_vc, data)
|
||||
response = kerberos.authGSSClientResponse(self.gss_vc)
|
||||
@ -207,8 +208,8 @@ class IMAPServer:
|
||||
imapobj.starttls()
|
||||
except imapobj.error as e:
|
||||
raise OfflineImapError("Failed to start "
|
||||
"TLS connection: %s" % str(e),
|
||||
OfflineImapError.ERROR.REPO, None, exc_info()[2])
|
||||
"TLS connection: %s"% str(e),
|
||||
OfflineImapError.ERROR.REPO, None, exc_info()[2])
|
||||
|
||||
|
||||
## All __authn_* procedures are helpers that do authentication.
|
||||
@ -260,8 +261,8 @@ class IMAPServer:
|
||||
# (per RFC 2595)
|
||||
if 'LOGINDISABLED' in imapobj.capabilities:
|
||||
raise OfflineImapError("IMAP LOGIN is "
|
||||
"disabled by server. Need to use SSL?",
|
||||
OfflineImapError.ERROR.REPO)
|
||||
"disabled by server. Need to use SSL?",
|
||||
OfflineImapError.ERROR.REPO)
|
||||
else:
|
||||
self.__loginauth(imapobj)
|
||||
return True
|
||||
@ -335,7 +336,7 @@ class IMAPServer:
|
||||
exc_stack
|
||||
))
|
||||
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:
|
||||
methods = ", ".join(map(
|
||||
@ -443,7 +444,7 @@ class IMAPServer:
|
||||
self.ui.warn(err)
|
||||
raise Exception(err)
|
||||
self.delim, self.root = \
|
||||
imaputil.imapsplit(listres[0])[1:]
|
||||
imaputil.imapsplit(listres[0])[1:]
|
||||
self.delim = imaputil.dequote(self.delim)
|
||||
self.root = imaputil.dequote(self.root)
|
||||
|
||||
@ -474,7 +475,7 @@ class IMAPServer:
|
||||
if self.port != 993:
|
||||
reason = "Could not connect via SSL to host '%s' and non-s"\
|
||||
"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:
|
||||
reason = "Unknown SSL protocol connecting to host '%s' for "\
|
||||
"repository '%s'. OpenSSL responded:\n%s"\
|
||||
@ -487,7 +488,7 @@ class IMAPServer:
|
||||
reason = "Connection to host '%s:%d' for repository '%s' was "\
|
||||
"refused. Make sure you have the right host and port "\
|
||||
"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]
|
||||
# Could not acquire connection to the remote;
|
||||
# socket.error(last_error) raised
|
||||
@ -709,15 +710,15 @@ class IdleThread(object):
|
||||
imapobj.idle(callback=callback)
|
||||
else:
|
||||
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()
|
||||
self.stop_sig.wait() # self.stop() or IDLE callback are invoked
|
||||
try:
|
||||
# End IDLE mode with noop, imapobj can point to a dropped conn.
|
||||
imapobj.noop()
|
||||
except imapobj.abort:
|
||||
self.ui.warn('Attempting NOOP on dropped connection %s' % \
|
||||
imapobj.identifier)
|
||||
self.ui.warn('Attempting NOOP on dropped connection %s'%
|
||||
imapobj.identifier)
|
||||
self.parent.releaseconnection(imapobj, True)
|
||||
else:
|
||||
self.parent.releaseconnection(imapobj)
|
||||
|
@ -211,7 +211,7 @@ def uid_sequence(uidlist):
|
||||
def getrange(start, end):
|
||||
if start == end:
|
||||
return(str(start))
|
||||
return "%s:%s" % (start, end)
|
||||
return "%s:%s"% (start, end)
|
||||
|
||||
if not len(uidlist): return '' # Empty list, return
|
||||
start, end = None, None
|
||||
|
@ -227,7 +227,7 @@ class OfflineImap:
|
||||
'of %s'% ', '.join(UI_LIST.keys()))
|
||||
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:
|
||||
dryrun = config.set('general', 'dry-run', 'True')
|
||||
config.set_if_not_exists('general', 'dry-run', 'False')
|
||||
|
@ -115,7 +115,6 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
|
||||
@property
|
||||
def readonly(self):
|
||||
"""Is the repository readonly?"""
|
||||
|
||||
return self._readonly
|
||||
|
||||
def getlocaleval(self):
|
||||
@ -123,13 +122,11 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
|
||||
|
||||
def getfolders(self):
|
||||
"""Returns a list of ALL folders on this server."""
|
||||
|
||||
return []
|
||||
|
||||
def forgetfolders(self):
|
||||
"""Forgets the cached list of folders, if any. Useful to run
|
||||
after a sync run."""
|
||||
|
||||
pass
|
||||
|
||||
def getsep(self):
|
||||
@ -150,8 +147,7 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
|
||||
self.getconfboolean('createfolders', True)
|
||||
|
||||
def makefolder(self, foldername):
|
||||
"""Create a new folder"""
|
||||
|
||||
"""Create a new folder."""
|
||||
raise NotImplementedError
|
||||
|
||||
def deletefolder(self, foldername):
|
||||
@ -200,8 +196,8 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
|
||||
dst_haschanged = True # Need to refresh list
|
||||
except OfflineImapError as e:
|
||||
self.ui.error(e, exc_info()[2],
|
||||
"Creating folder %s on repository %s" %\
|
||||
(src_name_t, dst_repo))
|
||||
"Creating folder %s on repository %s"%
|
||||
(src_name_t, dst_repo))
|
||||
raise
|
||||
status_repo.makefolder(src_name_t.replace(dst_repo.getsep(),
|
||||
status_repo.getsep()))
|
||||
@ -218,8 +214,8 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
|
||||
# case don't create it on it:
|
||||
if not self.should_sync_folder(dst_name_t):
|
||||
self.ui.debug('', "Not creating folder '%s' (repository '%s"
|
||||
"') as it would be filtered out on that repository." %
|
||||
(dst_name_t, self))
|
||||
"') as it would be filtered out on that repository."%
|
||||
(dst_name_t, self))
|
||||
continue
|
||||
# get IMAPFolder and see if the reverse nametrans
|
||||
# works fine TODO: getfolder() works only because we
|
||||
|
@ -38,7 +38,7 @@ class IMAPRepository(BaseRepository):
|
||||
self.folders = None
|
||||
if self.getconf('sep', None):
|
||||
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):
|
||||
keepalivetime = self.getkeepalive()
|
||||
@ -85,7 +85,7 @@ class IMAPRepository(BaseRepository):
|
||||
acquireconnection() or it will still be `None`"""
|
||||
assert self.imapserver.delim != None, "'%s' " \
|
||||
"repository called getsep() before the folder separator was " \
|
||||
"queried from the server" % self
|
||||
"queried from the server"% self
|
||||
return self.imapserver.delim
|
||||
|
||||
def gethost(self):
|
||||
@ -101,10 +101,9 @@ class IMAPRepository(BaseRepository):
|
||||
try:
|
||||
host = self.localeval.eval(host)
|
||||
except Exception as e:
|
||||
raise OfflineImapError("remotehosteval option for repository "\
|
||||
"'%s' failed:\n%s" % (self, e),
|
||||
OfflineImapError.ERROR.REPO), \
|
||||
None, exc_info()[2]
|
||||
raise OfflineImapError("remotehosteval option for repository "
|
||||
"'%s' failed:\n%s"% (self, e), OfflineImapError.ERROR.REPO), \
|
||||
None, exc_info()[2]
|
||||
if host:
|
||||
self._host = host
|
||||
return self._host
|
||||
@ -115,9 +114,8 @@ class IMAPRepository(BaseRepository):
|
||||
return self._host
|
||||
|
||||
# no success
|
||||
raise OfflineImapError("No remote host for repository "\
|
||||
"'%s' specified." % self,
|
||||
OfflineImapError.ERROR.REPO)
|
||||
raise OfflineImapError("No remote host for repository "
|
||||
"'%s' specified."% self, OfflineImapError.ERROR.REPO)
|
||||
|
||||
def get_remote_identity(self):
|
||||
"""Remote identity is used for certain SASL mechanisms
|
||||
@ -139,8 +137,8 @@ class IMAPRepository(BaseRepository):
|
||||
|
||||
for m in mechs:
|
||||
if m not in supported:
|
||||
raise OfflineImapError("Repository %s: " % self + \
|
||||
"unknown authentication mechanism '%s'" % m,
|
||||
raise OfflineImapError("Repository %s: "% self + \
|
||||
"unknown authentication mechanism '%s'"% m,
|
||||
OfflineImapError.ERROR.REPO)
|
||||
|
||||
self.ui.debug('imap', "Using authentication mechanisms %s" % mechs)
|
||||
@ -431,9 +429,8 @@ class IMAPRepository(BaseRepository):
|
||||
result = imapobj.create(foldername)
|
||||
if result[0] != 'OK':
|
||||
raise OfflineImapError("Folder '%s'[%s] could not be created. "
|
||||
"Server responded: %s" % \
|
||||
(foldername, self, str(result)),
|
||||
OfflineImapError.ERROR.FOLDER)
|
||||
"Server responded: %s"% (foldername, self, str(result)),
|
||||
OfflineImapError.ERROR.FOLDER)
|
||||
finally:
|
||||
self.imapserver.releaseconnection(imapobj)
|
||||
|
||||
|
@ -28,13 +28,13 @@ class LocalStatusRepository(BaseRepository):
|
||||
# class and root for all backends
|
||||
self.backends = {}
|
||||
self.backends['sqlite'] = {
|
||||
'class': LocalStatusSQLiteFolder,
|
||||
'root': os.path.join(account.getaccountmeta(), 'LocalStatus-sqlite')
|
||||
'class': LocalStatusSQLiteFolder,
|
||||
'root': os.path.join(account.getaccountmeta(), 'LocalStatus-sqlite')
|
||||
}
|
||||
|
||||
self.backends['plain'] = {
|
||||
'class': LocalStatusFolder,
|
||||
'root': os.path.join(account.getaccountmeta(), 'LocalStatus')
|
||||
'class': LocalStatusFolder,
|
||||
'root': os.path.join(account.getaccountmeta(), 'LocalStatus')
|
||||
}
|
||||
|
||||
# Set class and root for the configured backend
|
||||
@ -54,7 +54,7 @@ class LocalStatusRepository(BaseRepository):
|
||||
|
||||
else:
|
||||
raise SyntaxWarning("Unknown status_backend '%s' for account '%s'"%
|
||||
(backend, self.account.name))
|
||||
(backend, self.account.name))
|
||||
|
||||
def import_other_backend(self, folder):
|
||||
for bk, dic in self.backends.items():
|
||||
@ -101,7 +101,7 @@ class LocalStatusRepository(BaseRepository):
|
||||
|
||||
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():
|
||||
self.import_other_backend(folder)
|
||||
|
||||
|
@ -115,7 +115,7 @@ class MaildirRepository(BaseRepository):
|
||||
except OSError as e:
|
||||
if e.errno == 17 and os.path.isdir(full_path):
|
||||
self.debug("makefolder: '%s' already has subdir %s"%
|
||||
(foldername, subdir))
|
||||
(foldername, subdir))
|
||||
else:
|
||||
raise
|
||||
|
||||
|
@ -53,7 +53,7 @@ class Repository(object):
|
||||
'GmailMaildir': GmailMaildirRepository}
|
||||
|
||||
elif reqtype == 'status':
|
||||
# create and return a LocalStatusRepository
|
||||
# create and return a LocalStatusRepository.
|
||||
name = account.getconf('localrepository')
|
||||
return LocalStatusRepository(name, account)
|
||||
|
||||
@ -61,7 +61,7 @@ class Repository(object):
|
||||
errstr = "Repository type %s not supported" % reqtype
|
||||
raise OfflineImapError(errstr, OfflineImapError.ERROR.REPO)
|
||||
|
||||
# Get repository type
|
||||
# Get repository type.
|
||||
config = account.getconfig()
|
||||
try:
|
||||
repostype = config.get('Repository ' + name, 'type').strip()
|
||||
@ -74,8 +74,8 @@ class Repository(object):
|
||||
try:
|
||||
repo = typemap[repostype]
|
||||
except KeyError:
|
||||
errstr = "'%s' repository not supported for '%s' repositories." \
|
||||
% (repostype, reqtype)
|
||||
errstr = "'%s' repository not supported for '%s' repositories."% \
|
||||
(repostype, reqtype)
|
||||
raise OfflineImapError(errstr, OfflineImapError.ERROR.REPO), \
|
||||
None, exc_info()[2]
|
||||
|
||||
|
@ -32,6 +32,7 @@ from offlineimap.ui import getglobalui
|
||||
def semaphorereset(semaphore, originalstate):
|
||||
"""Block until `semaphore` gets back to its original state, ie all acquired
|
||||
resources have been released."""
|
||||
|
||||
for i in range(originalstate):
|
||||
semaphore.acquire()
|
||||
# Now release these.
|
||||
@ -41,6 +42,7 @@ def semaphorereset(semaphore, originalstate):
|
||||
class threadlist:
|
||||
"""Store the list of all threads in the software so it can be used to find out
|
||||
what's running and what's not."""
|
||||
|
||||
def __init__(self):
|
||||
self.lock = Lock()
|
||||
self.list = []
|
||||
@ -98,6 +100,7 @@ def exitnotifymonitorloop(callback):
|
||||
while the other thread is waiting.
|
||||
:type callback: a callable function
|
||||
"""
|
||||
|
||||
global exitthreads
|
||||
do_loop = True
|
||||
while do_loop:
|
||||
@ -116,6 +119,7 @@ def threadexited(thread):
|
||||
"""Called when a thread exits.
|
||||
|
||||
Main thread is aborted when this returns True."""
|
||||
|
||||
ui = getglobalui()
|
||||
if thread.exit_exception:
|
||||
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
|
||||
readable reason of the thread exit."""
|
||||
|
||||
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):
|
||||
super(ExitNotifyThread, self).__init__(*args, **kwargs)
|
||||
@ -167,7 +172,7 @@ class ExitNotifyThread(Thread):
|
||||
except SystemExit:
|
||||
pass
|
||||
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:
|
||||
# Thread exited with Exception, store it
|
||||
tb = traceback.format_exc()
|
||||
@ -179,6 +184,7 @@ class ExitNotifyThread(Thread):
|
||||
def set_exit_exception(self, exc, st=None):
|
||||
"""Sets Exception and stacktrace of a thread, so that other
|
||||
threads can query its exit status"""
|
||||
|
||||
self._exit_exc = exc
|
||||
self._exit_stacktrace = st
|
||||
|
||||
@ -187,16 +193,19 @@ class ExitNotifyThread(Thread):
|
||||
"""Returns the cause of the exit, one of:
|
||||
Exception() -- the thread aborted with this exception
|
||||
None -- normal termination."""
|
||||
|
||||
return self._exit_exc
|
||||
|
||||
@property
|
||||
def exit_stacktrace(self):
|
||||
"""Returns a string representing the stack trace if set"""
|
||||
|
||||
return self._exit_stacktrace
|
||||
|
||||
@classmethod
|
||||
def set_profiledir(cls, directory):
|
||||
"""If set, will output profile information to 'directory'"""
|
||||
|
||||
cls.profiledir = directory
|
||||
|
||||
|
||||
@ -210,6 +219,7 @@ instancelimitedlock = Lock()
|
||||
def initInstanceLimit(instancename, instancemax):
|
||||
"""Initialize the instance-limited thread implementation to permit
|
||||
up to intancemax threads with the given instancename."""
|
||||
|
||||
instancelimitedlock.acquire()
|
||||
if not instancename in instancelimitedsems:
|
||||
instancelimitedsems[instancename] = BoundedSemaphore(instancemax)
|
||||
|
@ -33,17 +33,19 @@ class CursesUtil:
|
||||
# iolock protects access to the
|
||||
self.iolock = RLock()
|
||||
self.tframe_lock = RLock()
|
||||
"""tframe_lock protects the self.threadframes manipulation to
|
||||
only happen from 1 thread"""
|
||||
# tframe_lock protects the self.threadframes manipulation to
|
||||
# only happen from 1 thread.
|
||||
self.colormap = {}
|
||||
"""dict, translating color string to curses color pair number"""
|
||||
|
||||
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])
|
||||
|
||||
def init_colorpairs(self):
|
||||
"""initialize the curses color pairs available"""
|
||||
"""Initialize the curses color pairs available."""
|
||||
|
||||
# set special colors 'gray' and 'banner'
|
||||
self.colormap['white'] = 0 #hardcoded by curses
|
||||
curses.init_pair(1, curses.COLOR_WHITE, curses.COLOR_BLUE)
|
||||
@ -66,24 +68,27 @@ class CursesUtil:
|
||||
curses.init_pair(i, fcol, bcol)
|
||||
|
||||
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
|
||||
from a non-owning thread blocks and waits until it has been
|
||||
unlocked by the owning thread."""
|
||||
|
||||
return self.iolock.acquire(block)
|
||||
|
||||
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
|
||||
counter reaches 0. Only call this method when the calling
|
||||
thread owns the lock. A RuntimeError is raised if this method is
|
||||
called when the lock is unlocked."""
|
||||
|
||||
self.iolock.release()
|
||||
|
||||
def exec_locked(self, target, *args, **kwargs):
|
||||
"""Perform an operation with full locking."""
|
||||
|
||||
self.lock()
|
||||
try:
|
||||
target(*args, **kwargs)
|
||||
@ -113,31 +118,34 @@ class CursesAccountFrame:
|
||||
def __init__(self, ui, account):
|
||||
"""
|
||||
:param account: An Account() or None (for eg SyncrunnerThread)"""
|
||||
|
||||
self.children = []
|
||||
self.account = account if account else '*Control'
|
||||
self.ui = ui
|
||||
self.window = None
|
||||
"""Curses window associated with this acc"""
|
||||
# Curses window associated with this acc.
|
||||
self.acc_num = None
|
||||
"""Account number (& hotkey) associated with this acc"""
|
||||
# Account number (& hotkey) associated with this acc.
|
||||
self.location = 0
|
||||
"""length of the account prefix string"""
|
||||
# length of the account prefix string
|
||||
|
||||
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."""
|
||||
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.location = len(accstr)
|
||||
|
||||
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 acc_num: int denoting the hotkey associated with this account."""
|
||||
|
||||
self.window = curses_win
|
||||
self.acc_num = acc_num
|
||||
self.drawleadstr()
|
||||
@ -147,39 +155,43 @@ class CursesAccountFrame:
|
||||
self.location += 1
|
||||
|
||||
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"""
|
||||
|
||||
tf = CursesThreadFrame(self.ui, self.window, self.location, 0)
|
||||
self.location += 1
|
||||
self.children.append(tf)
|
||||
return tf
|
||||
|
||||
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"""
|
||||
|
||||
self.drawleadstr(remainingsecs)
|
||||
self.ui.exec_locked(self.window.refresh)
|
||||
time.sleep(sleepsecs)
|
||||
return self.account.get_abort_event()
|
||||
|
||||
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
|
||||
# skipsleep pref
|
||||
if isinstance(self.account, offlineimap.accounts.Account):
|
||||
self.ui.info("Requested synchronization for acc: %s" % self.account)
|
||||
self.account.config.set('Account %s' % self.account.name,
|
||||
'skipsleep', '1')
|
||||
self.ui.info("Requested synchronization for acc: %s"% self.account)
|
||||
self.account.config.set('Account %s'% self.account.name,
|
||||
'skipsleep', '1')
|
||||
|
||||
class CursesThreadFrame:
|
||||
"""
|
||||
curses_color: current color pair for logging"""
|
||||
"""curses_color: current color pair for logging."""
|
||||
|
||||
def __init__(self, ui, acc_win, x, y):
|
||||
"""
|
||||
:param ui: is a Blinkenlights() instance
|
||||
:param acc_win: curses Account window"""
|
||||
|
||||
self.ui = ui
|
||||
self.window = acc_win
|
||||
self.x = x
|
||||
@ -188,7 +200,10 @@ class CursesThreadFrame:
|
||||
|
||||
def setcolor(self, color, modifier=0):
|
||||
"""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.colorname = color
|
||||
self.display()
|
||||
@ -201,7 +216,8 @@ class CursesThreadFrame:
|
||||
self.ui.exec_locked(locked_display)
|
||||
|
||||
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.y = y
|
||||
self.x = x
|
||||
@ -213,6 +229,7 @@ class CursesThreadFrame:
|
||||
|
||||
class InputHandler(ExitNotifyThread):
|
||||
"""Listens for input via the curses interfaces"""
|
||||
|
||||
#TODO, we need to use the ugly exitnotifythread (rather than simply
|
||||
#threading.Thread here, so exiting this thread via the callback
|
||||
#handler, kills off all parents too. Otherwise, they would simply
|
||||
@ -222,17 +239,18 @@ class InputHandler(ExitNotifyThread):
|
||||
self.char_handler = None
|
||||
self.ui = ui
|
||||
self.enabled = Event()
|
||||
"""We will only parse input if we are enabled"""
|
||||
# We will only parse input if we are enabled.
|
||||
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
|
||||
|
||||
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()
|
||||
msecs, releasing the inputlock.
|
||||
:returns: char or None if disabled while in here"""
|
||||
|
||||
self.enabled.wait()
|
||||
while self.enabled.is_set():
|
||||
with self.inputlock:
|
||||
@ -247,13 +265,14 @@ class InputHandler(ExitNotifyThread):
|
||||
#curses.ungetch(char)
|
||||
|
||||
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
|
||||
include the curses.KEY_RESIZE key.
|
||||
|
||||
callback is a function taking a single arg -- the char pressed.
|
||||
If callback is None, input will be ignored."""
|
||||
|
||||
with self.inputlock:
|
||||
self.char_handler = callback
|
||||
# start or stop the parsing of things
|
||||
@ -266,13 +285,14 @@ class InputHandler(ExitNotifyThread):
|
||||
"""Call this method when you want exclusive input control.
|
||||
|
||||
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.inputlock.acquire()
|
||||
|
||||
def input_release(self):
|
||||
"""Call this method when you are done getting input."""
|
||||
|
||||
self.inputlock.release()
|
||||
self.enabled.set()
|
||||
|
||||
@ -301,7 +321,7 @@ class CursesLogHandler(logging.StreamHandler):
|
||||
self.ui.stdscr.refresh()
|
||||
|
||||
class Blinkenlights(UIBase, CursesUtil):
|
||||
"""Curses-cased fancy UI
|
||||
"""Curses-cased fancy UI.
|
||||
|
||||
Notable instance variables self. ....:
|
||||
|
||||
@ -319,7 +339,7 @@ class Blinkenlights(UIBase, CursesUtil):
|
||||
|
||||
################################################## UTILS
|
||||
def setup_consolehandler(self):
|
||||
"""Backend specific console handler
|
||||
"""Backend specific console handler.
|
||||
|
||||
Sets up things and adds them to self.logger.
|
||||
:returns: The logging.Handler() for console output"""
|
||||
@ -337,7 +357,7 @@ class Blinkenlights(UIBase, CursesUtil):
|
||||
return ch
|
||||
|
||||
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.
|
||||
if not sys.stdout.isatty() and sys.stdin.isatty():
|
||||
@ -393,7 +413,7 @@ class Blinkenlights(UIBase, CursesUtil):
|
||||
self.info(offlineimap.banner)
|
||||
|
||||
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')
|
||||
super(Blinkenlights, self).acct(*args)
|
||||
@ -458,7 +478,8 @@ class Blinkenlights(UIBase, CursesUtil):
|
||||
super(Blinkenlights, self).threadExited(thread)
|
||||
|
||||
def gettf(self):
|
||||
"""Return the ThreadFrame() of the current thread"""
|
||||
"""Return the ThreadFrame() of the current thread."""
|
||||
|
||||
cur_thread = currentThread()
|
||||
acc = self.getthreadaccount() #Account() or None
|
||||
|
||||
@ -504,7 +525,7 @@ class Blinkenlights(UIBase, CursesUtil):
|
||||
|
||||
def sleep(self, sleepsecs, account):
|
||||
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)
|
||||
|
||||
def sleeping(self, sleepsecs, remainingsecs):
|
||||
@ -515,13 +536,14 @@ class Blinkenlights(UIBase, CursesUtil):
|
||||
return accframe.sleeping(sleepsecs, remainingsecs)
|
||||
|
||||
def resizeterm(self):
|
||||
"""Resize the current windows"""
|
||||
"""Resize the current windows."""
|
||||
|
||||
self.exec_locked(self.setupwindows, True)
|
||||
|
||||
def mainException(self):
|
||||
UIBase.mainException(self)
|
||||
|
||||
def getpass(self, accountname, config, errmsg = None):
|
||||
def getpass(self, accountname, config, errmsg=None):
|
||||
# disable the hotkeys inputhandler
|
||||
self.inputhandler.input_acquire()
|
||||
|
||||
@ -540,10 +562,11 @@ class Blinkenlights(UIBase, CursesUtil):
|
||||
return password
|
||||
|
||||
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
|
||||
function should be invoked with CursesUtils.locked()."""
|
||||
|
||||
self.height, self.width = self.stdscr.getmaxyx()
|
||||
self.logheight = self.height - len(self.accframes) - 1
|
||||
if resize:
|
||||
@ -571,22 +594,24 @@ class Blinkenlights(UIBase, CursesUtil):
|
||||
curses.doupdate()
|
||||
|
||||
def draw_bannerwin(self):
|
||||
"""Draw the top-line banner line"""
|
||||
"""Draw the top-line banner line."""
|
||||
|
||||
if curses.has_colors():
|
||||
color = curses.A_BOLD | self.curses_colorpair('banner')
|
||||
else:
|
||||
color = curses.A_REVERSE
|
||||
self.bannerwin.clear() # Delete old content (eg before resizes)
|
||||
self.bannerwin.bkgd(' ', color) # Fill background with that color
|
||||
string = "%s %s" % (offlineimap.__productname__,
|
||||
offlineimap.__bigversion__)
|
||||
string = "%s %s"% (offlineimap.__productname__,
|
||||
offlineimap.__bigversion__)
|
||||
self.bannerwin.addstr(0, 0, string, color)
|
||||
self.bannerwin.addstr(0, self.width -len(offlineimap.__copyright__) -1,
|
||||
offlineimap.__copyright__, color)
|
||||
self.bannerwin.noutrefresh()
|
||||
|
||||
def draw_logwin(self):
|
||||
"""(Re)draw the current logwindow"""
|
||||
"""(Re)draw the current logwindow."""
|
||||
|
||||
if curses.has_colors():
|
||||
color = curses.color_pair(0) #default colors
|
||||
else:
|
||||
@ -596,9 +621,10 @@ class Blinkenlights(UIBase, CursesUtil):
|
||||
self.logwin.bkgd(' ', color)
|
||||
|
||||
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`."""
|
||||
|
||||
with self.aflock:
|
||||
# 1) Return existing or 2) create a new CursesAccountFrame.
|
||||
if acc_name in self.accframes: return self.accframes[acc_name]
|
||||
|
@ -36,14 +36,18 @@ debugtypes = {'':'Other offlineimap related sync messages',
|
||||
|
||||
globalui = None
|
||||
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
|
||||
globalui = newui
|
||||
|
||||
def getglobalui():
|
||||
"""Return the current ui object"""
|
||||
"""Return the current ui object."""
|
||||
|
||||
global globalui
|
||||
return globalui
|
||||
|
||||
|
||||
class UIBase(object):
|
||||
def __init__(self, config, loglevel=logging.INFO):
|
||||
self.config = config
|
||||
@ -65,11 +69,11 @@ class UIBase(object):
|
||||
self.logger = logging.getLogger('OfflineImap')
|
||||
self.logger.setLevel(loglevel)
|
||||
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
|
||||
def setup_consolehandler(self):
|
||||
"""Backend specific console handler
|
||||
"""Backend specific console handler.
|
||||
|
||||
Sets up things and adds them to self.logger.
|
||||
:returns: The logging.Handler() for console output"""
|
||||
@ -86,10 +90,11 @@ class UIBase(object):
|
||||
return ch
|
||||
|
||||
def setlogfile(self, logfile):
|
||||
"""Create file handler which logs to file"""
|
||||
"""Create file handler which logs to file."""
|
||||
|
||||
fh = logging.FileHandler(logfile, 'at')
|
||||
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)
|
||||
self.logger.addHandler(fh)
|
||||
# write out more verbose initial info blurb on the log file
|
||||
@ -107,13 +112,14 @@ class UIBase(object):
|
||||
|
||||
def info(self, msg):
|
||||
"""Display a message."""
|
||||
|
||||
self.logger.info(msg)
|
||||
|
||||
def warn(self, msg, minor = 0):
|
||||
def warn(self, msg, minor=0):
|
||||
self.logger.warning(msg)
|
||||
|
||||
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
|
||||
error "msg", detailing at what point the error occurred.
|
||||
@ -134,7 +140,7 @@ class UIBase(object):
|
||||
One example of such a call might be:
|
||||
|
||||
ui.error(exc, sys.exc_info()[2], msg="While syncing Folder %s in "
|
||||
"repo %s")
|
||||
"repo %s")
|
||||
"""
|
||||
if msg:
|
||||
self.logger.error("ERROR: %s\n %s" % (msg, exc))
|
||||
@ -160,8 +166,8 @@ class UIBase(object):
|
||||
"'%s')" % (cur_thread.getName(),
|
||||
self.getthreadaccount(cur_thread), account))
|
||||
else:
|
||||
self.debug('thread', "Register new thread '%s' (account '%s')" %\
|
||||
(cur_thread.getName(), account))
|
||||
self.debug('thread', "Register new thread '%s' (account '%s')"%
|
||||
(cur_thread.getName(), account))
|
||||
self.threadaccounts[cur_thread] = account
|
||||
|
||||
def unregisterthread(self, thr):
|
||||
@ -216,7 +222,7 @@ class UIBase(object):
|
||||
self.warn("Invalid debug type: %s" % debugtype)
|
||||
|
||||
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...)"""
|
||||
|
||||
@ -234,49 +240,49 @@ class UIBase(object):
|
||||
################################################## INPUT
|
||||
|
||||
def getpass(self, accountname, config, errmsg = None):
|
||||
raise NotImplementedError("Prompting for a password is not supported"\
|
||||
" in this UI backend.")
|
||||
raise NotImplementedError("Prompting for a password is not supported"
|
||||
" in this UI backend.")
|
||||
|
||||
def folderlist(self, folder_list):
|
||||
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
|
||||
def msgtoreadonly(self, destfolder, uid, content, flags):
|
||||
if self.config.has_option('general', 'ignore-readonly') and \
|
||||
self.config.getboolean('general', 'ignore-readonly'):
|
||||
self.config.getboolean('general', 'ignore-readonly'):
|
||||
return
|
||||
self.warn("Attempted to synchronize message %d to folder %s[%s], "
|
||||
"but that folder is read-only. The message will not be "
|
||||
"copied to that folder." % (
|
||||
uid, self.getnicename(destfolder), destfolder))
|
||||
"but that folder is read-only. The message will not be "
|
||||
"copied to that folder."% (
|
||||
uid, self.getnicename(destfolder), destfolder))
|
||||
|
||||
def flagstoreadonly(self, destfolder, uidlist, flags):
|
||||
if self.config.has_option('general', 'ignore-readonly') and \
|
||||
self.config.getboolean('general', 'ignore-readonly'):
|
||||
return
|
||||
self.warn("Attempted to modify flags for messages %s in folder %s[%s], "
|
||||
"but that folder is read-only. No flags have been modified "
|
||||
"for that message." % (
|
||||
str(uidlist), self.getnicename(destfolder), destfolder))
|
||||
"but that folder is read-only. No flags have been modified "
|
||||
"for that message."% (
|
||||
str(uidlist), self.getnicename(destfolder), destfolder))
|
||||
|
||||
def labelstoreadonly(self, destfolder, uidlist, labels):
|
||||
if self.config.has_option('general', 'ignore-readonly') and \
|
||||
self.config.getboolean('general', 'ignore-readonly'):
|
||||
return
|
||||
self.warn("Attempted to modify labels for messages %s in folder %s[%s], "
|
||||
"but that folder is read-only. No labels have been modified "
|
||||
"for that message." % (
|
||||
str(uidlist), self.getnicename(destfolder), destfolder))
|
||||
"but that folder is read-only. No labels have been modified "
|
||||
"for that message."% (
|
||||
str(uidlist), self.getnicename(destfolder), destfolder))
|
||||
|
||||
def deletereadonly(self, destfolder, uidlist):
|
||||
if self.config.has_option('general', 'ignore-readonly') and \
|
||||
self.config.getboolean('general', 'ignore-readonly'):
|
||||
return
|
||||
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." % (str(uidlist), self.getnicename(destfolder),
|
||||
destfolder))
|
||||
"folder is read-only. No messages have been deleted in that "
|
||||
"folder."% (str(uidlist), self.getnicename(destfolder),
|
||||
destfolder))
|
||||
|
||||
################################################## MESSAGES
|
||||
|
||||
@ -293,7 +299,7 @@ class UIBase(object):
|
||||
if not self.logger.isEnabledFor(logging.INFO): return
|
||||
displaystr = ''
|
||||
hostname = hostname if hostname else ''
|
||||
port = "%s" % port if port else ''
|
||||
port = "%s"% port if port else ''
|
||||
if hostname:
|
||||
displaystr = ' to %s:%s' % (hostname, port)
|
||||
self.logger.info("Establishing connection%s" % displaystr)
|
||||
@ -309,8 +315,8 @@ class UIBase(object):
|
||||
|
||||
sec = time.time() - self.acct_startimes[account]
|
||||
del self.acct_startimes[account]
|
||||
self.logger.info("*** Finished account '%s' in %d:%02d" %
|
||||
(account, sec // 60, sec % 60))
|
||||
self.logger.info("*** Finished account '%s' in %d:%02d"%
|
||||
(account, sec // 60, sec % 60))
|
||||
|
||||
def syncfolders(self, src_repo, dst_repo):
|
||||
"""Log 'Copying folder structure...'."""
|
||||
@ -321,16 +327,18 @@ class UIBase(object):
|
||||
|
||||
############################## Folder syncing
|
||||
def makefolder(self, repo, foldername):
|
||||
"""Called when a folder is created"""
|
||||
"""Called when a folder is created."""
|
||||
|
||||
prefix = "[DRYRUN] " if self.dryrun else ""
|
||||
self.info("{0}Creating folder {1}[{2}]".format(
|
||||
prefix, foldername, repo))
|
||||
self.info(("{0}Creating folder {1}[{2}]".format(
|
||||
prefix, foldername, repo)))
|
||||
|
||||
def syncingfolder(self, srcrepos, srcfolder, destrepos, destfolder):
|
||||
"""Called when a folder sync operation is started."""
|
||||
self.logger.info("Syncing %s: %s -> %s" % (srcfolder,
|
||||
self.getnicename(srcrepos),
|
||||
self.getnicename(destrepos)))
|
||||
|
||||
self.logger.info("Syncing %s: %s -> %s"% (srcfolder,
|
||||
self.getnicename(srcrepos),
|
||||
self.getnicename(destrepos)))
|
||||
|
||||
def skippingfolder(self, folder):
|
||||
"""Called when a folder sync operation is started."""
|
||||
@ -344,7 +352,7 @@ class UIBase(object):
|
||||
folder.get_saveduidvalidity(), folder.get_uidvalidity()))
|
||||
|
||||
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),
|
||||
folder))
|
||||
|
||||
@ -369,8 +377,8 @@ class UIBase(object):
|
||||
ds = self.folderlist(destlist)
|
||||
prefix = "[DRYRUN] " if self.dryrun else ""
|
||||
self.info("{0}Deleting {1} messages ({2}) in {3}".format(
|
||||
prefix, len(uidlist),
|
||||
offlineimap.imaputil.uid_sequence(uidlist), ds))
|
||||
prefix, len(uidlist),
|
||||
offlineimap.imaputil.uid_sequence(uidlist), ds))
|
||||
|
||||
def addingflags(self, uidlist, flags, dest):
|
||||
self.logger.info("Adding flag %s to %d messages on %s" % (
|
||||
@ -407,9 +415,8 @@ class UIBase(object):
|
||||
self.getnicename(repository)))
|
||||
try:
|
||||
if hasattr(repository, 'gethost'): # IMAP
|
||||
self._msg("Host: %s Port: %s SSL: %s" % (repository.gethost(),
|
||||
repository.getport(),
|
||||
repository.getssl()))
|
||||
self._msg("Host: %s Port: %s SSL: %s"% (repository.gethost(),
|
||||
repository.getport(), repository.getssl()))
|
||||
try:
|
||||
conn = repository.imapserver.acquireconnection()
|
||||
except OfflineImapError as e:
|
||||
@ -437,8 +444,8 @@ class UIBase(object):
|
||||
self._msg("nametrans= %s\n" % nametrans)
|
||||
|
||||
folders = repository.getfolders()
|
||||
foldernames = [(f.name, f.getvisiblename(), f.sync_this) \
|
||||
for f in folders]
|
||||
foldernames = [(f.name, f.getvisiblename(), f.sync_this)
|
||||
for f in folders]
|
||||
folders = []
|
||||
for name, visiblename, sync_this in foldernames:
|
||||
syncstr = "" if sync_this else " (disabled)"
|
||||
@ -454,8 +461,8 @@ class UIBase(object):
|
||||
def savemessage(self, debugtype, uid, flags, folder):
|
||||
"""Output a log line stating that we save a msg."""
|
||||
|
||||
self.debug(debugtype, u"Write mail '%s:%d' with flags %s"%
|
||||
(folder, uid, repr(flags)))
|
||||
self.debug(debugtype, "Write mail '%s:%d' with flags %s"%
|
||||
(folder, uid, repr(flags)))
|
||||
|
||||
################################################## Threads
|
||||
|
||||
@ -465,8 +472,8 @@ class UIBase(object):
|
||||
% (len(self.debugmessages[thread]), thread.getName())
|
||||
message += "\n".join(self.debugmessages[thread])
|
||||
else:
|
||||
message = "\nNo debug messages were logged for %s." % \
|
||||
thread.getName()
|
||||
message = "\nNo debug messages were logged for %s."% \
|
||||
thread.getName()
|
||||
return message
|
||||
|
||||
def delThreadDebugLog(self, thread):
|
||||
@ -474,9 +481,9 @@ class UIBase(object):
|
||||
del self.debugmessages[thread]
|
||||
|
||||
def getThreadExceptionString(self, thread):
|
||||
message = u"Thread '%s' terminated with exception:\n%s"% \
|
||||
(thread.getName(), thread.exit_stacktrace)
|
||||
message += u"\n" + self.getThreadDebugLog(thread)
|
||||
message = "Thread '%s' terminated with exception:\n%s"% \
|
||||
(thread.getName(), thread.exit_stacktrace)
|
||||
message += "\n" + self.getThreadDebugLog(thread)
|
||||
return message
|
||||
|
||||
def threadException(self, thread):
|
||||
@ -492,21 +499,21 @@ class UIBase(object):
|
||||
|
||||
#print any exceptions that have occurred over the run
|
||||
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():
|
||||
msg, exc, exc_traceback = self.exc_queue.get()
|
||||
if msg:
|
||||
self.warn(u"ERROR: %s\n %s"% (msg, exc))
|
||||
self.warn("ERROR: %s\n %s"% (msg, exc))
|
||||
else:
|
||||
self.warn(u"ERROR: %s"% (exc))
|
||||
self.warn("ERROR: %s"% (exc))
|
||||
if exc_traceback:
|
||||
self.warn(u"\nTraceback:\n%s"% "".join(
|
||||
self.warn("\nTraceback:\n%s"% "".join(
|
||||
traceback.format_tb(exc_traceback)))
|
||||
|
||||
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:
|
||||
self.warn(u'%s\n' % errormsg)
|
||||
self.warn('%s\n'% errormsg)
|
||||
sys.exit(exitstatus)
|
||||
|
||||
def threadExited(self, thread):
|
||||
|
Loading…
Reference in New Issue
Block a user