Merge branch 'ia/maildir-keywords' into next
This commit is contained in:
		| @@ -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 | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Nicolas Sebrecht
					Nicolas Sebrecht