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
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)
===============================

View File

@ -110,3 +110,23 @@ within the sample file.
`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
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
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
================

View File

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

View File

@ -37,13 +37,14 @@ class BaseFolder(object):
self.sync_this = True
"""Should this folder be included in syncing?"""
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.visiblename = repository.nametrans(name)
# In case the visiblename becomes '.' (top-level) we use '' as
# that is the name that e.g. the Maildir scanning will return
# for the top-level dir.
if self.visiblename == '.':
# In case the visiblename becomes '.' or '/' (top-level) we use
# '' as that is the name that e.g. the Maildir scanning will
# return for the top-level dir.
if self.visiblename == self.getsep():
self.visiblename = ''
self.config = repository.getconfig()
@ -234,6 +235,15 @@ class BaseFolder(object):
for uid in uidlist:
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):
raise NotImplementedException
@ -275,21 +285,16 @@ class BaseFolder(object):
#remained negative, no server was willing to assign us an
#UID. If newid is 0, saving succeeded, but we could not
#retrieve the new UID. Ignore message in this case.
newuid = dstfolder.savemessage(uid, message, flags, rtime)
if newuid > 0:
if newuid != uid:
new_uid = dstfolder.savemessage(uid, message, flags, rtime)
if new_uid > 0:
if new_uid != 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.
#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
statusfolder.savemessage(uid, message, flags, rtime)
elif newuid == 0:
statusfolder.savemessage(new_uid, message, flags, rtime)
elif new_uid == 0:
# 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
# in IMAP. So we just delete local message and on next run
@ -299,11 +304,11 @@ class BaseFolder(object):
self.deletemessage(uid)
else:
raise OfflineImapError("Trying to save msg (uid %d) on folder "
"%s returned invalid uid %d" % \
(uid,
dstfolder.getvisiblename(),
newuid),
"%s returned invalid uid %d" % (uid,
dstfolder.getvisiblename(), new_uid),
OfflineImapError.ERROR.MESSAGE)
except (KeyboardInterrupt): # bubble up CTRL-C
raise
except OfflineImapError, e:
if e.severity > OfflineImapError.ERROR.MESSAGE:
raise # buble severe errors up
@ -311,10 +316,9 @@ class BaseFolder(object):
except Exception, e:
self.ui.error(e, "Copying message %s [acc: %s]:\n %s" %\
(uid, self.accountname,
traceback.format_exc()))
exc_info()[2]))
raise #raise on unknown errors, so we can fix those
def syncmessagesto_copy(self, dstfolder, statusfolder):
"""Pass1: Copy locally existing messages not on the other side

View File

@ -33,10 +33,10 @@ except NameError:
class IMAPFolder(BaseFolder):
def __init__(self, imapserver, name, repository):
name = imaputil.dequote(name)
self.sep = imapserver.delim
super(IMAPFolder, self).__init__(name, repository)
self.expunge = repository.getexpunge()
self.root = None # imapserver.root
self.sep = imapserver.delim
self.imapserver = imapserver
self.messagelist = None
self.randomgenerator = random.Random()
@ -570,7 +570,7 @@ class IMAPFolder(BaseFolder):
self.ui.warn("Server supports UIDPLUS but got no APPENDUID "
"appending a message.")
return 0
uid = long(imapobj._get_untagged_response('APPENDUID', True)[-1].split(' ')[1])
uid = long(imapobj._get_untagged_response('APPENDUID')[-1].split(' ')[1])
else:
# we don't support UIDPLUS
@ -597,8 +597,8 @@ class IMAPFolder(BaseFolder):
self.ui.debug('imap', 'savemessage: returning new UID %d' % uid)
return uid
def savemessageflags(self, uid, flags):
"""Change a message's flags to `flags`."""
imapobj = self.imapserver.acquireconnection()
try:
try:
@ -684,6 +684,14 @@ class IMAPFolder(BaseFolder):
elif operation == '-':
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):
self.deletemessages_noconvert([uid])

View File

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

View File

@ -1,6 +1,5 @@
# Maildir folder support
# Copyright (C) 2002 - 2007 John Goerzen
# <jgoerzen@complete.org>
# 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
@ -35,8 +34,10 @@ except NameError:
from offlineimap import OfflineImapError
uidmatchre = re.compile(',U=(\d+)')
timestampmatchre = re.compile('(\d+)');
# Find the UID in a message filename
re_uidmatch = re.compile(',U=(\d+)')
# Find a numeric timestamp in a string (filename prefix)
re_timestampmatch = re.compile('(\d+)');
timeseq = 0
lasttime = long(0)
@ -59,19 +60,22 @@ def gettimeseq():
class MaildirFolder(BaseFolder):
def __init__(self, root, name, sep, repository):
self.sep = sep # needs to be set before super().__init__
super(MaildirFolder, self).__init__(name, repository)
self.dofsync = self.config.getdefaultboolean("general", "fsync", True)
self.root = root
self.sep = sep
self.messagelist = None
# check if we should use a different infosep to support Win file systems
self.wincompatible = self.config.getdefaultboolean(
"Account "+self.accountname, "maildir-windows-compatible", False)
self.infosep = '!' if self.wincompatible else ':'
"""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()
# 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
self._fullname = os.path.join(self.getroot(), self.getname())
@ -97,7 +101,7 @@ class MaildirFolder(BaseFolder):
+ oldest_time_struct[5])
oldest_time_utc -= oldest_time_today_seconds
timestampmatch = timestampmatchre.search(messagename)
timestampmatch = re_timestampmatch.search(messagename)
timestampstr = timestampmatch.group()
timestamplong = long(timestampstr)
if(timestamplong < oldest_time_utc):
@ -105,68 +109,80 @@ class MaildirFolder(BaseFolder):
else:
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):
"""Cache the message list. Maildir flags are:
R (replied)
S (seen)
T (trashed)
D (draft)
F (flagged)
and must occur in ASCII order."""
"""Cache the message list from a Maildir.
Maildir flags are: R (replied) S (seen) T (trashed) D (draft) F
(flagged).
:returns: dict that can be used as self.messagelist"""
maxage = self.config.getdefaultint("Account " + self.accountname,
"maxage", None)
maxsize = self.config.getdefaultint("Account " + self.accountname,
"maxsize", None)
retval = {}
files = []
nouidcounter = -1 # Messages without UIDs get
# negative UID numbers.
foldermd5 = md5(self.getvisiblename()).hexdigest()
folderstr = ',FMD5=' + foldermd5
nouidcounter = -1 # Messages without UIDs get negative UIDs.
for dirannex in ['new', 'cur']:
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))
for file in files:
messagename = os.path.basename(file)
#check if there is a parameter for maxage / maxsize - then see if this
#message should be considered or not
maxage = self.config.getdefaultint("Account " + self.accountname, "maxage", -1)
maxsize = self.config.getdefaultint("Account " + self.accountname, "maxsize", -1)
for dirannex, filename in files:
# We store just dirannex and filename, ie 'cur/123...'
filepath = os.path.join(dirannex, filename)
# check maxage/maxsize if this message should be considered
if maxage and not self._iswithinmaxage(filename, maxage):
continue
if maxsize and (os.path.getsize(os.path.join(
self.getfullname(), filepath)) > maxsize):
continue
if(maxage != -1):
isnewenough = self._iswithinmaxage(messagename, maxage)
if(isnewenough != True):
#this message is older than we should consider....
continue
#Check and see if the message is too big if the maxsize for this account is set
if(maxsize != -1):
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
(prefix, uid, fmd5, flags) = self._parse_filename(filename)
if uid is None: # assign negative uid to upload it.
uid = nouidcounter
nouidcounter -= 1
else: # It comes from our folder.
uidmatch = uidmatchre.search(messagename)
uidmatch = re_uidmatch.search(filename)
uid = None
if not uidmatch:
uid = nouidcounter
nouidcounter -= 1
else:
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
retval[uid] = {'flags': flags, 'filename': file}
retval[uid] = {'flags': flags, 'filename': filepath}
return retval
def quickchanged(self, statusfolder):
@ -205,6 +221,17 @@ class MaildirFolder(BaseFolder):
filepath = os.path.join(self.getfullname(), filename)
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):
# This function only ever saves to tmp/,
# 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()
# to give it a permanent home.
tmpdir = os.path.join(self.getfullname(), 'tmp')
timeval, timeseq = gettimeseq()
messagename = '%d_%d.%d.%s,U=%d,FMD5=%s' % \
(timeval,
timeseq,
os.getpid(),
socket.gethostname(),
uid,
md5(self.getvisiblename()).hexdigest())
messagename = self.new_message_filename(uid, flags)
# open file and write it out
try:
fd = os.open(os.path.join(tmpdir, messagename),
@ -253,7 +273,7 @@ class MaildirFolder(BaseFolder):
if rtime != None:
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)}
# savemessageflags moves msg to 'cur' or 'new' as appropriate
self.savemessageflags(uid, flags)
@ -264,25 +284,27 @@ class MaildirFolder(BaseFolder):
return self.messagelist[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']
dir_prefix, newname = os.path.split(oldfilename)
tmpdir = os.path.join(self.getfullname(), 'tmp')
if 'S' in flags:
# If a message has been seen, it goes into the cur
# directory. CR debian#152482
dir_prefix = 'cur'
else:
dir_prefix = 'new'
dir_prefix, filename = os.path.split(oldfilename)
# If a message has been seen, it goes into 'cur'
dir_prefix = 'cur' if 'S' in flags else 'new'
# Strip off existing infostring (preserving small letter flags, that
# dovecot uses)
infomatch = self.flagmatchre.search(newname)
if infomatch:
newname = newname[:-len(infomatch.group())] #strip off
infostr = '%s2,%s' % (self.infosep, ''.join(sorted(flags)))
newname += infostr
if flags != self.messagelist[uid]['flags']:
# Flags have actually changed, construct new filename
# Strip off existing infostring (preserving small letter flags that
# dovecot uses)
infomatch = self.flagmatchre.search(filename)
if infomatch:
filename = filename[:-len(infomatch.group())] #strip off
infostr = '%s2,%s' % (self.infosep, ''.join(sorted(flags)))
filename += infostr
newfilename = os.path.join(dir_prefix, newname)
newfilename = os.path.join(dir_prefix, filename)
if (newfilename != oldfilename):
try:
os.rename(os.path.join(self.getfullname(), oldfilename),
@ -295,10 +317,26 @@ class MaildirFolder(BaseFolder):
self.messagelist[uid]['flags'] = flags
self.messagelist[uid]['filename'] = newfilename
# By now, the message had better not be in tmp/ land!
final_dir, final_name = os.path.split(self.messagelist[uid]['filename'])
assert final_dir != 'tmp'
def change_message_uid(self, uid, new_uid):
"""Change the message from existing uid to new_uid
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):
"""Unlinks a message file from the Maildir.

View File

@ -221,6 +221,31 @@ class MappedIMAPFolder(IMAPFolder):
self._mb.addmessagesflags(self._uidlist(self.r2l, uidlist),
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):
self.maplock.acquire()
try:

View File

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

View File

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