Merge branch 'ia/maildir-keywords' into next

This commit is contained in:
Nicolas Sebrecht 2015-11-22 20:06:43 +01:00
commit 5e0f733c3e
7 changed files with 103 additions and 9 deletions

View File

@ -536,6 +536,31 @@ localfolders = ~/Test
# #
#filename_use_mail_timestamp = no #filename_use_mail_timestamp = no
# This option stands in the [Repository LocalExample] section.
#
# Map IMAP [user-defined] keywords to lowercase letters, similar to Dovecot's
# format described in http://wiki2.dovecot.org/MailboxFormat/Maildir . This
# option makes sense for the Maildir type, only.
#
# Configuration example:
# customflag_x = some_keyword
#
# With the configuration example above enabled, all IMAP messages that have
# 'some_keyword' in their FLAGS field will have an 'x' in the flags part of the
# maildir filename:
# 1234567890.M20046P2137.mailserver,S=4542,W=4642:2,Sx
#
# Valid fields are customflag_[a-z], valid values are whatever the IMAP server
# allows.
#
# Comparison in offlineimap is case-sensitive.
#
# This option is EXPERIMENTAL.
#
#customflag_a = some_keyword
#customflag_b = $OtherKeyword
#customflag_c = NonJunk
#customflag_d = ToDo
[Repository GmailLocalExample] [Repository GmailLocalExample]

View File

@ -420,6 +420,11 @@ class BaseFolder(object):
raise NotImplementedError raise NotImplementedError
def getmessagekeywords(self, uid):
"""Returns the keywords for the specified message."""
raise NotImplementedError
def savemessageflags(self, uid, flags): def savemessageflags(self, uid, flags):
"""Sets the specified message's flags to the given set. """Sets the specified message's flags to the given set.
@ -903,6 +908,45 @@ class BaseFolder(object):
return #don't delete messages in dry-run mode return #don't delete messages in dry-run mode
dstfolder.deletemessages(deletelist) dstfolder.deletemessages(deletelist)
def combine_flags_and_keywords(self, uid, dstfolder):
"""Combine the message's flags and keywords using the mapping for the
destination folder."""
# Take a copy of the message flag set, otherwise
# __syncmessagesto_flags() will fail because statusflags is actually a
# reference to selfflags (which it should not, but I don't have time to
# debug THAT).
selfflags = set(self.getmessageflags(uid))
try:
keywordmap = dstfolder.getrepository().getkeywordmap()
if keywordmap is None:
return selfflags
knownkeywords = set(keywordmap.keys())
selfkeywords = self.getmessagekeywords(uid)
if not knownkeywords >= selfkeywords:
#some of the message's keywords are not in the mapping, so
#skip them
skipped_keywords = list(selfkeywords - knownkeywords)
selfkeywords &= knownkeywords
self.ui.warn("Unknown keywords skipped: %s\n"
"You may want to change your configuration to include "
"those\n" % (skipped_keywords))
keywordletterset = set([keywordmap[keyw] for keyw in selfkeywords])
#add the mapped keywords to the list of message flags
selfflags |= keywordletterset
except NotImplementedError:
pass
return selfflags
def __syncmessagesto_flags(self, dstfolder, statusfolder): def __syncmessagesto_flags(self, dstfolder, statusfolder):
"""Pass 3: Flag synchronization. """Pass 3: Flag synchronization.
@ -925,13 +969,13 @@ class BaseFolder(object):
if uid < 0 or not dstfolder.uidexists(uid): if uid < 0 or not dstfolder.uidexists(uid):
continue continue
selfflags = self.getmessageflags(uid)
if statusfolder.uidexists(uid): if statusfolder.uidexists(uid):
statusflags = statusfolder.getmessageflags(uid) statusflags = statusfolder.getmessageflags(uid)
else: else:
statusflags = set() statusflags = set()
selfflags = self.combine_flags_and_keywords(uid, dstfolder)
addflags = selfflags - statusflags addflags = selfflags - statusflags
delflags = statusflags - selfflags delflags = statusflags - selfflags

View File

@ -251,8 +251,10 @@ class IMAPFolder(BaseFolder):
uid = long(options['UID']) uid = long(options['UID'])
self.messagelist[uid] = self.msglist_item_initializer(uid) self.messagelist[uid] = self.msglist_item_initializer(uid)
flags = imaputil.flagsimap2maildir(options['FLAGS']) flags = imaputil.flagsimap2maildir(options['FLAGS'])
keywords = imaputil.flagsimap2keywords(options['FLAGS'])
rtime = imaplibutil.Internaldate2epoch(messagestr) rtime = imaplibutil.Internaldate2epoch(messagestr)
self.messagelist[uid] = {'uid': uid, 'flags': flags, 'time': rtime} self.messagelist[uid] = {'uid': uid, 'flags': flags, 'time': rtime,
'keywords': keywords}
self.ui.messagelistloaded(self.repository, self, self.getmessagecount()) self.ui.messagelistloaded(self.repository, self, self.getmessagecount())
def dropmessagelistcache(self): def dropmessagelistcache(self):
@ -309,6 +311,10 @@ class IMAPFolder(BaseFolder):
def getmessageflags(self, uid): def getmessageflags(self, uid):
return self.messagelist[uid]['flags'] return self.messagelist[uid]['flags']
# Interface from BaseFolder
def getmessagekeywords(self, uid):
return self.messagelist[uid]['keywords']
def __generate_randomheader(self, content): def __generate_randomheader(self, content):
"""Returns a unique X-OfflineIMAP header """Returns a unique X-OfflineIMAP header

View File

@ -135,9 +135,7 @@ class MaildirFolder(BaseFolder):
uid = long(uidmatch.group(1)) uid = long(uidmatch.group(1))
flagmatch = self.re_flagmatch.search(filename) flagmatch = self.re_flagmatch.search(filename)
if flagmatch: if flagmatch:
# Filter out all lowercase (custom maildir) flags. We don't flags = set((c for c in flagmatch.group(1)))
# handle them yet.
flags = set((c for c in flagmatch.group(1) if not c.islower()))
return prefix, uid, fmd5, flags return prefix, uid, fmd5, flags
def _scanfolder(self, min_date=None, min_uid=None): def _scanfolder(self, min_date=None, min_uid=None):
@ -149,7 +147,7 @@ class MaildirFolder(BaseFolder):
with similar UID's (e.g. the UID was reassigned much later). with similar UID's (e.g. the UID was reassigned much later).
Maildir flags are: R (replied) S (seen) T (trashed) D (draft) F Maildir flags are: R (replied) S (seen) T (trashed) D (draft) F
(flagged). (flagged), plus lower-case letters for custom flags.
:returns: dict that can be used as self.messagelist. :returns: dict that can be used as self.messagelist.
""" """
@ -414,8 +412,7 @@ class MaildirFolder(BaseFolder):
if flags != self.messagelist[uid]['flags']: if flags != self.messagelist[uid]['flags']:
# Flags have actually changed, construct new filename Strip # Flags have actually changed, construct new filename Strip
# off existing infostring (possibly discarding small letter # off existing infostring
# flags that dovecot uses TODO)
infomatch = self.re_flagmatch.search(filename) infomatch = self.re_flagmatch.search(filename)
if infomatch: if infomatch:
filename = filename[:-len(infomatch.group())] #strip off filename = filename[:-len(infomatch.group())] #strip off

View File

@ -195,6 +195,14 @@ def flagsimap2maildir(flagstring):
retval.add(maildirflag) retval.add(maildirflag)
return retval return retval
def flagsimap2keywords(flagstring):
"""Convert string '(\\Draft \\Deleted somekeyword otherkeyword)' into a
keyword set (somekeyword otherkeyword)."""
imapflagset = set(flagstring[1:-1].split())
serverflagset = set([flag for (flag, c) in flagmap])
return imapflagset - serverflagset
def flagsmaildir2imap(maildirflaglist): def flagsmaildir2imap(maildirflaglist):
"""Convert set of flags ([DR]) into a string '(\\Deleted \\Draft)'.""" """Convert set of flags ([DR]) into a string '(\\Deleted \\Draft)'."""

View File

@ -133,6 +133,9 @@ class BaseRepository(CustomConfig.ConfigHelperMixin, object):
def getsep(self): def getsep(self):
raise NotImplementedError raise NotImplementedError
def getkeywordmap(self):
raise NotImplementedError
def should_sync_folder(self, fname): def should_sync_folder(self, fname):
"""Should this folder be synced?""" """Should this folder be synced?"""

View File

@ -39,6 +39,14 @@ class MaildirRepository(BaseRepository):
if not os.path.isdir(self.root): if not os.path.isdir(self.root):
os.mkdir(self.root, 0o700) os.mkdir(self.root, 0o700)
# Create the keyword->char mapping
self.keyword2char = dict()
for c in 'abcdefghijklmnopqrstuvwxyz':
confkey = 'customflag_' + c
keyword = self.getconf(confkey, None)
if keyword is not None:
self.keyword2char[keyword] = c
def _append_folder_atimes(self, foldername): def _append_folder_atimes(self, foldername):
"""Store the atimes of a folder's new|cur in self.folder_atimes""" """Store the atimes of a folder's new|cur in self.folder_atimes"""
@ -72,6 +80,9 @@ class MaildirRepository(BaseRepository):
def getsep(self): def getsep(self):
return self.getconf('sep', '.').strip() return self.getconf('sep', '.').strip()
def getkeywordmap(self):
return self.keyword2char if len(self.keyword2char) > 0 else None
def makefolder(self, foldername): def makefolder(self, foldername):
"""Create new Maildir folder if necessary """Create new Maildir folder if necessary