diff --git a/offlineimap.conf b/offlineimap.conf
index 4351d0d..ce6d17b 100644
--- a/offlineimap.conf
+++ b/offlineimap.conf
@@ -159,6 +159,16 @@ remoterepository = RemoteExample
# autorefresh = 5
+# You can tell offlineimap to do a number of quicker synchronizations
+# between full updates. A quick synchronization only synchronizes
+# if a Maildir folder has changed, or if an IMAP folder has received
+# new messages or had messages deleted. It does not update if the
+# only changes were to IMAP flags. Specify 0 to never do quick updates,
+# -1 to always do quick updates, or a positive integer to do that many
+# quick updates between each full synchronization (requires autorefresh).
+
+# quick = 10
+
[Repository LocalExample]
# This is one of the two repositories that you'll work with given the
diff --git a/offlineimap.sgml b/offlineimap.sgml
index 6fc8d52..5b5d148 100644
--- a/offlineimap.sgml
+++ b/offlineimap.sgml
@@ -389,6 +389,11 @@ cd offlineimap-x.y.z
file.
+ -q
+ Run only quick synchronizations. Ignore any flag
+ updates on IMAP servers.
+
+
-h --help
Show summary of options.
diff --git a/offlineimap/accounts.py b/offlineimap/accounts.py
index d05928c..9f224d0 100644
--- a/offlineimap/accounts.py
+++ b/offlineimap/accounts.py
@@ -45,6 +45,7 @@ class Account(CustomConfig.ConfigHelperMixin):
self.localeval = config.getlocaleval()
self.ui = UIBase.getglobalui()
self.refreshperiod = self.getconffloat('autorefresh', 0.0)
+ self.quicknum = 0
if self.refreshperiod == 0.0:
self.refreshperiod = None
@@ -125,6 +126,20 @@ class AccountSynchronizationMixin:
def sync(self):
# We don't need an account lock because syncitall() goes through
# each account once, then waits for all to finish.
+
+ quickconfig = self.getconfint('quick', 0)
+ if quickconfig < 0:
+ quick = True
+ elif quickconfig > 0:
+ if self.quicknum == 0 or self.quicknum > quickconfig:
+ self.quicknum = 1
+ quick = False
+ else:
+ self.quicknum = self.quicknum + 1
+ quick = True
+ else:
+ quick = False
+
try:
remoterepos = self.remoterepos
localrepos = self.localrepos
@@ -140,7 +155,7 @@ class AccountSynchronizationMixin:
name = "Folder sync %s[%s]" % \
(self.name, remotefolder.getvisiblename()),
args = (self.name, remoterepos, remotefolder, localrepos,
- statusrepos))
+ statusrepos, quick))
thread.setDaemon(1)
thread.start()
folderthreads.append(thread)
@@ -157,7 +172,7 @@ class SyncableAccount(Account, AccountSynchronizationMixin):
pass
def syncfolder(accountname, remoterepos, remotefolder, localrepos,
- statusrepos):
+ statusrepos, quick):
global mailboxes
ui = UIBase.getglobalui()
ui.registerthread(accountname)
@@ -167,12 +182,6 @@ def syncfolder(accountname, remoterepos, remotefolder, localrepos,
replace(remoterepos.getsep(), localrepos.getsep()))
# Write the mailboxes
mbnames.add(accountname, localfolder.getvisiblename())
- # Load local folder
- ui.syncingfolder(remoterepos, remotefolder, localrepos, localfolder)
- ui.loadmessagelist(localrepos, localfolder)
- localfolder.cachemessagelist()
- ui.messagelistloaded(localrepos, localfolder, len(localfolder.getmessagelist().keys()))
-
# Load status folder.
statusfolder = statusrepos.getfolder(remotefolder.getvisiblename().\
@@ -185,6 +194,19 @@ def syncfolder(accountname, remoterepos, remotefolder, localrepos,
statusfolder.cachemessagelist()
+ if quick:
+ if not localfolder.quickchanged(statusfolder) \
+ and not remotefolder.quickchanged(statusfolder):
+ ui.skippingfolder(remotefolder)
+ localrepos.restore_atime()
+ return
+
+ # Load local folder
+ ui.syncingfolder(remoterepos, remotefolder, localrepos, localfolder)
+ ui.loadmessagelist(localrepos, localfolder)
+ localfolder.cachemessagelist()
+ ui.messagelistloaded(localrepos, localfolder, len(localfolder.getmessagelist().keys()))
+
# If either the local or the status folder has messages and there is a UID
# validity problem, warn and abort. If there are no messages, UW IMAPd
# loses UIDVALIDITY. But we don't really need it if both local folders are
diff --git a/offlineimap/folder/IMAP.py b/offlineimap/folder/IMAP.py
index 8772c12..12a9660 100644
--- a/offlineimap/folder/IMAP.py
+++ b/offlineimap/folder/IMAP.py
@@ -41,6 +41,16 @@ class IMAPFolder(BaseFolder):
self.randomgenerator = random.Random()
BaseFolder.__init__(self)
+ def selectro(self, imapobj):
+ """Select this folder when we do not need write access.
+ Prefer SELECT to EXAMINE if we can, since some servers
+ (Courier) do not stabilize UID validity until the folder is
+ selected."""
+ try:
+ imapobj.select(self.getfullname())
+ except imapobj.readonly:
+ imapobj.select(self.getfullname(), readonly = 1)
+
def getaccountname(self):
return self.accountname
@@ -60,11 +70,52 @@ class IMAPFolder(BaseFolder):
imapobj = self.imapserver.acquireconnection()
try:
# Primes untagged_responses
- imapobj.select(self.getfullname(), readonly = 1)
+ self.selectro(imapobj)
return long(imapobj.untagged_responses['UIDVALIDITY'][0])
finally:
self.imapserver.releaseconnection(imapobj)
+ def quickchanged(self, statusfolder):
+ # An IMAP folder has definitely changed if the number of
+ # messages or the UID of the last message have changed. Otherwise
+ # only flag changes could have occurred.
+ imapobj = self.imapserver.acquireconnection()
+ try:
+ # Primes untagged_responses
+ imapobj.select(self.getfullname(), readonly = 1, force = 1)
+ try:
+ # Some mail servers do not return an EXISTS response if
+ # the folder is empty.
+ maxmsgid = long(imapobj.untagged_responses['EXISTS'][0])
+ except KeyError:
+ return True
+
+ # Different number of messages than last time?
+ if maxmsgid != len(statusfolder.getmessagelist()):
+ return True
+
+ if maxmsgid < 1:
+ # No messages; return
+ return False
+
+ # Now, get the UID for the last message.
+ response = imapobj.fetch('%d' % maxmsgid, '(UID)')[1]
+ finally:
+ self.imapserver.releaseconnection(imapobj)
+
+ # Discard the message number.
+ messagestr = string.split(response[0], maxsplit = 1)[1]
+ options = imaputil.flags2hash(messagestr)
+ if not options.has_key('UID'):
+ return True
+ uid = long(options['UID'])
+ saveduids = statusfolder.getmessagelist().keys()
+ saveduids.sort()
+ if uid != saveduids[-1]:
+ return True
+
+ return False
+
def cachemessagelist(self):
imapobj = self.imapserver.acquireconnection()
self.messagelist = {}
diff --git a/offlineimap/folder/Maildir.py b/offlineimap/folder/Maildir.py
index 643f57a..56a63e4 100644
--- a/offlineimap/folder/Maildir.py
+++ b/offlineimap/folder/Maildir.py
@@ -22,7 +22,6 @@ from offlineimap.ui import UIBase
from threading import Lock
import os.path, os, re, time, socket, md5
-foldermatchre = re.compile(',FMD5=([0-9a-f]{32})')
uidmatchre = re.compile(',U=(\d+)')
flagmatchre = re.compile(':.*2,([A-Z]+)')
@@ -78,16 +77,16 @@ class MaildirFolder(BaseFolder):
files = []
nouidcounter = -1 # Messages without UIDs get
# negative UID numbers.
+ foldermd5 = md5.new(self.getvisiblename()).hexdigest()
+ folderstr = ',FMD5=' + foldermd5
for dirannex in ['new', 'cur']:
fulldirname = os.path.join(self.getfullname(), dirannex)
files.extend([os.path.join(fulldirname, filename) for
filename in os.listdir(fulldirname)])
for file in files:
messagename = os.path.basename(file)
- foldermatch = foldermatchre.search(messagename)
- if (not foldermatch) or \
- md5.new(self.getvisiblename()).hexdigest() \
- != foldermatch.group(1):
+ foldermatch = messagename.find(folderstr) != -1
+ if not foldermatch:
# If there is no folder MD5 specified, or if it mismatches,
# assume it is a foreign (new) message and generate a
# negative uid for it
@@ -111,8 +110,21 @@ class MaildirFolder(BaseFolder):
'filename': file}
return retval
+ def quickchanged(self, statusfolder):
+ self.cachemessagelist()
+ savedmessages = statusfolder.getmessagelist()
+ if len(self.messagelist) != len(savedmessages):
+ return True
+ for uid in self.messagelist.keys():
+ if uid not in savedmessages:
+ return True
+ if self.messagelist[uid]['flags'] != savedmessages[uid]['flags']:
+ return True
+ return False
+
def cachemessagelist(self):
- self.messagelist = self._scanfolder()
+ if self.messagelist is None:
+ self.messagelist = self._scanfolder()
def getmessagelist(self):
return self.messagelist
diff --git a/offlineimap/init.py b/offlineimap/init.py
index 480778f..9824f05 100644
--- a/offlineimap/init.py
+++ b/offlineimap/init.py
@@ -53,7 +53,7 @@ def startup(versionno):
sys.stdout.write(version.getcmdhelp() + "\n")
sys.exit(0)
- for optlist in getopt(sys.argv[1:], 'P:1oa:c:d:l:u:h')[0]:
+ for optlist in getopt(sys.argv[1:], 'P:1oqa:c:d:l:u:h')[0]:
options[optlist[0]] = optlist[1]
if options.has_key('-h'):
@@ -100,6 +100,10 @@ def startup(versionno):
for section in accounts.getaccountlist(config):
config.remove_option('Account ' + section, "autorefresh")
+ if options.has_key('-q'):
+ for section in accounts.getaccountlist(config):
+ config.set('Account ' + section, "quick", '-1')
+
lock(config, ui)
try:
diff --git a/offlineimap/ui/Blinkenlights.py b/offlineimap/ui/Blinkenlights.py
index 717e81b..6982351 100644
--- a/offlineimap/ui/Blinkenlights.py
+++ b/offlineimap/ui/Blinkenlights.py
@@ -42,6 +42,10 @@ class BlinkenBase:
s.gettf().setcolor('cyan')
s.__class__.__bases__[-1].syncingfolder(s, srcrepos, srcfolder, destrepos, destfolder)
+ def skippingfolder(s, folder):
+ s.gettf().setcolor('cyan')
+ s.__class__.__bases__[-1].skippingfolder(s, folder)
+
def loadmessagelist(s, repos, folder):
s.gettf().setcolor('green')
s._msg("Scanning folder [%s/%s]" % (s.getnicename(repos),
@@ -71,6 +75,13 @@ class BlinkenBase:
s.gettf().setcolor('pink')
s.__class__.__bases__[-1].deletingflags(s, uidlist, flags, destlist)
+ def warn(s, msg, minor = 0):
+ if minor:
+ s.gettf().setcolor('pink')
+ else:
+ s.gettf().setcolor('red')
+ s.__class__.__bases__[-1].warn(s, msg, minor)
+
def init_banner(s):
s.availablethreadframes = {}
s.threadframes = {}
diff --git a/offlineimap/ui/Curses.py b/offlineimap/ui/Curses.py
index 4768ea0..4cf9066 100644
--- a/offlineimap/ui/Curses.py
+++ b/offlineimap/ui/Curses.py
@@ -511,6 +511,8 @@ class Blinkenlights(BlinkenBase, UIBase):
return
if color:
s.gettf().setcolor(color)
+ elif s.gettf().getcolor() == 'black':
+ s.gettf().setcolor('gray')
s._addline(msg, s.gettf().getcolorpair())
s.logwindow.refresh()
finally:
diff --git a/offlineimap/ui/UIBase.py b/offlineimap/ui/UIBase.py
index 2f7b634..d0e92e6 100644
--- a/offlineimap/ui/UIBase.py
+++ b/offlineimap/ui/UIBase.py
@@ -208,6 +208,11 @@ class UIBase:
s.getnicename(srcrepos),
s.getnicename(destrepos)))
+ def skippingfolder(s, folder):
+ """Called when a folder sync operation is started."""
+ if s.verbose >= 0:
+ s._msg("Skipping %s (not changed)" % folder.getname())
+
def validityproblem(s, folder):
s.warn("UID validity problem for folder %s (repo %s) (saved %d; got %d); skipping it" % \
(folder.getname(), folder.getrepository().getname(),
diff --git a/offlineimap/version.py b/offlineimap/version.py
index 1a1a1eb..33620f7 100644
--- a/offlineimap/version.py
+++ b/offlineimap/version.py
@@ -43,7 +43,7 @@ def getcmdhelp():
return """
offlineimap [ -1 ] [ -P profiledir ] [ -a accountlist ] [
-c configfile ] [ -d debugtype[,debugtype...] ] [ -o ] [
- -u interface ]
+ -u interface ] [ -q ]
offlineimap -h | --help
@@ -97,6 +97,9 @@ def getcmdhelp():
-o Run only once, ignoring any autorefresh setting in
the config file.
+ -q Run only quick synchronizations. Ignore any flag
+ updates on IMAP servers.
+
-h, --help
Show summary of options.