2a5ef8c2ef
This fixes a bug in which a message ended up with multiple gmail labels header (X-Keywords or so). Fix fix it by removing all labels headers before adding the updated one. Signed-off-by: Abdo Roig-Maranges <abdo.roig@gmail.com>
326 lines
13 KiB
Python
326 lines
13 KiB
Python
# Maildir folder support with labels
|
|
# Copyright (C) 2002 - 2011 John Goerzen & contributors
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
|
|
|
|
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.
|
|
"""
|
|
def __init__(self, root, name, sep, repository):
|
|
super(GmailMaildirFolder, self).__init__(root, name, sep, repository)
|
|
|
|
# The header under which labels are stored
|
|
self.labelsheader = self.repository.account.getconf('labelsheader', 'X-Keywords')
|
|
|
|
# enables / disables label sync
|
|
self.synclabels = self.repository.account.getconfboolean('synclabels', 0)
|
|
|
|
# if synclabels is enabled, add a 4th pass to sync labels
|
|
if self.synclabels:
|
|
self.syncmessagesto_passes.append(('syncing labels', self.syncmessagesto_labels))
|
|
|
|
def quickchanged(self, statusfolder):
|
|
"""Returns True if the Maildir has changed. Checks uids, flags and mtimes"""
|
|
|
|
self.cachemessagelist()
|
|
# Folder has different uids than statusfolder => TRUE
|
|
if sorted(self.getmessageuidlist()) != \
|
|
sorted(statusfolder.getmessageuidlist()):
|
|
return True
|
|
# check for flag changes, it's quick on a Maildir
|
|
for (uid, message) in self.getmessagelist().iteritems():
|
|
if message['flags'] != statusfolder.getmessageflags(uid):
|
|
return True
|
|
# check for newer mtimes. it is also fast
|
|
for (uid, message) in self.getmessagelist().iteritems():
|
|
if message['mtime'] > statusfolder.getmessagemtime(uid):
|
|
return True
|
|
return False #Nope, nothing changed
|
|
|
|
|
|
# Interface from BaseFolder
|
|
def msglist_item_initializer(self, uid):
|
|
return {'flags': set(), 'labels': set(), 'labels_cached': False,
|
|
'filename': '/no-dir/no-such-file/', 'mtime': 0}
|
|
|
|
|
|
def cachemessagelist(self):
|
|
if self.messagelist is None:
|
|
self.messagelist = self._scanfolder()
|
|
|
|
# Get mtimes
|
|
if self.synclabels:
|
|
for uid, msg in self.messagelist.items():
|
|
filepath = os.path.join(self.getfullname(), msg['filename'])
|
|
msg['mtime'] = long(os.stat(filepath).st_mtime)
|
|
|
|
|
|
def getmessagelabels(self, uid):
|
|
# Labels are not cached in cachemessagelist because it is too slow.
|
|
if not self.messagelist[uid]['labels_cached']:
|
|
filename = self.messagelist[uid]['filename']
|
|
filepath = os.path.join(self.getfullname(), filename)
|
|
|
|
if not os.path.exists(filepath):
|
|
return set()
|
|
|
|
file = open(filepath, 'rt')
|
|
content = file.read()
|
|
file.close()
|
|
|
|
self.messagelist[uid]['labels'] = set()
|
|
for hstr in self.getmessageheaderlist(content, self.labelsheader):
|
|
self.messagelist[uid]['labels'].update(
|
|
imaputil.labels_from_header(self.labelsheader, hstr))
|
|
self.messagelist[uid]['labels_cached'] = True
|
|
|
|
return self.messagelist[uid]['labels']
|
|
|
|
|
|
def getmessagemtime(self, uid):
|
|
if not 'mtime' in self.messagelist[uid]:
|
|
return 0
|
|
else:
|
|
return self.messagelist[uid]['mtime']
|
|
|
|
def savemessage(self, uid, content, flags, rtime):
|
|
"""Writes a new message, with the specified uid.
|
|
|
|
See folder/Base for detail. Note that savemessage() does not
|
|
check against dryrun settings, so you need to ensure that
|
|
savemessage is never called in a dryrun mode."""
|
|
|
|
if not self.synclabels:
|
|
return super(GmailMaildirFolder, self).savemessage(uid, content, flags, rtime)
|
|
|
|
labels = set()
|
|
for hstr in self.getmessageheaderlist(content, self.labelsheader):
|
|
labels.update(imaputil.labels_from_header(self.labelsheader, hstr))
|
|
|
|
ret = super(GmailMaildirFolder, self).savemessage(uid, content, flags, rtime)
|
|
|
|
# Update the mtime and labels
|
|
filename = self.messagelist[uid]['filename']
|
|
filepath = os.path.join(self.getfullname(), filename)
|
|
self.messagelist[uid]['mtime'] = long(os.stat(filepath).st_mtime)
|
|
self.messagelist[uid]['labels'] = labels
|
|
return ret
|
|
|
|
def savemessagelabels(self, uid, labels, ignorelabels=set()):
|
|
"""Change a message's labels to `labels`.
|
|
|
|
Note that this function does not check against dryrun settings,
|
|
so you need to ensure that it is never called in a dryrun mode."""
|
|
|
|
filename = self.messagelist[uid]['filename']
|
|
filepath = os.path.join(self.getfullname(), filename)
|
|
|
|
file = open(filepath, 'rt')
|
|
content = file.read()
|
|
file.close()
|
|
|
|
oldlabels = set()
|
|
for hstr in self.getmessageheaderlist(content, self.labelsheader):
|
|
oldlabels.update(imaputil.labels_from_header(self.labelsheader, hstr))
|
|
|
|
labels = labels - ignorelabels
|
|
ignoredlabels = oldlabels & ignorelabels
|
|
oldlabels = oldlabels - ignorelabels
|
|
|
|
# Nothing to change
|
|
if labels == oldlabels:
|
|
return
|
|
|
|
# Change labels into content
|
|
labels_str = imaputil.format_labels_string(self.labelsheader,
|
|
sorted(labels | ignoredlabels))
|
|
|
|
# First remove old labels header, and then add the new one
|
|
content = self.deletemessageheaders(content, self.labelsheader)
|
|
content = self.addmessageheader(content, '\n', 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_to_tmp_file(messagename, content)
|
|
tmppath = os.path.join(self.getfullname(), tmpname)
|
|
|
|
# move to actual location
|
|
try:
|
|
os.rename(tmppath, filepath)
|
|
except OSError as e:
|
|
raise OfflineImapError("Can't rename file '%s' to '%s': %s" % \
|
|
(tmppath, filepath, e[1]), OfflineImapError.ERROR.FOLDER)
|
|
|
|
if rtime != None:
|
|
os.utime(filepath, (rtime, rtime))
|
|
|
|
# save the new mtime and labels
|
|
self.messagelist[uid]['mtime'] = long(os.stat(filepath).st_mtime)
|
|
self.messagelist[uid]['labels'] = labels
|
|
|
|
def copymessageto(self, uid, dstfolder, statusfolder, register = 1):
|
|
"""Copies a message from self to dst if needed, updating the status
|
|
|
|
Note that this function does not check against dryrun settings,
|
|
so you need to ensure that it is never called in a
|
|
dryrun mode.
|
|
|
|
:param uid: uid of the message to be copied.
|
|
:param dstfolder: A BaseFolder-derived instance
|
|
:param statusfolder: A LocalStatusFolder instance
|
|
:param register: whether we should register a new thread."
|
|
:returns: Nothing on success, or raises an Exception."""
|
|
|
|
# Check if we are really copying
|
|
realcopy = uid > 0 and not dstfolder.uidexists(uid)
|
|
|
|
# first copy the message
|
|
super(GmailMaildirFolder, self).copymessageto(uid, dstfolder, statusfolder, register)
|
|
|
|
# sync labels and mtime now when the message is new (the embedded labels are up to date,
|
|
# and have already propagated to the remote server.
|
|
# for message which already existed on the remote, this is useless, as later the labels may
|
|
# get updated.
|
|
if realcopy and self.synclabels:
|
|
try:
|
|
labels = dstfolder.getmessagelabels(uid)
|
|
statusfolder.savemessagelabels(uid, labels, mtime=self.getmessagemtime(uid))
|
|
|
|
# dstfolder is not GmailMaildir.
|
|
except NotImplementedError:
|
|
return
|
|
|
|
def syncmessagesto_labels(self, dstfolder, statusfolder):
|
|
"""Pass 4: Label Synchronization (Gmail only)
|
|
|
|
Compare label mismatches in self with those in statusfolder. If
|
|
msg has a valid UID and exists on dstfolder (has not e.g. been
|
|
deleted there), sync the labels change to both dstfolder and
|
|
statusfolder.
|
|
|
|
Also skips messages whose mtime remains the same as statusfolder, as the
|
|
contents have not changed.
|
|
|
|
This function checks and protects us from action in ryrun mode.
|
|
"""
|
|
# For each label, we store a list of uids to which it should be
|
|
# added. Then, we can call addmessageslabels() to apply them in
|
|
# bulk, rather than one call per message.
|
|
addlabellist = {}
|
|
dellabellist = {}
|
|
uidlist = []
|
|
|
|
try:
|
|
# filter uids (fast)
|
|
for uid in self.getmessageuidlist():
|
|
# bail out on CTRL-C or SIGTERM
|
|
if offlineimap.accounts.Account.abort_NOW_signal.is_set():
|
|
break
|
|
|
|
# Ignore messages with negative UIDs missed by pass 1 and
|
|
# don't do anything if the message has been deleted remotely
|
|
if uid < 0 or not dstfolder.uidexists(uid):
|
|
continue
|
|
|
|
selfmtime = self.getmessagemtime(uid)
|
|
|
|
if statusfolder.uidexists(uid):
|
|
statusmtime = statusfolder.getmessagemtime(uid)
|
|
else:
|
|
statusmtime = 0
|
|
|
|
if selfmtime > statusmtime:
|
|
uidlist.append(uid)
|
|
|
|
|
|
self.ui.collectingdata(uidlist, self)
|
|
# This can be slow if there is a lot of modified files
|
|
for uid in uidlist:
|
|
# bail out on CTRL-C or SIGTERM
|
|
if offlineimap.accounts.Account.abort_NOW_signal.is_set():
|
|
break
|
|
|
|
selflabels = self.getmessagelabels(uid)
|
|
|
|
if statusfolder.uidexists(uid):
|
|
statuslabels = statusfolder.getmessagelabels(uid)
|
|
else:
|
|
statuslabels = set()
|
|
|
|
addlabels = selflabels - statuslabels
|
|
dellabels = statuslabels - selflabels
|
|
|
|
for lb in addlabels:
|
|
if not lb in addlabellist:
|
|
addlabellist[lb] = []
|
|
addlabellist[lb].append(uid)
|
|
|
|
for lb in dellabels:
|
|
if not lb in dellabellist:
|
|
dellabellist[lb] = []
|
|
dellabellist[lb].append(uid)
|
|
|
|
for lb, uids in addlabellist.items():
|
|
# bail out on CTRL-C or SIGTERM
|
|
if offlineimap.accounts.Account.abort_NOW_signal.is_set():
|
|
break
|
|
|
|
self.ui.addinglabels(uids, lb, dstfolder)
|
|
if self.repository.account.dryrun:
|
|
continue #don't actually add in a dryrun
|
|
dstfolder.addmessageslabels(uids, set([lb]))
|
|
statusfolder.addmessageslabels(uids, set([lb]))
|
|
|
|
for lb, uids in dellabellist.items():
|
|
# bail out on CTRL-C or SIGTERM
|
|
if offlineimap.accounts.Account.abort_NOW_signal.is_set():
|
|
break
|
|
|
|
self.ui.deletinglabels(uids, lb, dstfolder)
|
|
if self.repository.account.dryrun:
|
|
continue #don't actually remove in a dryrun
|
|
dstfolder.deletemessageslabels(uids, set([lb]))
|
|
statusfolder.deletemessageslabels(uids, set([lb]))
|
|
|
|
# Update mtimes on StatusFolder. It is done last to be safe. If something els fails
|
|
# and the mtime is not updated, the labels will still be synced next time.
|
|
mtimes = {}
|
|
for uid in uidlist:
|
|
# bail out on CTRL-C or SIGTERM
|
|
if offlineimap.accounts.Account.abort_NOW_signal.is_set():
|
|
break
|
|
|
|
if self.repository.account.dryrun:
|
|
continue #don't actually update statusfolder
|
|
|
|
filename = self.messagelist[uid]['filename']
|
|
filepath = os.path.join(self.getfullname(), filename)
|
|
mtimes[uid] = long(os.stat(filepath).st_mtime)
|
|
|
|
# finally update statusfolder in a single DB transaction
|
|
statusfolder.savemessagesmtimebulk(mtimes)
|
|
|
|
except NotImplementedError:
|
|
self.ui.warn("Can't sync labels. You need to configure a remote repository of type Gmail.")
|