diff --git a/offlineimap/accounts.py b/offlineimap/accounts.py index 190f6de32..d9c9b88db 100644 --- a/offlineimap/accounts.py +++ b/offlineimap/accounts.py @@ -321,18 +321,18 @@ def __sync(self): if not remotefolder.sync_this: self.ui.debug('', "Not syncing filtered folder '%s'" - "[%s]" % (remotefolder, remoterepos)) + "[%s]"% (remotefolder, remoterepos)) continue # Ignore filtered folder localfolder = self.get_local_folder(remotefolder) if not localfolder.sync_this: self.ui.debug('', "Not syncing filtered folder '%s'" - "[%s]" % (localfolder, localfolder.repository)) + "[%s]"% (localfolder, localfolder.repository)) continue # Ignore filtered folder if not globals.options.singlethreading: thread = InstanceLimitedThread(\ instancename = 'FOLDER_' + self.remoterepos.getname(), target = syncfolder, - name = "Folder %s [acc: %s]" % (remotefolder.getexplainedname(), self), + name = "Folder %s [acc: %s]"% (remotefolder.getexplainedname(), self), args = (self, remotefolder, quick)) thread.start() folderthreads.append(thread) @@ -385,6 +385,7 @@ def syncfolder(account, remotefolder, quick): """Synchronizes given remote folder for the specified account. Filtered folders on the remote side will not invoke this function.""" + remoterepos = account.remoterepos localrepos = account.localrepos statusrepos = account.statusrepos diff --git a/offlineimap/emailutil.py b/offlineimap/emailutil.py index 4aa34a84b..0c732e2a3 100644 --- a/offlineimap/emailutil.py +++ b/offlineimap/emailutil.py @@ -19,13 +19,12 @@ from email.Parser import Parser as MailParser def get_message_date(content, header='Date'): - """ - Parses mail and returns resulting timestamp. + """Parses mail and returns resulting timestamp. :param header: the header to extract date from; :returns: timestamp or `None` in the case of failure. - """ + message = MailParser().parsestr(content, True) dateheader = message.get(header) # parsedate_tz returns a 10-tuple that can be passed to mktime_tz diff --git a/offlineimap/folder/Base.py b/offlineimap/folder/Base.py index 2e1b95f2d..14262e3ad 100644 --- a/offlineimap/folder/Base.py +++ b/offlineimap/folder/Base.py @@ -15,14 +15,15 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +import os.path +import re +from sys import exc_info + from offlineimap import threadutil, emailutil from offlineimap import globals from offlineimap.ui import getglobalui from offlineimap.error import OfflineImapError import offlineimap.accounts -import os.path -import re -from sys import exc_info class BaseFolder(object): @@ -31,6 +32,7 @@ def __init__(self, name, repository): :para name: Path & name of folder minus root or reference :para repository: Repository() in which the folder is. """ + self.ui = getglobalui() # Save original name for folderfilter operations self.ffilter_name = name @@ -56,15 +58,15 @@ def __init__(self, name, repository): # Determine if we're running static or dynamic folder filtering # and check filtering status - self._dynamic_folderfilter = \ - self.config.getdefaultboolean(repo, "dynamic_folderfilter", False) + self._dynamic_folderfilter = self.config.getdefaultboolean( + repo, "dynamic_folderfilter", False) self._sync_this = repository.should_sync_folder(self.ffilter_name) if self._dynamic_folderfilter: - self.ui.debug('', "Running dynamic folder filtering on '%s'[%s]" \ - % (self.ffilter_name, repository)) + self.ui.debug('', "Running dynamic folder filtering on '%s'[%s]"% + (self.ffilter_name, repository)) elif not self._sync_this: - self.ui.debug('', "Filtering out '%s'[%s] due to folderfilter" \ - % (self.ffilter_name, repository)) + self.ui.debug('', "Filtering out '%s'[%s] due to folderfilter"% + (self.ffilter_name, repository)) # Passes for syncmessagesto self.syncmessagesto_passes = [('copying messages' , self.__syncmessagesto_copy), @@ -115,17 +117,20 @@ def quickchanged(self, statusfolder): :param statusfolder: keeps track of the last known folder state. """ + return True def getcopyinstancelimit(self): """For threading folders, returns the instancelimitname for InstanceLimitedThreads.""" + raise NotImplementedError def storesmessages(self): """Should be true for any backend that actually saves message bodies. (Almost all of them). False for the LocalStatus backend. Saves us from having to slurp up messages just for localstatus purposes.""" + return 1 def getvisiblename(self): @@ -143,14 +148,17 @@ def getexplainedname(self): def getrepository(self): """Returns the repository object that this folder is within.""" + return self.repository def getroot(self): """Returns the root of the folder, in a folder-specific fashion.""" + return self.root def getsep(self): """Returns the separator for this folder type.""" + return self.sep def getfullname(self): @@ -160,7 +168,8 @@ def getfullname(self): return self.getname() def getfolderbasename(self): - """Return base file name of file to store Status/UID info in""" + """Return base file name of file to store Status/UID info in.""" + if not self.name: basename = '.' else: #avoid directory hierarchies and file names such as '/' @@ -188,6 +197,7 @@ def check_uidvalidity(self): def _getuidfilename(self): """provides UIDVALIDITY cache filename for class internal purposes""" + return os.path.join(self.repository.getuiddir(), self.getfolderbasename()) @@ -196,6 +206,7 @@ def get_saveduidvalidity(self): :returns: UIDVALIDITY as (long) number or None, if None had been saved yet.""" + if hasattr(self, '_base_saved_uidvalidity'): return self._base_saved_uidvalidity uidfilename = self._getuidfilename() @@ -212,6 +223,7 @@ def save_uidvalidity(self): This function is not threadsafe, so don't attempt to call it from concurrent threads.""" + newval = self.get_uidvalidity() uidfilename = self._getuidfilename() @@ -225,45 +237,50 @@ def get_uidvalidity(self): This function needs to be implemented by each Backend :returns: UIDVALIDITY as a (long) number""" + raise NotImplementedError def cachemessagelist(self): """Reads the message list from disk or network and stores it in memory for later use. This list will not be re-read from disk or memory unless this function is called again.""" + raise NotImplementedError def getmessagelist(self): """Gets the current message list. You must call cachemessagelist() before calling this function!""" + raise NotImplementedError def msglist_item_initializer(self, uid): - """ - Returns value for empty messagelist element with given UID. + """Returns value for empty messagelist element with given UID. This function must initialize all fields of messagelist item and must be called every time when one creates new messagelist - entry to ensure that all fields that must be present are present. + entry to ensure that all fields that must be present are present.""" - """ raise NotImplementedError def uidexists(self, uid): """Returns True if uid exists""" + return uid in self.getmessagelist() def getmessageuidlist(self): """Gets a list of UIDs. You may have to call cachemessagelist() before calling this function!""" + return self.getmessagelist().keys() def getmessagecount(self): """Gets the number of messages.""" + return len(self.getmessagelist()) def getmessage(self, uid): """Returns the content of the specified message.""" + raise NotImplementedError def savemessage(self, uid, content, flags, rtime): @@ -286,20 +303,23 @@ def savemessage(self, uid, content, flags, rtime): Note that savemessage() does not check against dryrun settings, so you need to ensure that savemessage is never called in a - dryrun mode. - """ + dryrun mode.""" + raise NotImplementedError def getmessagetime(self, uid): """Return the received time for the specified message.""" + raise NotImplementedError def getmessagemtime(self, uid): """Returns the message modification time of the specified message.""" + raise NotImplementedError def getmessageflags(self, uid): """Returns the flags for the specified message.""" + raise NotImplementedError def savemessageflags(self, uid, flags): @@ -308,6 +328,7 @@ def savemessageflags(self, uid, flags): Note that this function does not check against dryrun settings, so you need to ensure that it is never called in a dryrun mode.""" + raise NotImplementedError def addmessageflags(self, uid, flags): @@ -319,14 +340,15 @@ def addmessageflags(self, uid, flags): dryrun mode. :param flags: A set() of flags""" + newflags = self.getmessageflags(uid) | flags self.savemessageflags(uid, newflags) def addmessagesflags(self, uidlist, flags): - """ - Note that this function does not check against dryrun settings, + """Note that this function does not check against dryrun settings, so you need to ensure that it is never called in a dryrun mode.""" + for uid in uidlist: self.addmessageflags(uid, flags) @@ -337,6 +359,7 @@ def deletemessageflags(self, uid, flags): Note that this function does not check against dryrun settings, so you need to ensure that it is never called in a dryrun mode.""" + newflags = self.getmessageflags(uid) - flags self.savemessageflags(uid, newflags) @@ -345,10 +368,10 @@ def deletemessagesflags(self, uidlist, flags): Note that this function does not check against dryrun settings, so you need to ensure that it is never called in a dryrun mode.""" + for uid in uidlist: self.deletemessageflags(uid, flags) - def getmessagelabels(self, uid): """Returns the labels for the specified message.""" raise NotImplementedError @@ -359,6 +382,7 @@ def savemessagelabels(self, uid, labels, ignorelabels=set(), mtime=0): Note that this function does not check against dryrun settings, so you need to ensure that it is never called in a dryrun mode.""" + raise NotImplementedError def addmessagelabels(self, uid, labels): @@ -370,14 +394,15 @@ def addmessagelabels(self, uid, labels): dryrun mode. :param labels: A set() of labels""" + newlabels = self.getmessagelabels(uid) | labels self.savemessagelabels(uid, newlabels) def addmessageslabels(self, uidlist, labels): - """ - Note that this function does not check against dryrun settings, + """Note that this function does not check against dryrun settings, so you need to ensure that it is never called in a dryrun mode.""" + for uid in uidlist: self.addmessagelabels(uid, labels) @@ -388,6 +413,7 @@ def deletemessagelabels(self, uid, labels): Note that this function does not check against dryrun settings, so you need to ensure that it is never called in a dryrun mode.""" + newlabels = self.getmessagelabels(uid) - labels self.savemessagelabels(uid, newlabels) @@ -396,12 +422,12 @@ def deletemessageslabels(self, uidlist, labels): Note that this function does not check against dryrun settings, so you need to ensure that it is never called in a dryrun mode.""" + for uid in uidlist: self.deletemessagelabels(uid, labels) def addmessageheader(self, content, linebreak, headername, headervalue): - """ - Adds new header to the provided message. + """Adds new header to the provided message. WARNING: This function is a bit tricky, and modifying it in the wrong way, may easily lead to data-loss. @@ -454,9 +480,9 @@ def addmessageheader(self, content, linebreak, headername, headervalue): This is the body\n next line\n """ - self.ui.debug('', - 'addmessageheader: called to add %s: %s' % (headername, - headervalue)) + + self.ui.debug('', 'addmessageheader: called to add %s: %s'% + (headername, headervalue)) insertionpoint = content.find(linebreak * 2) if insertionpoint == -1: @@ -490,24 +516,23 @@ def addmessageheader(self, content, linebreak, headername, headervalue): if content[0:len(linebreak)] != linebreak: suffix = suffix + linebreak - self.ui.debug('', 'addmessageheader: insertionpoint = %d' % insertionpoint) + self.ui.debug('', 'addmessageheader: insertionpoint = %d'% insertionpoint) headers = content[0:insertionpoint] - self.ui.debug('', 'addmessageheader: headers = %s' % repr(headers)) + self.ui.debug('', 'addmessageheader: headers = %s'% repr(headers)) new_header = prefix + ("%s: %s" % (headername, headervalue)) + suffix self.ui.debug('', 'addmessageheader: new_header = ' + repr(new_header)) return headers + new_header + content[insertionpoint:] def __find_eoh(self, content): - """ - Searches for the point where mail headers end. + """ Searches for the point where mail headers end. Either double '\n', or end of string. Arguments: - content: contents of the message to search in Returns: position of the first non-header byte. - """ + eoh_cr = content.find('\n\n') if eoh_cr == -1: eoh_cr = len(content) @@ -516,8 +541,7 @@ def __find_eoh(self, content): def getmessageheader(self, content, name): - """ - Searches for the first occurence of the given header and returns + """Searches for the first occurence of the given header and returns its value. Header name is case-insensitive. Arguments: @@ -525,13 +549,13 @@ def getmessageheader(self, content, name): - name: name of the header to be searched Returns: header value or None if no such header was found - """ - self.ui.debug('', 'getmessageheader: called to get %s' % name) + + self.ui.debug('', 'getmessageheader: called to get %s'% name) eoh = self.__find_eoh(content) - self.ui.debug('', 'getmessageheader: eoh = %d' % eoh) + self.ui.debug('', 'getmessageheader: eoh = %d'% eoh) headers = content[0:eoh] - self.ui.debug('', 'getmessageheader: headers = %s' % repr(headers)) + self.ui.debug('', 'getmessageheader: headers = %s'% repr(headers)) m = re.search('^%s:(.*)$' % name, headers, flags = re.MULTILINE | re.IGNORECASE) if m: @@ -541,8 +565,7 @@ def getmessageheader(self, content, name): def getmessageheaderlist(self, content, name): - """ - Searches for the given header and returns a list of values for + """Searches for the given header and returns a list of values for that header. Arguments: @@ -550,8 +573,8 @@ def getmessageheaderlist(self, content, name): - name: name of the header to be searched Returns: list of header values or emptylist if no such header was found - """ + self.ui.debug('', 'getmessageheaderlist: called to get %s' % name) eoh = self.__find_eoh(content) self.ui.debug('', 'getmessageheaderlist: eoh = %d' % eoh) @@ -562,27 +585,26 @@ def getmessageheaderlist(self, content, name): def deletemessageheaders(self, content, header_list): - """ - Deletes headers in the given list from the message content. + """Deletes headers in the given list from the message content. Arguments: - content: message itself - header_list: list of headers to be deleted or just the header name We expect our message to have '\n' as line endings. - """ + if type(header_list) != type([]): header_list = [header_list] - self.ui.debug('', 'deletemessageheaders: called to delete %s' % (header_list)) + self.ui.debug('', 'deletemessageheaders: called to delete %s'% (header_list)) if not len(header_list): return content eoh = self.__find_eoh(content) - self.ui.debug('', 'deletemessageheaders: end of headers = %d' % eoh) + self.ui.debug('', 'deletemessageheaders: end of headers = %d'% eoh) headers = content[0:eoh] rest = content[eoh:] - self.ui.debug('', 'deletemessageheaders: headers = %s' % repr(headers)) + self.ui.debug('', 'deletemessageheaders: headers = %s'% repr(headers)) new_headers = [] for h in headers.split('\n'): keep_it = True @@ -609,16 +631,14 @@ def change_message_uid(self, uid, new_uid): raise NotImplementedError def deletemessage(self, uid): - """ - Note that this function does not check against dryrun settings, + """Note that this function does not check against dryrun settings, so you need to ensure that it is never called in a dryrun mode.""" raise NotImplementedError def deletemessages(self, uidlist): - """ - Note that this function does not check against dryrun settings, + """Note that this function does not check against dryrun settings, so you need to ensure that it is never called in a dryrun mode.""" @@ -686,9 +706,8 @@ def copymessageto(self, uid, dstfolder, statusfolder, register = 1): self.deletemessage(uid) else: raise OfflineImapError("Trying to save msg (uid %d) on folder " - "%s returned invalid uid %d" % (uid, - dstfolder.getvisiblename(), new_uid), - OfflineImapError.ERROR.MESSAGE) + "%s returned invalid uid %d"% (uid, dstfolder.getvisiblename(), + new_uid), OfflineImapError.ERROR.MESSAGE) except (KeyboardInterrupt): # bubble up CTRL-C raise except OfflineImapError as e: @@ -697,8 +716,7 @@ def copymessageto(self, uid, dstfolder, statusfolder, register = 1): self.ui.error(e, exc_info()[2]) except Exception as e: self.ui.error(e, exc_info()[2], - msg="Copying message %s [acc: %s]" %\ - (uid, self.accountname)) + msg = "Copying message %s [acc: %s]"% (uid, self.accountname)) raise #raise on unknown errors, so we can fix those def __syncmessagesto_copy(self, dstfolder, statusfolder): @@ -714,6 +732,7 @@ def __syncmessagesto_copy(self, dstfolder, statusfolder): This function checks and protects us from action in ryrun mode. """ + threads = [] copylist = filter(lambda uid: not \ @@ -754,6 +773,7 @@ def __syncmessagesto_delete(self, dstfolder, statusfolder): This function checks and protects us from action in ryrun mode. """ + deletelist = filter(lambda uid: uid>=0 \ and not self.uidexists(uid), statusfolder.getmessageuidlist()) @@ -776,6 +796,7 @@ def __syncmessagesto_flags(self, dstfolder, statusfolder): This function checks and protects us from action in ryrun mode. """ + # For each flag, we store a list of uids to which it should be # added. Then, we can call addmessagesflags() to apply them in # bulk, rather than one call per message. @@ -854,8 +875,8 @@ def syncmessagesto(self, dstfolder, statusfolder): :param dstfolder: Folderinstance to sync the msgs to. :param statusfolder: LocalStatus instance to sync against. - """ + for (passdesc, action) in self.syncmessagesto_passes: # bail out on CTRL-C or SIGTERM if offlineimap.accounts.Account.abort_NOW_signal.is_set(): @@ -883,6 +904,7 @@ def __eq__(self, other): MailDirFolder('foo') == IMAPFolder('foo') --> False MailDirFolder('foo') == MaildirFolder('foo') --> False """ + if isinstance(other, basestring): return other == self.name return id(self) == id(other) diff --git a/offlineimap/folder/IMAP.py b/offlineimap/folder/IMAP.py index a69888871..52493fc00 100644 --- a/offlineimap/folder/IMAP.py +++ b/offlineimap/folder/IMAP.py @@ -20,6 +20,7 @@ import re import time from sys import exc_info + from .Base import BaseFolder from offlineimap import imaputil, imaplibutil, emailutil, OfflineImapError from offlineimap import globals @@ -54,7 +55,7 @@ def __init__(self, imapserver, name, repository): self.filterheaders = [h for h in re.split(r'\s*,\s*', fh_conf) if h] - def __selectro(self, imapobj, force = False): + def __selectro(self, imapobj, force=False): """Select this folder when we do not need write access. Prefer SELECT to EXAMINE if we can, since some servers @@ -86,6 +87,7 @@ def get_uidvalidity(self): UIDVALIDITY value will be cached on the first call. :returns: The UIDVALIDITY as (long) number.""" + if hasattr(self, '_uidvalidity'): # use cached value if existing return self._uidvalidity @@ -141,8 +143,7 @@ def quickchanged(self, statusfolder): def _msgs_to_fetch(self, imapobj): - """ - Determines sequence numbers of messages to be fetched. + """Determines sequence numbers of messages to be fetched. Message sequence numbers (MSNs) are more easily compacted into ranges which makes transactions slightly faster. @@ -151,9 +152,8 @@ def _msgs_to_fetch(self, imapobj): - imapobj: instance of IMAPlib Returns: range(s) for messages or None if no messages - are to be fetched. + are to be fetched.""" - """ res_type, imapdata = imapobj.select(self.getfullname(), True, True) if imapdata == [None] or imapdata[0] == '0': # Empty folder, no need to populate message list @@ -162,9 +162,9 @@ def _msgs_to_fetch(self, imapobj): # By default examine all messages in this folder msgsToFetch = '1:*' - maxage = self.config.getdefaultint("Account %s" % self.accountname, + maxage = self.config.getdefaultint("Account %s"% self.accountname, "maxage", -1) - maxsize = self.config.getdefaultint("Account %s" % self.accountname, + maxsize = self.config.getdefaultint("Account %s"% self.accountname, "maxsize", -1) # Build search condition @@ -193,7 +193,7 @@ def _msgs_to_fetch(self, imapobj): res_type, res_data = imapobj.search(None, search_cond) if res_type != 'OK': raise OfflineImapError("SEARCH in folder [%s]%s failed. " - "Search string was '%s'. Server responded '[%s] %s'" % ( + "Search string was '%s'. Server responded '[%s] %s'"% ( self.getrepository(), self, search_cond, res_type, res_data), OfflineImapError.ERROR.FOLDER) @@ -219,11 +219,11 @@ def cachemessagelist(self): # Get the flags and UIDs for these. single-quotes prevent # imaplib2 from quoting the sequence. - res_type, response = imapobj.fetch("'%s'" % msgsToFetch, - '(FLAGS UID)') + res_type, response = imapobj.fetch("'%s'"% + msgsToFetch, '(FLAGS UID)') if res_type != 'OK': raise OfflineImapError("FETCHING UIDs in folder [%s]%s failed. " - "Server responded '[%s] %s'" % ( + "Server responded '[%s] %s'"% ( self.getrepository(), self, res_type, response), OfflineImapError.ERROR.FOLDER) @@ -238,7 +238,7 @@ def cachemessagelist(self): messagestr = messagestr.split(' ', 1)[1] options = imaputil.flags2hash(messagestr) if not 'UID' in options: - self.ui.warn('No UID in message with options %s' %\ + self.ui.warn('No UID in message with options %s'% \ str(options), minor = 1) else: @@ -255,16 +255,15 @@ def getmessagelist(self): # 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'. :returns: the message body or throws and OfflineImapError (probably severity MESSAGE) if e.g. no message with this UID could be found. - """ + imapobj = self.imapserver.acquireconnection() try: data = self._fetch_from_imap(imapobj, str(uid), 2) @@ -281,7 +280,7 @@ def getmessage(self, uid): else: dbg_output = data - self.ui.debug('imap', "Returned object from fetching %d: '%s'" % + self.ui.debug('imap', "Returned object from fetching %d: '%s'"% (uid, dbg_output)) return data @@ -307,6 +306,7 @@ def __generate_randomheader(self, content): headername == 'X-OfflineIMAP' and headervalue will be a random string """ + headername = 'X-OfflineIMAP' # We need a random component too. If we ever upload the same # mail twice (e.g. in different folders), we would still need to @@ -322,20 +322,20 @@ def __generate_randomheader(self, content): def __savemessage_searchforheader(self, imapobj, headername, headervalue): - self.ui.debug('imap', '__savemessage_searchforheader called for %s: %s' % \ - (headername, headervalue)) + self.ui.debug('imap', '__savemessage_searchforheader called for %s: %s'% \ + (headername, headervalue)) # Now find the UID it got. headervalue = imapobj._quote(headervalue) try: matchinguids = imapobj.uid('search', '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)) + self.ui.debug('imap', "__savemessage_searchforheader: got IMAP error '%s' while attempting to UID SEARCH for message with header %s"% (err, headername)) return 0 self.ui.debug('imap', '__savemessage_searchforheader got initial matchinguids: ' + repr(matchinguids)) if matchinguids == '': - self.ui.debug('imap', "__savemessage_searchforheader: UID SEARCH for message with header %s yielded no results" % headername) + self.ui.debug('imap', "__savemessage_searchforheader: UID SEARCH for message with header %s yielded no results"% headername) return 0 matchinguids = matchinguids.split(' ') @@ -343,7 +343,7 @@ def __savemessage_searchforheader(self, imapobj, headername, headervalue): repr(matchinguids)) if len(matchinguids) != 1 or matchinguids[0] == None: raise ValueError("While attempting to find UID for message with " - "header %s, got wrong-sized matchinguids of %s" %\ + "header %s, got wrong-sized matchinguids of %s"%\ (headername, str(matchinguids))) return long(matchinguids[0]) @@ -368,9 +368,9 @@ def __savemessage_fetchheaders(self, imapobj, headername, headervalue): We need to locate the UID just after mail headers containing our X-OfflineIMAP line. - Returns UID when found, 0 when not found. - """ - self.ui.debug('imap', '__savemessage_fetchheaders called for %s: %s' % \ + Returns UID when found, 0 when not found.""" + + self.ui.debug('imap', '__savemessage_fetchheaders called for %s: %s'% \ (headername, headervalue)) # run "fetch X:* rfc822.header" @@ -381,7 +381,7 @@ def __savemessage_fetchheaders(self, imapobj, headername, headervalue): # ascending. if self.getmessagelist(): - start = 1+max(self.getmessagelist().keys()) + start = 1 + max(self.getmessagelist().keys()) else: # Folder was empty - start from 1 start = 1 @@ -390,7 +390,7 @@ def __savemessage_fetchheaders(self, imapobj, headername, headervalue): # with the range X:*. So we use bytearray to stop imaplib from getting # in our way - result = imapobj.uid('FETCH', bytearray('%d:*' % start), 'rfc822.header') + 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) @@ -401,7 +401,7 @@ def __savemessage_fetchheaders(self, imapobj, headername, headervalue): for item in result: if found == 0 and type(item) == type( () ): # Walk just tuples - if re.search("(?:^|\\r|\\n)%s:\s*%s(?:\\r|\\n)" % (headername, headervalue), + if re.search("(?:^|\\r|\\n)%s:\s*%s(?:\\r|\\n)"% (headername, headervalue), item[1], flags=re.IGNORECASE): found = 1 elif found == 1: @@ -467,8 +467,8 @@ def __getmessageinternaldate(self, content, rtime=None): # or something. Argh. It seems that Time2Internaldate # will rause a ValueError if the year is 0102 but not 1902, # but some IMAP servers nonetheless choke on 1902. - self.ui.debug('imap', "Message with invalid date %s. Server will use local time." \ - % datetuple) + self.ui.debug('imap', "Message with invalid date %s. " + "Server will use local time."% datetuple) return None #produce a string representation of datetuple that works as @@ -507,6 +507,7 @@ def savemessage(self, uid, content, flags, rtime): message is saved, but it's UID can not be found, it will return 0. If the message can't be written (folder is read-only for example) it will return -1.""" + self.ui.savemessage('imap', uid, flags, self) # already have it, just save modified flags @@ -543,17 +544,17 @@ def savemessage(self, uid, content, flags, rtime): if not use_uidplus: # insert a random unique header that we can fetch later (headername, headervalue) = self.__generate_randomheader( - content) - self.ui.debug('imap', 'savemessage: header is: %s: %s' %\ - (headername, headervalue)) + content) + self.ui.debug('imap', 'savemessage: header is: %s: %s'% + (headername, headervalue)) content = self.addmessageheader(content, CRLF, headername, headervalue) if len(content)>200: dbg_output = "%s...%s" % (content[:150], content[-50:]) else: dbg_output = content - self.ui.debug('imap', "savemessage: date: %s, content: '%s'" % - (date, dbg_output)) + self.ui.debug('imap', "savemessage: date: %s, content: '%s'"% + (date, dbg_output)) try: # Select folder for append and make the box READ-WRITE @@ -566,9 +567,8 @@ def savemessage(self, uid, content, flags, rtime): #Do the APPEND try: - (typ, dat) = imapobj.append(self.getfullname(), - imaputil.flagsmaildir2imap(flags), - date, content) + (typ, dat) = imapobj.append(fullname, + imaputil.flagsmaildir2imap(flags), date, content) # This should only catch 'NO' responses since append() # will raise an exception for 'BAD' responses: if typ != 'OK': @@ -580,7 +580,7 @@ def savemessage(self, uid, content, flags, rtime): # and continue with the next account. msg = \ "Saving msg (%s) in folder '%s', repository '%s' failed (abort). " \ - "Server responded: %s %s\n" % \ + "Server responded: %s %s\n"% \ (msg_id, self, self.getrepository(), typ, dat) raise OfflineImapError(msg, OfflineImapError.ERROR.REPO) retry_left = 0 # Mark as success @@ -592,7 +592,7 @@ def savemessage(self, uid, content, flags, rtime): if not retry_left: raise OfflineImapError("Saving msg (%s) in folder '%s', " "repository '%s' failed (abort). Server responded: %s\n" - "Message content was: %s" % + "Message content was: %s"% (msg_id, self, self.getrepository(), str(e), dbg_output), OfflineImapError.ERROR.MESSAGE) self.ui.error(e, exc_info()[2]) @@ -604,8 +604,8 @@ def savemessage(self, uid, content, flags, rtime): imapobj = None raise OfflineImapError("Saving msg (%s) folder '%s', repo '%s'" "failed (error). Server responded: %s\nMessage content was: " - "%s" % (msg_id, self, self.getrepository(), str(e), dbg_output), - OfflineImapError.ERROR.MESSAGE) + "%s"% (msg_id, self, self.getrepository(), str(e), dbg_output), + OfflineImapError.ERROR.MESSAGE) # Checkpoint. Let it write out stuff, etc. Eg searches for # just uploaded messages won't work if we don't do this. (typ,dat) = imapobj.check() @@ -622,24 +622,24 @@ def savemessage(self, uid, content, flags, rtime): resp = imapobj._get_untagged_response('APPENDUID') if resp == [None] or resp is None: self.ui.warn("Server supports UIDPLUS but got no APPENDUID " - "appending a message.") + "appending a message.") return 0 uid = long(resp[-1].split(' ')[1]) if uid == 0: self.ui.warn("savemessage: Server supports UIDPLUS, but" - " we got no usable uid back. APPENDUID reponse was " - "'%s'" % str(resp)) + " we got no usable uid back. APPENDUID reponse was " + "'%s'"% str(resp)) else: # we don't support UIDPLUS uid = self.__savemessage_searchforheader(imapobj, headername, - headervalue) + headervalue) # See docs for savemessage in Base.py for explanation # of this and other return values if uid == 0: self.ui.debug('imap', 'savemessage: attempt to get new UID ' 'UID failed. Search headers manually.') uid = self.__savemessage_fetchheaders(imapobj, headername, - headervalue) + headervalue) self.ui.warn('imap', "savemessage: Searching mails for new " "Message-ID failed. Could not determine new UID.") finally: @@ -649,22 +649,21 @@ def savemessage(self, uid, content, flags, rtime): self.messagelist[uid] = self.msglist_item_initializer(uid) self.messagelist[uid]['flags'] = flags - self.ui.debug('imap', 'savemessage: returning new UID %d' % uid) + self.ui.debug('imap', 'savemessage: returning new UID %d'% uid) return uid def _fetch_from_imap(self, imapobj, uids, retry_num=1): - """ - Fetches data from IMAP server. + """Fetches data from IMAP server. Arguments: - imapobj: IMAPlib object - uids: message UIDS - retry_num: number of retries to make - Returns: data obtained by this query. - """ - query = "(%s)" % (" ".join(self.imap_query)) + Returns: data obtained by this query.""" + + query = "(%s)"% (" ".join(self.imap_query)) fails_left = retry_num # retry on dropped connection while fails_left: try: @@ -683,7 +682,7 @@ def _fetch_from_imap(self, imapobj, uids, retry_num=1): #IMAP server says bad request or UID does not exist severity = OfflineImapError.ERROR.MESSAGE reason = "IMAP server '%s' failed to fetch messages UID '%s'."\ - "Server responded: %s %s" % (self.getrepository(), uids, + "Server responded: %s %s"% (self.getrepository(), uids, res_type, data) if data == [None]: #IMAP server did not find a message with this UID @@ -695,23 +694,21 @@ def _fetch_from_imap(self, imapobj, uids, retry_num=1): def _store_to_imap(self, imapobj, uid, field, data): - """ - Stores data to IMAP server + """Stores data to IMAP server Arguments: - imapobj: instance of IMAPlib to use - uid: message UID - field: field name to be stored/updated - data: field contents - """ imapobj.select(self.getfullname()) res_type, retdata = imapobj.uid('store', uid, field, data) if res_type != 'OK': severity = OfflineImapError.ERROR.MESSAGE reason = "IMAP server '%s' failed to store %s for message UID '%d'."\ - "Server responded: %s %s" % (self.getrepository(), field, uid, - res_type, retdata) + "Server responded: %s %s"% ( + self.getrepository(), field, uid, res_type, retdata) raise OfflineImapError(reason, severity) return retdata[0] @@ -724,12 +721,11 @@ def savemessageflags(self, uid, flags): dryrun mode.""" imapobj = self.imapserver.acquireconnection() try: - result = self._store_to_imap(imapobj, str(uid), 'FLAGS', imaputil.flagsmaildir2imap(flags)) - + result = self._store_to_imap(imapobj, str(uid), 'FLAGS', + imaputil.flagsmaildir2imap(flags)) except imapobj.readonly: self.ui.flagstoreadonly(self, [uid], flags) return - finally: self.imapserver.releaseconnection(imapobj) @@ -751,6 +747,7 @@ def addmessagesflags(self, uidlist, flags): """This is here for the sake of UIDMaps.py -- deletemessages must add flags and get a converted UID, and if we don't have noconvert, then UIDMaps will try to convert it twice.""" + self.__addmessagesflags_noconvert(uidlist, flags) # Interface from BaseFolder @@ -770,9 +767,8 @@ def __processmessagesflags_real(self, operation, uidlist, flags): self.ui.flagstoreadonly(self, uidlist, flags) return r = imapobj.uid('store', - imaputil.uid_sequence(uidlist), - operation + 'FLAGS', - imaputil.flagsmaildir2imap(flags)) + imaputil.uid_sequence(uidlist), operation + 'FLAGS', + imaputil.flagsmaildir2imap(flags)) assert r[0] == 'OK', 'Error with store: ' + '. '.join(r[1]) r = r[1] finally: @@ -818,9 +814,9 @@ def change_message_uid(self, uid, new_uid): """Change the message from existing uid to new_uid If the backend supports it. IMAP does not and will throw errors.""" + raise OfflineImapError('IMAP backend cannot change a messages UID from ' - '%d to %d' % (uid, new_uid), - OfflineImapError.ERROR.MESSAGE) + '%d to %d'% (uid, new_uid), OfflineImapError.ERROR.MESSAGE) # Interface from BaseFolder def deletemessage(self, uid): diff --git a/offlineimap/imapserver.py b/offlineimap/imapserver.py index 844012e2d..5601033a6 100644 --- a/offlineimap/imapserver.py +++ b/offlineimap/imapserver.py @@ -56,7 +56,7 @@ def __init__(self, repos): 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) @@ -116,8 +116,9 @@ def __getpassword(self): # XXX: is this function used anywhere? def getroot(self): - """Returns this server's folder root. Can only be called after one + """Returns this server's folder root. Can only be called after one or more calls to acquireconnection.""" + return self.root @@ -126,6 +127,7 @@ def releaseconnection(self, connection, drop_conn=False): :param drop_conn: If True, the connection will be released and not be reused. This can be used to indicate broken connections.""" + if connection is None: return #noop on bad connection self.connectionlock.acquire() self.assignedconnections.remove(connection) @@ -139,25 +141,24 @@ def releaseconnection(self, connection, drop_conn=False): def __md5handler(self, response): challenge = response.strip() - self.ui.debug('imap', '__md5handler: got challenge %s' % challenge) + self.ui.debug('imap', '__md5handler: got challenge %s'% challenge) passwd = self.__getpassword() retval = self.username + ' ' + hmac.new(passwd, challenge).hexdigest() - self.ui.debug('imap', '__md5handler: returning %s' % retval) + self.ui.debug('imap', '__md5handler: returning %s'% retval) return retval def __loginauth(self, imapobj): - """ Basic authentication via LOGIN command """ + """ Basic authentication via LOGIN command.""" + self.ui.debug('imap', 'Attempting IMAP LOGIN authentication') imapobj.login(self.username, self.__getpassword()) def __plainhandler(self, response): - """ - Implements SASL PLAIN authentication, RFC 4616, - http://tools.ietf.org/html/rfc4616 + """Implements SASL PLAIN authentication, RFC 4616, + http://tools.ietf.org/html/rfc4616""" - """ authc = self.username passwd = self.__getpassword() authz = '' @@ -175,8 +176,8 @@ def __gssauth(self, response): try: if self.gss_step == self.GSS_STATE_STEP: if not self.gss_vc: - rc, self.gss_vc = kerberos.authGSSClientInit('imap@' + - self.hostname) + rc, self.gss_vc = kerberos.authGSSClientInit( + 'imap@' + self.hostname) response = kerberos.authGSSClientResponse(self.gss_vc) rc = kerberos.authGSSClientStep(self.gss_vc, data) if rc != kerberos.AUTH_GSS_CONTINUE: @@ -184,13 +185,13 @@ def __gssauth(self, response): elif self.gss_step == self.GSS_STATE_WRAP: rc = kerberos.authGSSClientUnwrap(self.gss_vc, data) response = kerberos.authGSSClientResponse(self.gss_vc) - rc = kerberos.authGSSClientWrap(self.gss_vc, response, - self.username) + rc = kerberos.authGSSClientWrap( + self.gss_vc, response, self.username) response = kerberos.authGSSClientResponse(self.gss_vc) except kerberos.GSSError as err: # Kerberos errored out on us, respond with None to cancel the # authentication - self.ui.debug('imap', '%s: %s' % (err[0][0], err[1][0])) + self.ui.debug('imap', '%s: %s'% (err[0][0], err[1][0])) return None if not response: @@ -205,7 +206,7 @@ def __start_tls(self, imapobj): imapobj.starttls() except imapobj.error as e: raise OfflineImapError("Failed to start " - "TLS connection: %s" % str(e), + "TLS connection: %s"% str(e), OfflineImapError.ERROR.REPO) @@ -266,8 +267,7 @@ def __authn_login(self, imapobj): def __authn_helper(self, imapobj): - """ - Authentication machinery for self.acquireconnection(). + """Authentication machinery for self.acquireconnection(). Raises OfflineImapError() of type ERROR.REPO when there are either fatal problems or no authentications @@ -275,9 +275,7 @@ def __authn_helper(self, imapobj): If any authentication method succeeds, routine should exit: warnings for failed methods are to be produced in the - respective except blocks. - - """ + respective except blocks.""" # Authentication routines, hash keyed by method name # with value that is a tuple with @@ -321,13 +319,13 @@ def __authn_helper(self, imapobj): continue tried_to_authn = True - self.ui.debug('imap', 'Attempting ' - '%s authentication' % m) + self.ui.debug('imap', u'Attempting ' + '%s authentication'% m) try: if func(imapobj): return except (imapobj.error, OfflineImapError) as e: - self.ui.warn('%s authentication failed: %s' % (m, e)) + self.ui.warn('%s authentication failed: %s'% (m, e)) exc_stack.append((m, e)) if len(exc_stack): @@ -343,9 +341,9 @@ def __authn_helper(self, imapobj): lambda x: x[5:], filter(lambda x: x[0:5] == "AUTH=", imapobj.capabilities) )) - raise OfflineImapError("Repository %s: no supported " + raise OfflineImapError(u"Repository %s: no supported " "authentication mechanisms found; configured %s, " - "server advertises %s" % (self.repos, + "server advertises %s"% (self.repos, ", ".join(self.authmechs), methods), OfflineImapError.ERROR.REPO) @@ -383,9 +381,8 @@ def acquireconnection(self): self.connectionlock.release() # Release until need to modify data - """ Must be careful here that if we fail we should bail out gracefully - and release locks / threads so that the next attempt can try... - """ + # Must be careful here that if we fail we should bail out gracefully + # and release locks / threads so that the next attempt can try... success = 0 try: while not success: @@ -441,7 +438,7 @@ def acquireconnection(self): # No Folders were returned. This occurs, e.g. if the # 'reference' prefix does not exist on the mail # server. Raise exception. - err = "Server '%s' returned no folders in '%s'" % \ + err = "Server '%s' returned no folders in '%s'"% \ (self.repos.getname(), self.reference) self.ui.warn(err) raise Exception(err) @@ -458,6 +455,7 @@ def acquireconnection(self): """If we are here then we did not succeed in getting a connection - we should clean up and then re-raise the error...""" + self.semaphore.release() severity = OfflineImapError.ERROR.REPO @@ -489,7 +487,7 @@ def acquireconnection(self): 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) # Could not acquire connection to the remote; # socket.error(last_error) raised @@ -503,13 +501,15 @@ def acquireconnection(self): raise def connectionwait(self): - """Waits until there is a connection available. Note that between - the time that a connection becomes available and the time it is - requested, another thread may have grabbed it. This function is - mainly present as a way to avoid spawning thousands of threads - to copy messages, then have them all wait for 3 available connections. - It's OK if we have maxconnections + 1 or 2 threads, which is what - this will help us do.""" + """Waits until there is a connection available. + + Note that between the time that a connection becomes available and the + time it is requested, another thread may have grabbed it. This function + is mainly present as a way to avoid spawning thousands of threads to + copy messages, then have them all wait for 3 available connections. + It's OK if we have maxconnections + 1 or 2 threads, which is what this + will help us do.""" + self.semaphore.acquire() self.semaphore.release() @@ -533,11 +533,13 @@ def close(self): self.gssapi = False def keepalive(self, timeout, event): - """Sends a NOOP to each connection recorded. It will wait a maximum - of timeout seconds between doing this, and will continue to do so - until the Event object as passed is true. This method is expected - to be invoked in a separate thread, which should be join()'d after - the event is set.""" + """Sends a NOOP to each connection recorded. + + It will wait a maximum of timeout seconds between doing this, and will + continue to do so until the Event object as passed is true. This method + is expected to be invoked in a separate thread, which should be join()'d + after the event is set.""" + self.ui.debug('imap', 'keepalive thread started') while not event.isSet(): self.connectionlock.acquire() @@ -547,7 +549,7 @@ def keepalive(self, timeout, event): threads = [] for i in range(numconnections): - self.ui.debug('imap', 'keepalive: processing connection %d of %d' % (i, numconnections)) + self.ui.debug('imap', 'keepalive: processing connection %d of %d'% (i, numconnections)) if len(self.idlefolders) > i: # IDLE thread idler = IdleThread(self, self.idlefolders[i]) @@ -570,14 +572,14 @@ def keepalive(self, timeout, event): return def __verifycert(self, cert, hostname): - '''Verify that cert (in socket.getpeercert() format) matches hostname. + """Verify that cert (in socket.getpeercert() format) matches hostname. + CRLs are not handled. + Returns error message if any problems are found and None on success.""" - Returns error message if any problems are found and None on success. - ''' errstr = "CA Cert verifying failed: " if not cert: - return ('%s no certificate received' % errstr) + return ('%s no certificate received'% errstr) dnsname = hostname.lower() certnames = [] @@ -585,7 +587,7 @@ def __verifycert(self, cert, hostname): notafter = cert.get('notAfter') if notafter: if time.time() >= cert_time_to_seconds(notafter): - return '%s certificate expired %s' % (errstr, notafter) + return '%s certificate expired %s'% (errstr, notafter) # First read commonName for s in cert.get('subject', []): @@ -593,7 +595,7 @@ def __verifycert(self, cert, hostname): if key == 'commonName': certnames.append(value.lower()) if len(certnames) == 0: - return ('%s no commonName found in certificate' % errstr) + return ('%s no commonName found in certificate'% errstr) # Then read subjectAltName for key, value in cert.get('subjectAltName', []): @@ -606,7 +608,7 @@ def __verifycert(self, cert, hostname): '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1]): return None - return ('%s no matching domain name found in certificate' % errstr) + return ('%s no matching domain name found in certificate'% errstr) class IdleThread(object): @@ -614,6 +616,7 @@ def __init__(self, parent, folder=None): """If invoked without 'folder', perform a NOOP and wait for self.stop() to be called. If invoked with folder, switch to IDLE mode and synchronize once we have a new message""" + self.parent = parent self.folder = folder self.stop_sig = Event() @@ -634,18 +637,18 @@ def join(self): self.thread.join() def noop(self): - #TODO: AFAIK this is not optimal, we will send a NOOP on one - #random connection (ie not enough to keep all connections - #open). In case we do the noop multiple times, we can well use - #the same connection every time, as we get a random one. This - #function should IMHO send a noop on ALL available connections - #to the server. + # TODO: AFAIK this is not optimal, we will send a NOOP on one + # random connection (ie not enough to keep all connections + # open). In case we do the noop multiple times, we can well use + # the same connection every time, as we get a random one. This + # function should IMHO send a noop on ALL available connections + # to the server. imapobj = self.parent.acquireconnection() try: 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) imapobj = None finally: diff --git a/offlineimap/imaputil.py b/offlineimap/imaputil.py index 962358996..e5eb541f0 100644 --- a/offlineimap/imaputil.py +++ b/offlineimap/imaputil.py @@ -121,8 +121,7 @@ def imapsplit(imapstring): arg = arg.replace('\\', '\\\\') arg = arg.replace('"', '\\"') arg = '"%s"' % arg - __debug("imapsplit() non-string [%d]: Appending %s" %\ - (i, arg)) + __debug("imapsplit() non-string [%d]: Appending %s"% (i, arg)) retval.append(arg) else: # Even -- we have a string that ends with a literal @@ -131,8 +130,8 @@ def imapsplit(imapstring): # Recursion to the rescue. arg = imapstring[i] arg = re.sub('\{\d+\}$', '', arg) - __debug("imapsplit() non-string [%d]: Feeding %s to recursion" %\ - (i, arg)) + __debug("imapsplit() non-string [%d]: Feeding %s to recursion"%\ + (i, arg)) retval.extend(imapsplit(arg)) __debug("imapsplit() non-string: returning %s" % str(retval)) return retval @@ -274,7 +273,7 @@ def __split_quoted(s): def format_labels_string(header, labels): """Formats labels for embedding into a message, with format according to header name. - + Headers from SPACE_SEPARATED_LABEL_HEADERS keep space-separated list of labels, the rest uses comma (',') as the separator. diff --git a/offlineimap/init.py b/offlineimap/init.py index 6ceb86031..878f5cff9 100644 --- a/offlineimap/init.py +++ b/offlineimap/init.py @@ -23,6 +23,7 @@ import socket import logging from optparse import OptionParser + import offlineimap from offlineimap import accounts, threadutil, syncmaster from offlineimap import globals @@ -180,7 +181,7 @@ def __parse_cmd_options(self): config = CustomConfigParser() if not os.path.exists(configfilename): # TODO, initialize and make use of chosen ui for logging - logging.error(" *** Config file '%s' does not exist; aborting!" % + logging.error(" *** Config file '%s' does not exist; aborting!"% configfilename) sys.exit(1) config.read(configfilename) @@ -193,14 +194,14 @@ def __parse_cmd_options(self): options.singlethreading = True if os.path.exists(options.profiledir): # TODO, make use of chosen ui for logging - logging.warn("Profile mode: Directory '%s' already exists!" % + logging.warn("Profile mode: Directory '%s' already exists!"% options.profiledir) else: os.mkdir(options.profiledir) threadutil.ExitNotifyThread.set_profiledir(options.profiledir) # TODO, make use of chosen ui for logging logging.warn("Profile mode: Potentially large data will be " - "created in '%s'" % options.profiledir) + "created in '%s'"% options.profiledir) #override a config value if options.configoverride: @@ -234,8 +235,8 @@ def __parse_cmd_options(self): # create the ui class self.ui = UI_LIST[ui_type.lower()](config) except KeyError: - logging.error("UI '%s' does not exist, choose one of: %s" % \ - (ui_type,', '.join(UI_LIST.keys()))) + logging.error("UI '%s' does not exist, choose one of: %s"% \ + (ui_type, ', '.join(UI_LIST.keys()))) sys.exit(1) setglobalui(self.ui) @@ -331,13 +332,13 @@ def __sync(self, options): for account in activeaccounts: if account not in allaccounts: if len(allaccounts) == 0: - errormsg = "The account '%s' does not exist because no"\ - " accounts are defined!" % account + errormsg = "The account '%s' does not exist because no" \ + " accounts are defined!"% account else: - errormsg = "The account '%s' does not exist. Valid ac"\ - "counts are: " % account - errormsg += ", ".join(allaccounts.keys()) - self.ui.terminate(1, errormsg = errormsg) + errormsg = "The account '%s' does not exist. Valid ac" \ + "counts are: %s"% \ + (account, ", ".join(allaccounts.keys())) + self.ui.terminate(1, errormsg=errormsg) if account not in syncaccounts: syncaccounts.append(account) diff --git a/offlineimap/localeval.py b/offlineimap/localeval.py index e7d656fd9..06adc5715 100644 --- a/offlineimap/localeval.py +++ b/offlineimap/localeval.py @@ -23,9 +23,7 @@ pass class LocalEval: - """Here is a powerfull but very dangerous option, of course. - - Assume source file to be ASCII encoded.""" + """Here is a powerfull but very dangerous option, of course.""" def __init__(self, path=None): self.namespace = {} diff --git a/offlineimap/repository/Base.py b/offlineimap/repository/Base.py index 4edd6c5eb..cb9d06a3e 100644 --- a/offlineimap/repository/Base.py +++ b/offlineimap/repository/Base.py @@ -167,6 +167,7 @@ def sync_folder_structure(self, dst_repo, status_repo): that forward and backward nametrans actually match up! Configuring nametrans on BOTH repositories therefore could lead to infinite folder creation cycles.""" + if not self.get_create_folders() and not dst_repo.get_create_folders(): # quick exit if no folder creation is enabled on either side. return diff --git a/offlineimap/repository/LocalStatus.py b/offlineimap/repository/LocalStatus.py index 52ba714e8..13903475a 100644 --- a/offlineimap/repository/LocalStatus.py +++ b/offlineimap/repository/LocalStatus.py @@ -53,8 +53,8 @@ def setup_backend(self, backend): self.LocalStatusFolderClass = self.backends[backend]['class'] else: - raise SyntaxWarning("Unknown status_backend '%s' for account '%s'" \ - % (backend, self.account.name)) + raise SyntaxWarning("Unknown status_backend '%s' for account '%s'"% + (backend, self.account.name)) def import_other_backend(self, folder): for bk, dic in self.backends.items(): @@ -68,8 +68,9 @@ def import_other_backend(self, folder): # if backend contains data, import it to folder. if not folderbk.isnewfolder(): - self.ui._msg('Migrating LocalStatus cache from %s to %s ' % (bk, self._backend) + \ - 'status folder for %s:%s' % (self.name, folder.name)) + self.ui._msg('Migrating LocalStatus cache from %s to %s " \ + "status folder for %s:%s'% + (bk, self._backend, self.name, folder.name)) folderbk.cachemessagelist() folder.messagelist = folderbk.messagelist diff --git a/offlineimap/repository/Maildir.py b/offlineimap/repository/Maildir.py index 9c244bf2f..be86b34d4 100644 --- a/offlineimap/repository/Maildir.py +++ b/offlineimap/repository/Maildir.py @@ -32,7 +32,7 @@ def __init__(self, reposname, account): self.root = self.getlocalroot() self.folders = None self.ui = getglobalui() - self.debug("MaildirRepository initialized, sep is " + repr(self.getsep())) + self.debug("MaildirRepository initialized, sep is %s"% repr(self.getsep())) self.folder_atimes = [] # Create the top-level folder if it doesn't exist @@ -101,12 +101,12 @@ def makefolder(self, foldername): # If we're using hierarchical folders, it's possible that # sub-folders may be created before higher-up ones. - self.debug("makefolder: calling makedirs '%s'" % full_path) + self.debug("makefolder: calling makedirs '%s'"% full_path) try: os.makedirs(full_path, 0o700) except OSError as e: if e.errno == 17 and os.path.isdir(full_path): - self.debug("makefolder: '%s' already a directory" % foldername) + self.debug("makefolder: '%s' already a directory"% foldername) else: raise for subdir in ['cur', 'new', 'tmp']: @@ -114,13 +114,13 @@ def makefolder(self, foldername): os.mkdir(os.path.join(full_path, subdir), 0o700) except OSError as e: if e.errno == 17 and os.path.isdir(full_path): - self.debug("makefolder: '%s' already has subdir %s" % + self.debug("makefolder: '%s' already has subdir %s"% (foldername, subdir)) else: raise def deletefolder(self, foldername): - self.ui.warn("NOT YET IMPLEMENTED: DELETE FOLDER %s" % foldername) + self.ui.warn("NOT YET IMPLEMENTED: DELETE FOLDER %s"% foldername) def getfolder(self, foldername): """Return a Folder instance of this Maildir @@ -128,6 +128,7 @@ def getfolder(self, foldername): If necessary, scan and cache all foldernames to make sure that we only return existing folders and that 2 calls with the same name will return the same object.""" + # getfolders() will scan and cache the values *if* necessary folders = self.getfolders() for f in folders: @@ -142,8 +143,9 @@ def _getfolders_scandir(self, root, extension=None): :param root: (absolute) path to Maildir root :param extension: (relative) subfolder to examine within root""" - self.debug("_GETFOLDERS_SCANDIR STARTING. root = %s, extension = %s" \ - % (root, extension)) + + self.debug("_GETFOLDERS_SCANDIR STARTING. root = %s, extension = %s"% + (root, extension)) retval = [] # Configure the full path to this repository -- "toppath" @@ -151,11 +153,11 @@ def _getfolders_scandir(self, root, extension=None): toppath = os.path.join(root, extension) else: toppath = root - self.debug(" toppath = %s" % toppath) + self.debug(" toppath = %s"% toppath) # Iterate over directories in top & top itself. for dirname in os.listdir(toppath) + ['']: - self.debug(" dirname = %s" % dirname) + self.debug(" dirname = %s"% dirname) if dirname == '' and extension is not None: self.debug(' skip this entry (already scanned)') continue @@ -178,7 +180,7 @@ def _getfolders_scandir(self, root, extension=None): os.path.isdir(os.path.join(fullname, 'new')) and os.path.isdir(os.path.join(fullname, 'tmp'))): # This directory has maildir stuff -- process - self.debug(" This is maildir folder '%s'." % foldername) + self.debug(" This is maildir folder '%s'."% foldername) if self.getconfboolean('restoreatime', False): self._append_folder_atimes(foldername) fd = self.getfoldertype()(self.root, foldername, @@ -188,7 +190,7 @@ def _getfolders_scandir(self, root, extension=None): if self.getsep() == '/' and dirname != '': # Recursively check sub-directories for folders too. retval.extend(self._getfolders_scandir(root, foldername)) - self.debug("_GETFOLDERS_SCANDIR RETURNING %s" % \ + self.debug("_GETFOLDERS_SCANDIR RETURNING %s"% \ repr([x.getname() for x in retval])) return retval diff --git a/offlineimap/ui/TTY.py b/offlineimap/ui/TTY.py index 5fa4dde4b..0b5aa6a89 100644 --- a/offlineimap/ui/TTY.py +++ b/offlineimap/ui/TTY.py @@ -23,7 +23,7 @@ from offlineimap.ui.UIBase import UIBase class TTYFormatter(logging.Formatter): - """Specific Formatter that adds thread information to the log output""" + """Specific Formatter that adds thread information to the log output.""" def __init__(self, *args, **kwargs): #super() doesn't work in py2.6 as 'logging' uses old-style class @@ -31,7 +31,8 @@ def __init__(self, *args, **kwargs): self._last_log_thread = None def format(self, record): - """Override format to add thread information""" + """Override format to add thread information.""" + #super() doesn't work in py2.6 as 'logging' uses old-style class log_str = logging.Formatter.format(self, record) # If msg comes from a different thread than our last, prepend @@ -44,7 +45,7 @@ def format(self, record): self._last_log_thread = t_name log_str = "%s:\n %s" % (t_name, log_str) else: - log_str = " %s" % log_str + log_str = " %s"% log_str return log_str @@ -69,15 +70,15 @@ def setup_consolehandler(self): return ch def isusable(self): - """TTYUI is reported as usable when invoked on a terminal""" + """TTYUI is reported as usable when invoked on a terminal.""" return sys.stdout.isatty() and sys.stdin.isatty() def getpass(self, accountname, config, errmsg=None): - """TTYUI backend is capable of querying the password""" + """TTYUI backend is capable of querying the password.""" if errmsg: - self.warn("%s: %s" % (accountname, errmsg)) + self.warn("%s: %s"% (accountname, errmsg)) self._log_con_handler.acquire() # lock the console output try: return getpass("Enter password for account '%s': " % accountname) @@ -87,7 +88,7 @@ def getpass(self, accountname, config, errmsg=None): def mainException(self): if isinstance(sys.exc_info()[1], KeyboardInterrupt): self.logger.warn("Timer interrupted at user request; program " - "terminating.\n") + "terminating.\n") self.terminate() else: UIBase.mainException(self) @@ -100,12 +101,11 @@ def sleeping(self, sleepsecs, remainingsecs): This implementation in UIBase does not support this, but some implementations return 0 for successful sleep and 1 for an - 'abort', ie a request to sync immediately. - """ + 'abort', ie a request to sync immediately.""" if sleepsecs > 0: if remainingsecs//60 != (remainingsecs-sleepsecs)//60: self.logger.info("Next refresh in %.1f minutes" % ( - remainingsecs/60.0)) + remainingsecs/60.0)) time.sleep(sleepsecs) return 0 diff --git a/offlineimap/ui/UIBase.py b/offlineimap/ui/UIBase.py index f6007da1c..0090291cc 100644 --- a/offlineimap/ui/UIBase.py +++ b/offlineimap/ui/UIBase.py @@ -393,11 +393,11 @@ def settinglabels(self, uid, num, num_to_set, labels, dest): uid, dest, num, num_to_set, ", ".join(labels))) def collectingdata(self, uidlist, source): - if uidlist: - self.logger.info("Collecting data from %d messages on %s" % ( + if uidlist: + self.logger.info("Collecting data from %d messages on %s"% ( len(uidlist), source)) - else: - self.logger.info("Collecting data from messages on %s" % source) + else: + self.logger.info("Collecting data from messages on %s"% source) def serverdiagnostics(self, repository, type): """Connect to repository and output useful information for debugging.""" diff --git a/offlineimap/ui/debuglock.py b/offlineimap/ui/debuglock.py index ef6e8250d..673efb042 100644 --- a/offlineimap/ui/debuglock.py +++ b/offlineimap/ui/debuglock.py @@ -28,7 +28,8 @@ def __init__(self, name): def acquire(self, blocking = 1): self.print_tb("Acquire lock") self.lock.acquire(blocking) - self.logmsg("===== %s: Thread %s acquired lock\n" % (self.name, currentThread().getName())) + self.logmsg("===== %s: Thread %s acquired lock\n"% + (self.name, currentThread().getName())) def release(self): self.print_tb("Release lock") @@ -41,7 +42,7 @@ def logmsg(self, msg): loglock.release() def print_tb(self, msg): - self.logmsg(".... %s: Thread %s attempting to %s\n" % \ + self.logmsg(".... %s: Thread %s attempting to %s\n"% \ (self.name, currentThread().getName(), msg) + \ "\n".join(traceback.format_list(traceback.extract_stack())))