123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344 |
- # Maildir folder support with labels
- # Copyright (C) 2002-2016 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 sys import exc_info
- import offlineimap.accounts
- from offlineimap import OfflineImapError
- from offlineimap import imaputil
- from .Maildir import MaildirFolder
- 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(self.syncmessagesto_labels)
- def quickchanged(self, statusfolder):
- """Returns True if the Maildir has changed.
- Checks uids, flags and mtimes"""
- if self._utime_from_header is True:
- raise Exception("GmailMaildir does not support quick mode"
- " when 'utime_from_header' is enabled.")
- 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 list(self.getmessagelist().items()):
- if message['flags'] != statusfolder.getmessageflags(uid):
- return True
- # check for newer mtimes. it is also fast
- for (uid, message) in list(self.getmessagelist().items()):
- 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, min_date=None, min_uid=None):
- if self.ismessagelistempty():
- self.messagelist = self._scanfolder(min_date=min_date,
- min_uid=min_uid)
- # Get mtimes
- if self.synclabels:
- for uid, msg in list(self.messagelist.items()):
- filepath = os.path.join(self.getfullname(), msg['filename'])
- msg['mtime'] = int(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()
- fd = open(filepath, 'rb')
- msg = self.parser['8bit'].parse(fd)
- fd.close()
- self.messagelist[uid]['labels'] = set()
- for hstr in self.getmessageheaderlist(msg, 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 'mtime' not in self.messagelist[uid]:
- return 0
- else:
- return self.messagelist[uid]['mtime']
- def savemessage(self, uid, msg, 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, msg,
- flags, rtime)
- labels = set()
- for hstr in self.getmessageheaderlist(msg, self.labelsheader):
- labels.update(imaputil.labels_from_header(self.labelsheader, hstr))
- # TODO - Not sure why the returned uid is stored early as ret here?
- ret = super(GmailMaildirFolder, self).savemessage(uid, msg, flags,
- rtime)
- # Update the mtime and labels.
- filename = self.messagelist[uid]['filename']
- filepath = os.path.join(self.getfullname(), filename)
- self.messagelist[uid]['mtime'] = int(os.stat(filepath).st_mtime)
- self.messagelist[uid]['labels'] = labels
- return ret
- def savemessagelabels(self, uid, labels, ignorelabels=None):
- """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."""
- if ignorelabels is None:
- ignorelabels = set()
- filename = self.messagelist[uid]['filename']
- filepath = os.path.join(self.getfullname(), filename)
- fd = open(filepath, 'rb')
- msg = self.parser['8bit'].parse(fd)
- fd.close()
- oldlabels = set()
- for hstr in self.getmessageheaderlist(msg, 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.
- self.deletemessageheaders(msg, self.labelsheader)
- self.addmessageheader(msg, self.labelsheader, labels_str)
- mtime = int(os.stat(filepath).st_mtime)
- # 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, msg)
- 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.errno),
- OfflineImapError.ERROR.FOLDER,
- exc_info()[2])
- # If utime_from_header=true, we don't want to change the mtime.
- if self._utime_from_header and mtime:
- os.utime(filepath, (mtime, mtime))
- # save the new mtime and labels
- self.messagelist[uid]['mtime'] = int(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 lb not in addlabellist:
- addlabellist[lb] = []
- addlabellist[lb].append(uid)
- for lb in dellabels:
- if lb not in dellabellist:
- dellabellist[lb] = []
- dellabellist[lb].append(uid)
- for lb, uids in list(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 list(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] = int(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.")
|