diff --git a/offlineimap.conf b/offlineimap.conf index 3b219d4..7d74df4 100644 --- a/offlineimap.conf +++ b/offlineimap.conf @@ -277,7 +277,14 @@ remoterepository = RemoteExample # #synclabels = no -# Name of the header to use for label storage. +# Name of the header to use for label storage. Format for the header +# value differs for different headers, because there are some de-facto +# standards set by popular clients: +# - X-Label or Keywords keep values separated with spaces; for these +# you, obviously, should not have label values that contain spaces; +# - X-Keywords use comma (',') as the separator. +# To be consistent with the usual To-like headers, for the rest of header +# types we use comma as the separator. # #labelsheader = X-Keywords diff --git a/offlineimap/folder/Gmail.py b/offlineimap/folder/Gmail.py index b735c29..08185a1 100644 --- a/offlineimap/folder/Gmail.py +++ b/offlineimap/folder/Gmail.py @@ -92,8 +92,8 @@ class GmailFolder(IMAPFolder): else: labels = set() labels = labels - self.ignorelabels - labels = ', '.join(sorted(labels)) - body = self.addmessageheader(body, self.labelsheader, labels) + labels_str = imaputil.format_labels_string(self.labelsheader, sorted(labels)) + body = self.addmessageheader(body, self.labelsheader, labels_str) if len(body)>200: dbg_output = "%s...%s" % (str(body)[:150], str(body)[-50:]) @@ -183,11 +183,8 @@ class GmailFolder(IMAPFolder): if not self.synclabels: return super(GmailFolder, self).savemessage(uid, content, flags, rtime) - labels = self.getmessageheader(content, self.labelsheader) - if labels: - labels = set([lb.strip() for lb in labels.split(',') if len(lb.strip()) > 0]) - else: - labels = set() + labels = imaputil.labels_from_header(self.labelsheader, + self.getmessageheader(content, self.labelsheader)) ret = super(GmailFolder, self).savemessage(uid, content, flags, rtime) self.savemessagelabels(ret, labels) diff --git a/offlineimap/folder/GmailMaildir.py b/offlineimap/folder/GmailMaildir.py index 7e903e6..e94dffe 100644 --- a/offlineimap/folder/GmailMaildir.py +++ b/offlineimap/folder/GmailMaildir.py @@ -20,6 +20,7 @@ import os from .Maildir import MaildirFolder from offlineimap import OfflineImapError import offlineimap.accounts +from offlineimap import imaputil class GmailMaildirFolder(MaildirFolder): """Folder implementation to support adding labels to messages in a Maildir. @@ -78,12 +79,10 @@ class GmailMaildirFolder(MaildirFolder): content = file.read() file.close() - labels = self.getmessageheader(content, self.labelsheader) - if labels: - labels = set([lb.strip() for lb in labels.split(',') if len(lb.strip()) > 0]) - else: - labels = set() - self.messagelist[uid]['labels'] = labels + self.messagelist[uid]['labels'] = \ + imaputil.labels_from_header(self.labelsheader, + self.getmessageheader(content, self.labelsheader)) + return self.messagelist[uid]['labels'] @@ -103,11 +102,8 @@ class GmailMaildirFolder(MaildirFolder): if not self.synclabels: return super(GmailMaildirFolder, self).savemessage(uid, content, flags, rtime) - labels = self.getmessageheader(content, self.labelsheader) - if labels: - labels = set([lb.strip() for lb in labels.split(',') if len(lb.strip()) > 0]) - else: - labels = set() + labels = imaputil.labels_from_header(self.labelsheader, + self.getmessageheader(content, self.labelsheader)) ret = super(GmailMaildirFolder, self).savemessage(uid, content, flags, rtime) # Update the mtime and labels @@ -130,12 +126,9 @@ class GmailMaildirFolder(MaildirFolder): content = file.read() file.close() - oldlabels = self.getmessageheader(content, self.labelsheader) + oldlabels = imaputil.labels_from_header(self.labelsheader, + self.getmessageheader(content, self.labelsheader)) - if oldlabels: - oldlabels = set([lb.strip() for lb in oldlabels.split(',') if len(lb.strip()) > 0]) - else: - oldlabels = set() labels = labels - ignorelabels ignoredlabels = oldlabels & ignorelabels @@ -146,13 +139,14 @@ class GmailMaildirFolder(MaildirFolder): return # Change labels into content - labels_str = ', '.join(sorted(labels | ignoredlabels)) + labels_str = imaputil.format_labels_string(self.labelsheader, + sorted(labels | ignoredlabels)) content = self.addmessageheader(content, self.labelsheader, labels_str) rtime = self.messagelist[uid].get('rtime', None) # write file with new labels to a unique file in tmp messagename = self.new_message_filename(uid, set()) - tmpname = self.save_tmp_file(messagename, content) + tmpname = self.save_to_tmp_file(messagename, content) tmppath = os.path.join(self.getfullname(), tmpname) # move to actual location diff --git a/offlineimap/imaputil.py b/offlineimap/imaputil.py index cdf3c4e..c8e3f76 100644 --- a/offlineimap/imaputil.py +++ b/offlineimap/imaputil.py @@ -21,6 +21,12 @@ import string from offlineimap.ui import getglobalui +## Globals + +# Message headers that use space as the separator (for label storage) +SPACE_SEPARATED_LABEL_HEADERS = ('X-Label', 'Keywords') + + def __debug(*args): msg = [] for arg in args: @@ -260,3 +266,69 @@ def __split_quoted(string): rest = rest[next_q + 1:] if not is_escaped: return (quoted, rest.lstrip()) + + +def format_labels_string(header, labels): + """ + Formats labels for embedding into a message, + with format according to header name. + + Headers from SPACE_SEPARATED_LABEL_HEADERS keep space-separated list + of labels, the rest uses comma (',') as the separator. + + Also see parse_labels_string() and modify it accordingly + if logics here gets changed. + + """ + if header in SPACE_SEPARATED_LABEL_HEADERS: + sep = ' ' + else: + sep = ',' + + return sep.join(labels) + + +def parse_labels_string(header, labels_str): + """ + Parses a string into a set of labels, with a format according to + the name of the header. + + See __format_labels_string() for explanation on header handling + and keep these two functions synced with each other. + + TODO: add test to ensure that + format_labels_string * parse_labels_string is unity + and + parse_labels_string * format_labels_string is unity + + """ + + if header in SPACE_SEPARATED_LABEL_HEADERS: + sep = ' ' + else: + sep = ',' + + labels = labels_str.strip().split(sep) + + return set([l.strip() for l in labels if l.strip()]) + + +def labels_from_header(header_name, header_value): + """ + Helper that builds label set from the corresponding header value. + + Arguments: + - header_name: name of the header that keeps labels; + - header_value: value of the said header, can be None + + Returns: set of labels parsed from the header (or empty set). + + """ + + if header_value: + labels = parse_labels_string(header_name, header_value) + else: + labels = set() + + return labels +