123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380 |
- # Base folder support
- # 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.path
- import shutil
- from os import fsync, unlink
- from sys import exc_info
- from threading import Lock
- try:
- import portalocker
- except:
- try:
- import fcntl
- except:
- pass # Ok if this fails, we can do without.
- from offlineimap import OfflineImapError
- from .IMAP import IMAPFolder
- class MappedIMAPFolder(IMAPFolder):
- """IMAP class to map between Folder() instances where both side assign a uid
- This Folder is used on the local side, while the remote side should
- be an IMAPFolder.
- Instance variables (self.):
- dryrun: boolean.
- r2l: dict mapping message uids: self.r2l[remoteuid]=localuid
- l2r: dict mapping message uids: self.r2l[localuid]=remoteuid
- #TODO: what is the difference, how are they used?
- diskr2l: dict mapping message uids: self.r2l[remoteuid]=localuid
- diskl2r: dict mapping message uids: self.r2l[localuid]=remoteuid"""
- def __init__(self, imapserver, name, repository, decode=True):
- IMAPFolder.__init__(self, imapserver, name, repository, decode=False)
- self.dryrun = self.config.getdefaultboolean("general", "dry-run", True)
- self.maplock = Lock()
- self.diskr2l, self.diskl2r = self._loadmaps()
- self.r2l, self.l2r = None, None
- # Representing the local IMAP Folder using local UIDs.
- # XXX: This should be removed since we inherit from IMAPFolder.
- # See commit 3ce514e92ba7 to know more.
- self._mb = IMAPFolder(imapserver, name, repository, decode=False)
- def _getmapfilename(self):
- return os.path.join(self.repository.getmapdir(),
- self.getfolderbasename())
- def _loadmaps(self):
- mapfilename = self._getmapfilename()
- mapfilenametmp = "%s.tmp" % mapfilename
- mapfilenamelock = "%s.lock" % mapfilename
- with self.maplock and open(mapfilenamelock, 'w') as mapfilelock:
- try:
- fcntl.lockf(mapfilelock, fcntl.LOCK_EX) # Blocks until acquired.
- except NameError:
- pass # Windows...
- if os.path.exists(mapfilenametmp):
- self.ui.warn("a previous run might have leave the UIDMaps file"
- " in incorrect state; some sync operations might be done"
- " again and some emails might become duplicated.")
- unlink(mapfilenametmp)
- if not os.path.exists(mapfilename):
- return {}, {}
- file = open(mapfilename, 'rt')
- r2l = {}
- l2r = {}
- while True:
- line = file.readline()
- if not len(line):
- break
- try:
- line = line.strip()
- except ValueError:
- raise Exception(
- "Corrupt line '%s' in UID mapping file '%s'" %
- (line, mapfilename),
- exc_info()[2])
- (str1, str2) = line.split(':')
- loc = int(str1)
- rem = int(str2)
- r2l[rem] = loc
- l2r[loc] = rem
- return r2l, l2r
- def _savemaps(self):
- if self.dryrun is True:
- return
- mapfilename = self._getmapfilename()
- # Do not use the map file directly to prevent from leaving it truncated.
- mapfilenametmp = "%s.tmp" % mapfilename
- mapfilenamelock = "%s.lock" % mapfilename
- with self.maplock and open(mapfilenamelock, 'w') as mapfilelock:
- # The "account" lock already prevents from multiple access by
- # different processes. However, we still need to protect for
- # multiple access from different threads.
- try:
- fcntl.lockf(mapfilelock, fcntl.LOCK_EX) # Blocks until acquired.
- except NameError:
- pass # Windows...
- with open(mapfilenametmp, 'wt') as mapfilefd:
- for (key, value) in list(self.diskl2r.items()):
- mapfilefd.write("%d:%d\n" % (key, value))
- if self.dofsync():
- fsync(mapfilefd)
- # The lock is released when the file descriptor ends.
- shutil.move(mapfilenametmp, mapfilename)
- def _uidlist(self, mapping, items):
- try:
- return [mapping[x] for x in items]
- except KeyError as e:
- raise OfflineImapError(
- "Could not find UID for msg '{0}' (f:'{1}'."
- " This is usually a bad thing and should be "
- "reported on the mailing list.".format(
- e.args[0], self),
- OfflineImapError.ERROR.MESSAGE,
- exc_info()[2])
- # Interface from BaseFolder
- def cachemessagelist(self, min_date=None, min_uid=None):
- self._mb.cachemessagelist(min_date=min_date, min_uid=min_uid)
- reallist = self._mb.getmessagelist()
- self.messagelist = self._mb.messagelist
- with self.maplock:
- # OK. Now we've got a nice list. First, delete things from the
- # summary that have been deleted from the folder.
- for luid in list(self.diskl2r.keys()):
- if luid not in reallist:
- ruid = self.diskl2r[luid]
- # XXX: the following KeyError are sightly unexpected. This
- # would require more digging to understand how it's
- # possible.
- errorMessage = ("unexpected error: key {} was not found "
- "in memory, see "
- "https://github.com/OfflineIMAP/offlineimap/issues/445"
- " to know more."
- )
- try:
- del self.diskr2l[ruid]
- except KeyError:
- self.ui.warn(errorMessage.format(ruid))
- try:
- del self.diskl2r[luid]
- except KeyError:
- self.ui.warn(errorMessage.format(ruid))
- # Now, assign negative UIDs to local items.
- self._savemaps()
- nextneg = -1
- self.r2l = self.diskr2l.copy()
- self.l2r = self.diskl2r.copy()
- for luid in list(reallist.keys()):
- if luid not in self.l2r:
- ruid = nextneg
- nextneg -= 1
- self.l2r[luid] = ruid
- self.r2l[ruid] = luid
- def dropmessagelistcache(self):
- self._mb.dropmessagelistcache()
- # Interface from BaseFolder
- def uidexists(self, ruid):
- """Checks if the (remote) UID exists in this Folder"""
- # This implementation overrides the one in BaseFolder, as it is
- # much more efficient for the mapped case.
- return ruid in self.r2l
- # Interface from BaseFolder
- def getmessageuidlist(self):
- """Gets a list of (remote) UIDs.
- You may have to call cachemessagelist() before calling this function!"""
- # This implementation overrides the one in BaseFolder, as it is
- # much more efficient for the mapped case.
- return list(self.r2l.keys())
- # Interface from BaseFolder
- def getmessagecount(self):
- """Gets the number of messages in this folder.
- You may have to call cachemessagelist() before calling this function!"""
- # This implementation overrides the one in BaseFolder, as it is
- # much more efficient for the mapped case.
- return len(self.r2l)
- # Interface from BaseFolder
- def getmessagelist(self):
- """Gets the current message list.
- This function's implementation is quite expensive for the mapped UID
- case. You must call cachemessagelist() before calling this function!"""
- retval = {}
- localhash = self._mb.getmessagelist()
- with self.maplock:
- for key, value in list(localhash.items()):
- try:
- key = self.l2r[key]
- except KeyError:
- # Sometimes, the IMAP backend may put in a new message,
- # then this function acquires the lock before the system
- # has the chance to note it in the mapping. In that case,
- # just ignore it.
- continue
- value = value.copy()
- value['uid'] = self.l2r[value['uid']]
- retval[key] = value
- return retval
- # Interface from BaseFolder
- def getmessage(self, uid):
- """Returns the specified message."""
- return self._mb.getmessage(self.r2l[uid])
- # Interface from BaseFolder
- def savemessage(self, uid, msg, flags, rtime):
- """Writes a new message, with the specified uid.
- The UIDMaps class will not return a newly assigned uid, as it
- internally maps different uids between IMAP servers. So a
- successful savemessage() invocation will return the same uid it
- has been invoked with. As it maps between 2 IMAP servers which
- means the source message must already have an uid, it requires a
- positive uid to be passed in. Passing in a message with a
- negative uid will do nothing and return the negative uid.
- If the uid is > 0, the backend should set the uid to this, if it can.
- If it cannot set the uid to that, it will save it anyway.
- It will return the uid assigned in any case.
- See folder/Base for details. Note that savemessage() does not
- check against dryrun settings, so you need to ensure that
- savemessage is never called in a dryrun mode.
- """
- self.ui.savemessage('imap', uid, flags, self)
- # Mapped UID instances require the source to already have a
- # positive UID, so simply return here.
- if uid < 0:
- return uid
- # If msg uid already exists, just modify the flags.
- if uid in self.r2l:
- self.savemessageflags(uid, flags)
- return uid
- newluid = self._mb.savemessage(-1, msg, flags, rtime)
- if newluid < 1:
- raise OfflineImapError("server of repository '%s' did not return "
- "a valid UID (got '%s') for UID '%s' from '%s'" % (
- self._mb.getname(), newluid, uid, self.getname()
- ),
- OfflineImapError.ERROR.MESSAGE
- )
- with self.maplock:
- self.diskl2r[newluid] = uid
- self.diskr2l[uid] = newluid
- self.l2r[newluid] = uid
- self.r2l[uid] = newluid
- self._savemaps()
- return uid
- # Interface from BaseFolder
- def getmessageflags(self, uid):
- return self._mb.getmessageflags(self.r2l[uid])
- # Interface from BaseFolder
- def getmessagetime(self, uid):
- return None
- # Interface from BaseFolder
- def savemessageflags(self, uid, flags):
- """Note that this function does not check against dryrun settings,
- so you need to ensure that it is never called in a
- dryrun mode."""
- self._mb.savemessageflags(self.r2l[uid], flags)
- # Interface from BaseFolder
- def addmessageflags(self, uid, flags):
- self._mb.addmessageflags(self.r2l[uid], flags)
- # Interface from BaseFolder
- def addmessagesflags(self, uidlist, flags):
- self._mb.addmessagesflags(self._uidlist(self.r2l, uidlist),
- flags)
- # Interface from BaseFolder
- def change_message_uid(self, ruid, new_ruid):
- """Change the message from existing ruid to new_ruid
- The old remote UID will be changed to a new
- UID. The UIDMaps case handles this efficiently by simply
- changing the mappings file.
- Args:
- ruid: Remote UID
- new_ruid: New Remote UID
- """
- if ruid not in self.r2l:
- raise OfflineImapError("Cannot change unknown Maildir UID %s" %
- ruid, OfflineImapError.ERROR.MESSAGE)
- if ruid == new_ruid:
- return # sanity check shortcut
- with self.maplock:
- luid = self.r2l[ruid]
- self.l2r[luid] = new_ruid
- del self.r2l[ruid]
- self.r2l[new_ruid] = luid
- # TODO: diskl2r|r2l are a pain to sync and should be done away with
- # diskl2r only contains positive UIDs, so wrap in ifs.
- if luid > 0:
- self.diskl2r[luid] = new_ruid
- if ruid > 0:
- del self.diskr2l[ruid]
- if new_ruid > 0:
- self.diskr2l[new_ruid] = luid
- self._savemaps()
- def _mapped_delete(self, uidlist):
- with self.maplock:
- needssave = 0
- for ruid in uidlist:
- luid = self.r2l[ruid]
- del self.r2l[ruid]
- del self.l2r[luid]
- if ruid > 0:
- del self.diskr2l[ruid]
- del self.diskl2r[luid]
- needssave = 1
- if needssave:
- self._savemaps()
- # Interface from BaseFolder
- def deletemessageflags(self, uid, flags):
- self._mb.deletemessageflags(self.r2l[uid], flags)
- # Interface from BaseFolder
- def deletemessagesflags(self, uidlist, flags):
- self._mb.deletemessagesflags(self._uidlist(self.r2l, uidlist),
- flags)
- # Interface from BaseFolder
- def deletemessage(self, uid):
- self._mb.deletemessage(self.r2l[uid])
- self._mapped_delete([uid])
- # Interface from BaseFolder
- def deletemessages(self, uidlist):
- self._mb.deletemessages(self._uidlist(self.r2l, uidlist))
- self._mapped_delete(uidlist)
|