Merge branch 'ia/maildir-keywords' into next
This commit is contained in:
commit
5e0f733c3e
@ -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]
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)'."""
|
||||||
|
|
||||||
|
@ -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?"""
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user