Merge branch 'next'

This commit is contained in:
Sebastian Spaeth 2012-01-06 22:26:52 +01:00
commit d653c6a404
11 changed files with 240 additions and 109 deletions

View File

@ -11,6 +11,22 @@ ChangeLog
on releases. And because I'm lazy, it will also be used as a draft for the on releases. And because I'm lazy, it will also be used as a draft for the
releases announces. releases announces.
OfflineIMAP v6.5.0 (2012-01-06)
===============================
This is a CRITICAL bug fix release for everyone who is on the 6.4.x series. Please upgrade to avoid potential data loss! The version has been bumped to 6.5.0, please let everyone know that the 6.4.x series is problematic.
* Uploading multiple emails to an IMAP server would lead to wrong UIDs
being returned (ie the same for all), which confused offlineimap and
led to recurrent upload/download loops and inconsistencies in the
IMAP<->IMAP uid mapping.
* Uploading of Messages from Maildir and IMAP<->IMAP has been made more
efficient by renaming files/mapping entries, rather than actually
loading and saving the message under a new UID.
* Fix regression that broke MachineUI
OfflineIMAP v6.4.4 (2012-01-06) OfflineIMAP v6.4.4 (2012-01-06)
=============================== ===============================

View File

@ -110,3 +110,23 @@ within the sample file.
`OfflineIMAP`_ also ships a file named `offlineimap.conf.minimal` that you can `OfflineIMAP`_ also ships a file named `offlineimap.conf.minimal` that you can
also try. It's useful if you want to get started with the most basic feature also try. It's useful if you want to get started with the most basic feature
set, and you can read about other features later with `offlineimap.conf`. set, and you can read about other features later with `offlineimap.conf`.
===============
Uninstall
===============
If you installed a system-wide installation via "python setup.py
install", there are a few files to purge to uninstall it again. I assume
that /usr/local is the standard prefix that your system and you use
python 2.7. Adapt to your system. In that case you need to:
1) Delete:
/usr/local/lib/python2.7/dist-packages/offlineimap-6.4.4.egg-info
/usr/local/lib/python2.7/dist-packages/offlineimap
2) Delete the cache at (default location) ~/.offlineimap
Delete your manually created (default loc) ~/.offlineimaprc
(It is possible that you created those in different spots)
That's it. Have fun without OfflineImap.

View File

@ -305,6 +305,25 @@ achieve this.
to hit the disk before continueing, you can set this to True. If you to hit the disk before continueing, you can set this to True. If you
set it to False, you lose some of that safety, trading it for speed. set it to False, you lose some of that safety, trading it for speed.
Upgrading from plain text cache to SQLITE based cache
=====================================================
OfflineImap uses a cache to store the last know status of mails (flags etc). Historically that has meant plain text files, but recently we introduced sqlite-based cache, which helps with performance and CPU usage on large folders. Here is how to upgrade existing plain text cache installations to sqlite based one:
1) Sync to make sure things are reasonably similar
3) Change the account section to status_backend = sqlite
4) A new sync will convert your plain text cache to an sqlite cache (but
leave the old plain text cache around for easy reverting)
This should be quick and not involve any mail up/downloading.
5) See if it works :-)
6a) If it does not work, go back to the old version or set
status_backend=plain
6b) Or once you are sure it works, you can delete the
.offlineimap/Account-foo/LocalStatus folder (the new cache will be in
the LocalStatus-sqlite folder)
Security and SSL Security and SSL
================ ================

View File

@ -1,7 +1,7 @@
__all__ = ['OfflineImap'] __all__ = ['OfflineImap']
__productname__ = 'OfflineIMAP' __productname__ = 'OfflineIMAP'
__version__ = "6.4.4" __version__ = "6.5.0"
__copyright__ = "Copyright 2002-2012 John Goerzen & contributors" __copyright__ = "Copyright 2002-2012 John Goerzen & contributors"
__author__ = "John Goerzen" __author__ = "John Goerzen"
__author_email__= "john@complete.org" __author_email__= "john@complete.org"

View File

@ -37,13 +37,14 @@ class BaseFolder(object):
self.sync_this = True self.sync_this = True
"""Should this folder be included in syncing?""" """Should this folder be included in syncing?"""
self.ui = getglobalui() self.ui = getglobalui()
self.name = name # Top level dir name is always ''
self.name = name if not name == self.getsep() else ''
self.repository = repository self.repository = repository
self.visiblename = repository.nametrans(name) self.visiblename = repository.nametrans(name)
# In case the visiblename becomes '.' (top-level) we use '' as # In case the visiblename becomes '.' or '/' (top-level) we use
# that is the name that e.g. the Maildir scanning will return # '' as that is the name that e.g. the Maildir scanning will
# for the top-level dir. # return for the top-level dir.
if self.visiblename == '.': if self.visiblename == self.getsep():
self.visiblename = '' self.visiblename = ''
self.config = repository.getconfig() self.config = repository.getconfig()
@ -234,6 +235,15 @@ class BaseFolder(object):
for uid in uidlist: for uid in uidlist:
self.deletemessageflags(uid, flags) self.deletemessageflags(uid, flags)
def change_message_uid(self, uid, new_uid):
"""Change the message from existing uid to new_uid
If the backend supports it (IMAP does not).
:param new_uid: (optional) If given, the old UID will be changed
to a new UID. This allows backends efficient renaming of
messages if the UID has changed."""
raise NotImplementedException
def deletemessage(self, uid): def deletemessage(self, uid):
raise NotImplementedException raise NotImplementedException
@ -275,21 +285,16 @@ class BaseFolder(object):
#remained negative, no server was willing to assign us an #remained negative, no server was willing to assign us an
#UID. If newid is 0, saving succeeded, but we could not #UID. If newid is 0, saving succeeded, but we could not
#retrieve the new UID. Ignore message in this case. #retrieve the new UID. Ignore message in this case.
newuid = dstfolder.savemessage(uid, message, flags, rtime) new_uid = dstfolder.savemessage(uid, message, flags, rtime)
if new_uid > 0:
if newuid > 0: if new_uid != uid:
if newuid != uid: # Got new UID, change the local uid to match the new one.
self.change_message_uid(uid, new_uid)
statusfolder.deletemessage(uid)
# Got new UID, change the local uid. # Got new UID, change the local uid.
#TODO: Maildir could do this with a rename rather than
#load/save/del operation, IMPLEMENT a changeuid()
#function or so.
self.savemessage(newuid, message, flags, rtime)
self.deletemessage(uid)
uid = newuid
# Save uploaded status in the statusfolder # Save uploaded status in the statusfolder
statusfolder.savemessage(uid, message, flags, rtime) statusfolder.savemessage(new_uid, message, flags, rtime)
elif new_uid == 0:
elif newuid == 0:
# Message was stored to dstfolder, but we can't find it's UID # Message was stored to dstfolder, but we can't find it's UID
# This means we can't link current message to the one created # This means we can't link current message to the one created
# in IMAP. So we just delete local message and on next run # in IMAP. So we just delete local message and on next run
@ -299,11 +304,11 @@ class BaseFolder(object):
self.deletemessage(uid) self.deletemessage(uid)
else: else:
raise OfflineImapError("Trying to save msg (uid %d) on folder " raise OfflineImapError("Trying to save msg (uid %d) on folder "
"%s returned invalid uid %d" % \ "%s returned invalid uid %d" % (uid,
(uid, dstfolder.getvisiblename(), new_uid),
dstfolder.getvisiblename(),
newuid),
OfflineImapError.ERROR.MESSAGE) OfflineImapError.ERROR.MESSAGE)
except (KeyboardInterrupt): # bubble up CTRL-C
raise
except OfflineImapError, e: except OfflineImapError, e:
if e.severity > OfflineImapError.ERROR.MESSAGE: if e.severity > OfflineImapError.ERROR.MESSAGE:
raise # buble severe errors up raise # buble severe errors up
@ -311,10 +316,9 @@ class BaseFolder(object):
except Exception, e: except Exception, e:
self.ui.error(e, "Copying message %s [acc: %s]:\n %s" %\ self.ui.error(e, "Copying message %s [acc: %s]:\n %s" %\
(uid, self.accountname, (uid, self.accountname,
traceback.format_exc())) exc_info()[2]))
raise #raise on unknown errors, so we can fix those raise #raise on unknown errors, so we can fix those
def syncmessagesto_copy(self, dstfolder, statusfolder): def syncmessagesto_copy(self, dstfolder, statusfolder):
"""Pass1: Copy locally existing messages not on the other side """Pass1: Copy locally existing messages not on the other side

View File

@ -33,10 +33,10 @@ except NameError:
class IMAPFolder(BaseFolder): class IMAPFolder(BaseFolder):
def __init__(self, imapserver, name, repository): def __init__(self, imapserver, name, repository):
name = imaputil.dequote(name) name = imaputil.dequote(name)
self.sep = imapserver.delim
super(IMAPFolder, self).__init__(name, repository) super(IMAPFolder, self).__init__(name, repository)
self.expunge = repository.getexpunge() self.expunge = repository.getexpunge()
self.root = None # imapserver.root self.root = None # imapserver.root
self.sep = imapserver.delim
self.imapserver = imapserver self.imapserver = imapserver
self.messagelist = None self.messagelist = None
self.randomgenerator = random.Random() self.randomgenerator = random.Random()
@ -570,7 +570,7 @@ class IMAPFolder(BaseFolder):
self.ui.warn("Server supports UIDPLUS but got no APPENDUID " self.ui.warn("Server supports UIDPLUS but got no APPENDUID "
"appending a message.") "appending a message.")
return 0 return 0
uid = long(imapobj._get_untagged_response('APPENDUID', True)[-1].split(' ')[1]) uid = long(imapobj._get_untagged_response('APPENDUID')[-1].split(' ')[1])
else: else:
# we don't support UIDPLUS # we don't support UIDPLUS
@ -597,8 +597,8 @@ class IMAPFolder(BaseFolder):
self.ui.debug('imap', 'savemessage: returning new UID %d' % uid) self.ui.debug('imap', 'savemessage: returning new UID %d' % uid)
return uid return uid
def savemessageflags(self, uid, flags): def savemessageflags(self, uid, flags):
"""Change a message's flags to `flags`."""
imapobj = self.imapserver.acquireconnection() imapobj = self.imapserver.acquireconnection()
try: try:
try: try:
@ -684,6 +684,14 @@ class IMAPFolder(BaseFolder):
elif operation == '-': elif operation == '-':
self.messagelist[uid]['flags'] -= flags self.messagelist[uid]['flags'] -= flags
def change_message_uid(self, uid, new_uid):
"""Change the message from existing uid to new_uid
If the backend supports it. IMAP does not and will throw errors."""
raise OfflineImapError('IMAP backend cannot change a messages UID from '
'%d to %d' % (uid, new_uid),
OfflineImapError.ERROR.MESSAGE)
def deletemessage(self, uid): def deletemessage(self, uid):
self.deletemessages_noconvert([uid]) self.deletemessages_noconvert([uid])

View File

@ -27,8 +27,8 @@ magicline = "OFFLINEIMAP LocalStatus CACHE DATA - DO NOT MODIFY - FORMAT 1"
class LocalStatusFolder(BaseFolder): class LocalStatusFolder(BaseFolder):
def __init__(self, name, repository): def __init__(self, name, repository):
self.sep = '.' #needs to be set before super.__init__()
super(LocalStatusFolder, self).__init__(name, repository) super(LocalStatusFolder, self).__init__(name, repository)
self.sep = '.'
self.filename = os.path.join(self.getroot(), self.getfolderbasename()) self.filename = os.path.join(self.getroot(), self.getfolderbasename())
self.messagelist = {} self.messagelist = {}
self.savelock = threading.Lock() self.savelock = threading.Lock()

View File

@ -1,6 +1,5 @@
# Maildir folder support # Maildir folder support
# Copyright (C) 2002 - 2007 John Goerzen # Copyright (C) 2002 - 2011 John Goerzen & contributors
# <jgoerzen@complete.org>
# #
# This program is free software; you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@ -35,8 +34,10 @@ except NameError:
from offlineimap import OfflineImapError from offlineimap import OfflineImapError
uidmatchre = re.compile(',U=(\d+)') # Find the UID in a message filename
timestampmatchre = re.compile('(\d+)'); re_uidmatch = re.compile(',U=(\d+)')
# Find a numeric timestamp in a string (filename prefix)
re_timestampmatch = re.compile('(\d+)');
timeseq = 0 timeseq = 0
lasttime = long(0) lasttime = long(0)
@ -59,19 +60,22 @@ def gettimeseq():
class MaildirFolder(BaseFolder): class MaildirFolder(BaseFolder):
def __init__(self, root, name, sep, repository): def __init__(self, root, name, sep, repository):
self.sep = sep # needs to be set before super().__init__
super(MaildirFolder, self).__init__(name, repository) super(MaildirFolder, self).__init__(name, repository)
self.dofsync = self.config.getdefaultboolean("general", "fsync", True) self.dofsync = self.config.getdefaultboolean("general", "fsync", True)
self.root = root self.root = root
self.sep = sep
self.messagelist = None self.messagelist = None
# check if we should use a different infosep to support Win file systems # check if we should use a different infosep to support Win file systems
self.wincompatible = self.config.getdefaultboolean( self.wincompatible = self.config.getdefaultboolean(
"Account "+self.accountname, "maildir-windows-compatible", False) "Account "+self.accountname, "maildir-windows-compatible", False)
self.infosep = '!' if self.wincompatible else ':' self.infosep = '!' if self.wincompatible else ':'
"""infosep is the separator between maildir name and flag appendix""" """infosep is the separator between maildir name and flag appendix"""
self.flagmatchre = re.compile('(%s2,)(\w*)' % self.infosep) self.re_flagmatch = re.compile('%s2,(\w*)' % self.infosep)
#self.ui is set in BaseFolder.init() #self.ui is set in BaseFolder.init()
# Everything up to the first comma or colon (or ! if Windows):
self.re_prefixmatch = re.compile('([^'+ self.infosep + ',]*)')
#folder's md, so we can match with recorded file md5 for validity
self._foldermd5 = md5(self.getvisiblename()).hexdigest()
# Cache the full folder path, as we use getfullname() very often # Cache the full folder path, as we use getfullname() very often
self._fullname = os.path.join(self.getroot(), self.getname()) self._fullname = os.path.join(self.getroot(), self.getname())
@ -97,7 +101,7 @@ class MaildirFolder(BaseFolder):
+ oldest_time_struct[5]) + oldest_time_struct[5])
oldest_time_utc -= oldest_time_today_seconds oldest_time_utc -= oldest_time_today_seconds
timestampmatch = timestampmatchre.search(messagename) timestampmatch = re_timestampmatch.search(messagename)
timestampstr = timestampmatch.group() timestampstr = timestampmatch.group()
timestamplong = long(timestampstr) timestamplong = long(timestampstr)
if(timestamplong < oldest_time_utc): if(timestamplong < oldest_time_utc):
@ -105,68 +109,80 @@ class MaildirFolder(BaseFolder):
else: else:
return True return True
def _parse_filename(self, filename):
"""Returns a messages file name components
Receives the file name (without path) of a msg. Usual format is
'<%d_%d.%d.%s>,U=<%d>,FMD5=<%s>:2,<FLAGS>' (pointy brackets
denoting the various components).
If FMD5 does not correspond with the current folder MD5, we will
return None for the UID & FMD5 (as it is not valid in this
folder). If UID or FMD5 can not be detected, we return `None`
for the respective element. If flags are empty or cannot be
detected, we return an empty flags list.
:returns: (prefix, UID, FMD5, flags). UID is a numeric "long"
type. flags is a set() of Maildir flags"""
prefix, uid, fmd5, flags = None, None, None, set()
prefixmatch = self.re_prefixmatch.match(filename)
if prefixmatch:
prefix = prefixmatch.group(1)
folderstr = ',FMD5=%s' % self._foldermd5
foldermatch = folderstr in filename
# If there was no folder MD5 specified, or if it mismatches,
# assume it is a foreign (new) message and ret: uid, fmd5 = None, None
if foldermatch:
uidmatch = re_uidmatch.search(filename)
if uidmatch:
uid = long(uidmatch.group(1))
flagmatch = self.re_flagmatch.search(filename)
if flagmatch:
flags = set(flagmatch.group(1))
return prefix, uid, fmd5, flags
def _scanfolder(self): def _scanfolder(self):
"""Cache the message list. Maildir flags are: """Cache the message list from a Maildir.
R (replied)
S (seen) Maildir flags are: R (replied) S (seen) T (trashed) D (draft) F
T (trashed) (flagged).
D (draft) :returns: dict that can be used as self.messagelist"""
F (flagged) maxage = self.config.getdefaultint("Account " + self.accountname,
and must occur in ASCII order.""" "maxage", None)
maxsize = self.config.getdefaultint("Account " + self.accountname,
"maxsize", None)
retval = {} retval = {}
files = [] files = []
nouidcounter = -1 # Messages without UIDs get nouidcounter = -1 # Messages without UIDs get negative UIDs.
# negative UID numbers.
foldermd5 = md5(self.getvisiblename()).hexdigest()
folderstr = ',FMD5=' + foldermd5
for dirannex in ['new', 'cur']: for dirannex in ['new', 'cur']:
fulldirname = os.path.join(self.getfullname(), dirannex) fulldirname = os.path.join(self.getfullname(), dirannex)
files.extend(os.path.join(dirannex, filename) for files.extend((dirannex, filename) for
filename in os.listdir(fulldirname)) filename in os.listdir(fulldirname))
for file in files:
messagename = os.path.basename(file)
#check if there is a parameter for maxage / maxsize - then see if this for dirannex, filename in files:
#message should be considered or not # We store just dirannex and filename, ie 'cur/123...'
maxage = self.config.getdefaultint("Account " + self.accountname, "maxage", -1) filepath = os.path.join(dirannex, filename)
maxsize = self.config.getdefaultint("Account " + self.accountname, "maxsize", -1) # check maxage/maxsize if this message should be considered
if maxage and not self._iswithinmaxage(filename, maxage):
if(maxage != -1): continue
isnewenough = self._iswithinmaxage(messagename, maxage) if maxsize and (os.path.getsize(os.path.join(
if(isnewenough != True): self.getfullname(), filepath)) > maxsize):
#this message is older than we should consider....
continue continue
#Check and see if the message is too big if the maxsize for this account is set (prefix, uid, fmd5, flags) = self._parse_filename(filename)
if(maxsize != -1): if uid is None: # assign negative uid to upload it.
size = os.path.getsize(os.path.join(self.getfullname(), file))
if(size > maxsize):
continue
foldermatch = messagename.find(folderstr) != -1
if not foldermatch:
# If there is no folder MD5 specified, or if it mismatches,
# assume it is a foreign (new) message and generate a
# negative uid for it
uid = nouidcounter uid = nouidcounter
nouidcounter -= 1 nouidcounter -= 1
else: # It comes from our folder. else: # It comes from our folder.
uidmatch = uidmatchre.search(messagename) uidmatch = re_uidmatch.search(filename)
uid = None uid = None
if not uidmatch: if not uidmatch:
uid = nouidcounter uid = nouidcounter
nouidcounter -= 1 nouidcounter -= 1
else: else:
uid = long(uidmatch.group(1)) uid = long(uidmatch.group(1))
#identify flags in the path name
flagmatch = self.flagmatchre.search(messagename)
if flagmatch:
flags = set(flagmatch.group(2))
else:
flags = set()
# 'filename' is 'dirannex/filename', e.g. cur/123,U=1,FMD5=1:2,S # 'filename' is 'dirannex/filename', e.g. cur/123,U=1,FMD5=1:2,S
retval[uid] = {'flags': flags, 'filename': file} retval[uid] = {'flags': flags, 'filename': filepath}
return retval return retval
def quickchanged(self, statusfolder): def quickchanged(self, statusfolder):
@ -205,6 +221,17 @@ class MaildirFolder(BaseFolder):
filepath = os.path.join(self.getfullname(), filename) filepath = os.path.join(self.getfullname(), filename)
return os.path.getmtime(filepath) return os.path.getmtime(filepath)
def new_message_filename(self, uid, flags=set()):
"""Creates a new unique Maildir filename
:param uid: The UID`None`, or a set of maildir flags
:param flags: A set of maildir flags
:returns: String containing unique message filename"""
timeval, timeseq = gettimeseq()
return '%d_%d.%d.%s,U=%d,FMD5=%s%s2,%s' % \
(timeval, timeseq, os.getpid(), socket.gethostname(),
uid, self._foldermd5, self.infosep, ''.join(sorted(flags)))
def savemessage(self, uid, content, flags, rtime): def savemessage(self, uid, content, flags, rtime):
# This function only ever saves to tmp/, # This function only ever saves to tmp/,
# but it calls savemessageflags() to actually save to cur/ or new/. # but it calls savemessageflags() to actually save to cur/ or new/.
@ -221,14 +248,7 @@ class MaildirFolder(BaseFolder):
# Otherwise, save the message in tmp/ and then call savemessageflags() # Otherwise, save the message in tmp/ and then call savemessageflags()
# to give it a permanent home. # to give it a permanent home.
tmpdir = os.path.join(self.getfullname(), 'tmp') tmpdir = os.path.join(self.getfullname(), 'tmp')
timeval, timeseq = gettimeseq() messagename = self.new_message_filename(uid, flags)
messagename = '%d_%d.%d.%s,U=%d,FMD5=%s' % \
(timeval,
timeseq,
os.getpid(),
socket.gethostname(),
uid,
md5(self.getvisiblename()).hexdigest())
# open file and write it out # open file and write it out
try: try:
fd = os.open(os.path.join(tmpdir, messagename), fd = os.open(os.path.join(tmpdir, messagename),
@ -253,7 +273,7 @@ class MaildirFolder(BaseFolder):
if rtime != None: if rtime != None:
os.utime(os.path.join(tmpdir, messagename), (rtime, rtime)) os.utime(os.path.join(tmpdir, messagename), (rtime, rtime))
self.messagelist[uid] = {'flags': set(), self.messagelist[uid] = {'flags': flags,
'filename': os.path.join('tmp', messagename)} 'filename': os.path.join('tmp', messagename)}
# savemessageflags moves msg to 'cur' or 'new' as appropriate # savemessageflags moves msg to 'cur' or 'new' as appropriate
self.savemessageflags(uid, flags) self.savemessageflags(uid, flags)
@ -264,25 +284,27 @@ class MaildirFolder(BaseFolder):
return self.messagelist[uid]['flags'] return self.messagelist[uid]['flags']
def savemessageflags(self, uid, flags): def savemessageflags(self, uid, flags):
"""Sets the specified message's flags to the given set.
This function moves the message to the cur or new subdir,
depending on the 'S'een flag."""
oldfilename = self.messagelist[uid]['filename'] oldfilename = self.messagelist[uid]['filename']
dir_prefix, newname = os.path.split(oldfilename) dir_prefix, filename = os.path.split(oldfilename)
tmpdir = os.path.join(self.getfullname(), 'tmp') # If a message has been seen, it goes into 'cur'
if 'S' in flags: dir_prefix = 'cur' if 'S' in flags else 'new'
# If a message has been seen, it goes into the cur
# directory. CR debian#152482
dir_prefix = 'cur'
else:
dir_prefix = 'new'
# Strip off existing infostring (preserving small letter flags, that if flags != self.messagelist[uid]['flags']:
# Flags have actually changed, construct new filename
# Strip off existing infostring (preserving small letter flags that
# dovecot uses) # dovecot uses)
infomatch = self.flagmatchre.search(newname) infomatch = self.flagmatchre.search(filename)
if infomatch: if infomatch:
newname = newname[:-len(infomatch.group())] #strip off filename = filename[:-len(infomatch.group())] #strip off
infostr = '%s2,%s' % (self.infosep, ''.join(sorted(flags))) infostr = '%s2,%s' % (self.infosep, ''.join(sorted(flags)))
newname += infostr filename += infostr
newfilename = os.path.join(dir_prefix, newname) newfilename = os.path.join(dir_prefix, filename)
if (newfilename != oldfilename): if (newfilename != oldfilename):
try: try:
os.rename(os.path.join(self.getfullname(), oldfilename), os.rename(os.path.join(self.getfullname(), oldfilename),
@ -295,9 +317,25 @@ class MaildirFolder(BaseFolder):
self.messagelist[uid]['flags'] = flags self.messagelist[uid]['flags'] = flags
self.messagelist[uid]['filename'] = newfilename self.messagelist[uid]['filename'] = newfilename
# By now, the message had better not be in tmp/ land! def change_message_uid(self, uid, new_uid):
final_dir, final_name = os.path.split(self.messagelist[uid]['filename']) """Change the message from existing uid to new_uid
assert final_dir != 'tmp'
This will not update the statusfolder UID, you need to do that yourself.
:param new_uid: (optional) If given, the old UID will be changed
to a new UID. The Maildir backend can implement this as an efficient
rename."""
if not uid in self.messagelist:
raise OfflineImapError("Cannot change unknown Maildir UID %s" % uid)
if uid == new_uid: return
oldfilename = self.messagelist[uid]['filename']
dir_prefix, filename = os.path.split(oldfilename)
flags = self.getmessageflags(uid)
filename = self.new_message_filename(new_uid, flags)
os.rename(os.path.join(self.getfullname(), oldfilename),
os.path.join(self.getfullname(), dir_prefix, filename))
self.messagelist[new_uid] = self.messagelist[uid]
del self.messagelist[uid]
def deletemessage(self, uid): def deletemessage(self, uid):
"""Unlinks a message file from the Maildir. """Unlinks a message file from the Maildir.

View File

@ -221,6 +221,31 @@ class MappedIMAPFolder(IMAPFolder):
self._mb.addmessagesflags(self._uidlist(self.r2l, uidlist), self._mb.addmessagesflags(self._uidlist(self.r2l, uidlist),
flags) flags)
def change_message_uid(self, ruid, new_ruid):
"""Change the message from existing ruid to new_ruid
:param new_uid: The old remote UID will be changed to a new
UID. The UIDMaps case handles this efficiently by simply
changing the mappings file."""
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
self.maplock.acquire()
try:
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(dolock = 0)
finally:
self.maplock.release()
def _mapped_delete(self, uidlist): def _mapped_delete(self, uidlist):
self.maplock.acquire() self.maplock.acquire()
try: try:

View File

@ -324,6 +324,8 @@ class IMAPRepository(BaseRepository):
:param foldername: Full path of the folder to be created.""" :param foldername: Full path of the folder to be created."""
if self.getreference(): if self.getreference():
foldername = self.getreference() + self.getsep() + foldername foldername = self.getreference() + self.getsep() + foldername
if not foldername: # Create top level folder as folder separator
foldername = self.getsep()
imapobj = self.imapserver.acquireconnection() imapobj = self.imapserver.acquireconnection()
try: try:

View File

@ -14,7 +14,6 @@
# along with this program; if not, write to the Free Software # along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import urllib
import sys import sys
import time import time
import logging import logging