From 29e06a60f9d209ab507d6dc6a588c903663d5b50 Mon Sep 17 00:00:00 2001 From: Nicolas Sebrecht Date: Tue, 28 Jun 2016 23:58:03 +0200 Subject: [PATCH] learn to not download UIDs defined by the user Allow users to workaround offending emails that offlineimap can't download. Signed-off-by: Nicolas Sebrecht --- offlineimap.conf | 11 +++++++++++ offlineimap/folder/Base.py | 8 ++++++++ offlineimap/folder/IMAP.py | 4 ++++ offlineimap/repository/IMAP.py | 15 +++++++++++++++ offlineimap/ui/Curses.py | 6 +++++- offlineimap/ui/Machine.py | 14 ++++++++++---- offlineimap/ui/Noninteractive.py | 8 +++++--- offlineimap/ui/UIBase.py | 13 ++++++++++--- 8 files changed, 68 insertions(+), 11 deletions(-) diff --git a/offlineimap.conf b/offlineimap.conf index e2e6115..14d14df 100644 --- a/offlineimap.conf +++ b/offlineimap.conf @@ -1187,6 +1187,17 @@ remoteuser = u"username" #"cvlc --play-and-stop --play-and-exit /path/to/sound/file.mp3 > /dev/null 2>&1") +# This option stands in the [Repository RemoteExample] section. +# +# If offlineiamp is having troubles to download some UIDS, it's possible to get +# them ignored in a list. +# +# The function must return the list of UIDs to ignore, None otherwise. It is +# passed the folder name. +# +#copy_ignore_eval = lambda foldername: {'INBOX': [2, 3, 4]}.get(foldername) + + [Repository GmailExample] # A repository using Gmail's IMAP interface. diff --git a/offlineimap/folder/Base.py b/offlineimap/folder/Base.py index 98f78f8..e467f23 100644 --- a/offlineimap/folder/Base.py +++ b/offlineimap/folder/Base.py @@ -45,6 +45,7 @@ class BaseFolder(object): if self.name == 'INBOX': self.newmail_hook = repository.newmail_hook self.have_newmail = False + self.copy_ignoreUIDs = None # List of UIDs to ignore. self.repository = repository self.visiblename = repository.nametrans(name) # In case the visiblename becomes '.' or '/' (top-level) we use @@ -870,6 +871,13 @@ class BaseFolder(object): if not statusfolder.uidexists(uid)] num_to_copy = len(copylist) + # Honor 'copy_ignore_eval' configuration option. + if self.copy_ignoreUIDs is not None: + for uid in self.copy_ignoreUIDs: + if uid in copylist: + copylist.remove(uid) + self.ui.ignorecopyingmessage(uid, self, dstfolder) + if num_to_copy > 0 and self.repository.account.dryrun: self.ui.info( "[DRYRUN] Copy {0} messages from {1}[{2}] to {3}".format( diff --git a/offlineimap/folder/IMAP.py b/offlineimap/folder/IMAP.py index e5431f5..bb5c851 100644 --- a/offlineimap/folder/IMAP.py +++ b/offlineimap/folder/IMAP.py @@ -58,6 +58,10 @@ class IMAPFolder(BaseFolder): fh_conf = self.repository.account.getconf('filterheaders', '') self.filterheaders = [h for h in re.split(r'\s*,\s*', fh_conf) if h] + # self.copy_ignoreUIDs is used by BaseFolder. + self.copy_ignoreUIDs = repository.get_copy_ignore_UIDs( + self.getvisiblename()) + def __selectro(self, imapobj, force=False): """Select this folder when we do not need write access. diff --git a/offlineimap/repository/IMAP.py b/offlineimap/repository/IMAP.py index 5057a79..d5db185 100644 --- a/offlineimap/repository/IMAP.py +++ b/offlineimap/repository/IMAP.py @@ -40,6 +40,8 @@ class IMAPRepository(BaseRepository): self._oauth2_request_url = None self.imapserver = imapserver.IMAPServer(self) self.folders = None + self.copy_ignore_eval = None + # Only set the newmail_hook in an IMAP repository. if self.config.has_option(self.getsection(), 'newmail_hook'): self.newmail_hook = self.localeval.eval( @@ -75,6 +77,19 @@ class IMAPRepository(BaseRepository): def dropconnections(self): self.imapserver.close() + def get_copy_ignore_UIDs(self, foldername): + """Return a list of UIDs to not copy for this foldername.""" + + if self.copy_ignore_eval is None: + if self.config.has_option(self.getsection(), + 'copy_ignore_eval'): + self.copy_ignore_eval = self.localeval.eval( + self.getconf('copy_ignore_eval')) + else: + self.copy_ignore_eval = lambda x: None + + return self.copy_ignore_eval(foldername) + def getholdconnectionopen(self): if self.getidlefolders(): return 1 diff --git a/offlineimap/ui/Curses.py b/offlineimap/ui/Curses.py index d5b148d..dfefb7d 100644 --- a/offlineimap/ui/Curses.py +++ b/offlineimap/ui/Curses.py @@ -1,5 +1,5 @@ # Curses-based interfaces -# Copyright (C) 2003-2015 John Goerzen & contributors +# Copyright (C) 2003-2016 John Goerzen & contributors. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -442,6 +442,10 @@ class Blinkenlights(UIBase, CursesUtil): self.gettf().setcolor('blue') super(Blinkenlights, self).syncingmessages(*args) + def ignorecopyingmessage(self, *args): + self.gettf().setcolor('red') + super(Blinkenlights, self).ignorecopyingmessage(*args) + def copyingmessage(self, *args): self.gettf().setcolor('orange') super(Blinkenlights, self).copyingmessage(*args) diff --git a/offlineimap/ui/Machine.py b/offlineimap/ui/Machine.py index dc650c3..06de17b 100644 --- a/offlineimap/ui/Machine.py +++ b/offlineimap/ui/Machine.py @@ -1,4 +1,4 @@ -# Copyright (C) 2007-2015 John Goerzen & contributors +# Copyright (C) 2007-2016 John Goerzen & contributors. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -21,10 +21,11 @@ import sys import time import logging from threading import currentThread -from offlineimap.ui.UIBase import UIBase -import offlineimap -protocol = '7.0.0' +import offlineimap +from offlineimap.ui.UIBase import UIBase + +protocol = '7.1.0' class MachineLogFormatter(logging.Formatter): """urlencodes any outputted line, to avoid multi-line output""" @@ -125,6 +126,11 @@ class MachineUI(UIBase): (s.getnicename(sr), sf.getname(), s.getnicename(dr), df.getname())) + def ignorecopyingmessage(s, uid, srcfolder, destfolder): + s._printData(s.logger.info, 'ignorecopyingmessage', "%d\n%s\n%s\n%s[%s]"% + (uid, s.getnicename(srcfolder), srcfolder.getname(), + s.getnicename(destfolder), destfolder)) + def copyingmessage(s, uid, num, num_to_copy, srcfolder, destfolder): s._printData(s.logger.info, 'copyingmessage', "%d\n%s\n%s\n%s[%s]"% (uid, s.getnicename(srcfolder), srcfolder.getname(), diff --git a/offlineimap/ui/Noninteractive.py b/offlineimap/ui/Noninteractive.py index 0e23a93..e7ef096 100644 --- a/offlineimap/ui/Noninteractive.py +++ b/offlineimap/ui/Noninteractive.py @@ -1,5 +1,5 @@ # Noninteractive UI -# Copyright (C) 2002-2012 John Goerzen & contributors +# Copyright (C) 2002-2016 John Goerzen & contributors. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -16,11 +16,13 @@ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import logging -from offlineimap.ui.UIBase import UIBase + import offlineimap +from offlineimap.ui.UIBase import UIBase class Basic(UIBase): - """'Basic' simply sets log level to INFO""" + """'Basic' simply sets log level to INFO.""" + def __init__(self, config, loglevel = logging.INFO): return super(Basic, self).__init__(config, loglevel) diff --git a/offlineimap/ui/UIBase.py b/offlineimap/ui/UIBase.py index 769a008..2aa7ca8 100644 --- a/offlineimap/ui/UIBase.py +++ b/offlineimap/ui/UIBase.py @@ -1,5 +1,5 @@ # UI base class -# Copyright (C) 2002-2016 John Goerzen & contributors +# Copyright (C) 2002-2016 John Goerzen & contributors. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -24,11 +24,12 @@ import traceback import threading try: from Queue import Queue -except ImportError: #python3 +except ImportError: # python3 from queue import Queue from collections import deque -from offlineimap.error import OfflineImapError + import offlineimap +from offlineimap.error import OfflineImapError debugtypes = {'':'Other offlineimap related sync messages', 'imap': 'IMAP protocol debugging', @@ -385,6 +386,12 @@ class UIBase(object): self.getnicename(sr), srcfolder, self.getnicename(dr), dstfolder)) + def ignorecopyingmessage(self, uid, src, destfolder): + """Output a log line stating which message is ignored.""" + + self.logger.info("IGNORED: Copy message UID %s %s:%s -> %s"% ( + uid, src.repository, src, destfolder.repository)) + def copyingmessage(self, uid, num, num_to_copy, src, destfolder): """Output a log line stating which message we copy."""