more style consistency

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

View File

@ -55,7 +55,7 @@ def AccountHashGenerator(customconfig):
class Account(CustomConfig.ConfigHelperMixin):
"""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)
@ -218,11 +224,11 @@ class SyncableAccount(Account):
self._lockfd.close()
raise OfflineImapError("Could not lock account %s. Is another "
"instance using this account?"% self,
OfflineImapError.ERROR.REPO), \
None, exc_info()[2]
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()
@ -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', '')
@ -410,9 +417,9 @@ def syncfolder(account, remotefolder, quick):
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():

View File

@ -48,12 +48,10 @@ 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,
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
@ -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:
@ -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())
@ -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):
@ -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,13 +742,11 @@ 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),
copylist = filter(lambda uid: not statusfolder.uidexists(uid),
self.getmessageuidlist())
num_to_copy = len(copylist)
if num_to_copy and self.repository.account.dryrun:
@ -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

View File

@ -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.

View File

@ -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):
@ -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'.
@ -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,20 +475,21 @@ 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, \
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
@ -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',

View File

@ -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.
@ -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])

View File

@ -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,7 +154,7 @@ 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.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 '';
@ -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,7 +219,8 @@ 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 (?,?,?,?)',
self.__sql_write('INSERT OR REPLACE INTO status '
'(id,flags,mtime,labels) VALUES (?,?,?,?)',
data, executemany=True)
@ -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):

View File

@ -128,7 +128,8 @@ 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)
@ -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,

View File

@ -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:
@ -272,7 +272,7 @@ class MappedIMAPFolder(IMAPFolder):
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
# 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

View File

@ -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

View File

@ -46,6 +46,7 @@ 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,7 +57,7 @@ class IMAPServer:
self.preauth_tunnel = repos.getpreauthtunnel()
self.transport_tunnel = repos.gettransporttunnel()
if self.preauth_tunnel and self.transport_tunnel:
raise OfflineImapError('%s: '% repos + \
raise OfflineImapError('%s: '% repos +
'you must enable precisely one '
'type of tunnel (preauth or transport), '
'not both', OfflineImapError.ERROR.REPO)
@ -716,7 +717,7 @@ class IdleThread(object):
# 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' % \
self.ui.warn('Attempting NOOP on dropped connection %s'%
imapobj.identifier)
self.parent.releaseconnection(imapobj, True)
else:

View File

@ -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,7 +196,7 @@ 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" %\
"Creating folder %s on repository %s"%
(src_name_t, dst_repo))
raise
status_repo.makefolder(src_name_t.replace(dst_repo.getsep(),

View File

@ -101,9 +101,8 @@ 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), \
raise OfflineImapError("remotehosteval option for repository "
"'%s' failed:\n%s"% (self, e), OfflineImapError.ERROR.REPO), \
None, exc_info()[2]
if host:
self._host = 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
@ -431,8 +429,7 @@ 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)),
"Server responded: %s"% (foldername, self, str(result)),
OfflineImapError.ERROR.FOLDER)
finally:
self.imapserver.releaseconnection(imapobj)

View File

@ -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)

View File

@ -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]

View File

@ -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)
@ -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)

View File

@ -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,20 +118,22 @@ 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)
@ -134,10 +141,11 @@ class CursesAccountFrame:
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,25 +155,28 @@ 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):
@ -174,12 +185,13 @@ class CursesAccountFrame:
'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
@ -515,7 +536,8 @@ 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):
@ -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,7 +594,8 @@ 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:
@ -586,7 +610,8 @@ class Blinkenlights(UIBase, CursesUtil):
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]

View File

@ -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,7 +90,8 @@ 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')
@ -107,13 +112,14 @@ class UIBase(object):
def info(self, msg):
"""Display a message."""
self.logger.info(msg)
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.
@ -160,7 +166,7 @@ class UIBase(object):
"'%s')" % (cur_thread.getName(),
self.getthreadaccount(cur_thread), account))
else:
self.debug('thread', "Register new thread '%s' (account '%s')" %\
self.debug('thread', "Register new thread '%s' (account '%s')"%
(cur_thread.getName(), account))
self.threadaccounts[cur_thread] = account
@ -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,7 +240,7 @@ class UIBase(object):
################################################## INPUT
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.")
def folderlist(self, folder_list):
@ -321,13 +327,15 @@ 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)))
@ -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))
@ -408,8 +416,7 @@ class UIBase(object):
try:
if hasattr(repository, 'gethost'): # IMAP
self._msg("Host: %s Port: %s SSL: %s"% (repository.gethost(),
repository.getport(),
repository.getssl()))
repository.getport(), repository.getssl()))
try:
conn = repository.imapserver.acquireconnection()
except OfflineImapError as e:
@ -437,7 +444,7 @@ class UIBase(object):
self._msg("nametrans= %s\n" % nametrans)
folders = repository.getfolders()
foldernames = [(f.name, f.getvisiblename(), f.sync_this) \
foldernames = [(f.name, f.getvisiblename(), f.sync_this)
for f in folders]
folders = []
for name, visiblename, sync_this in foldernames:
@ -454,7 +461,7 @@ 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"%
self.debug(debugtype, "Write mail '%s:%d' with flags %s"%
(folder, uid, repr(flags)))
################################################## Threads
@ -474,9 +481,9 @@ class UIBase(object):
del self.debugmessages[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)
message += u"\n" + self.getThreadDebugLog(thread)
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):