Merge branch 'next'

This commit is contained in:
Nicolas Sebrecht 2011-07-07 18:46:28 +02:00
commit ce901800ff
11 changed files with 107 additions and 62 deletions

View File

@ -20,6 +20,7 @@ Bug Fixes
---------
Pending for the next major release
==================================

View File

@ -12,6 +12,38 @@ ChangeLog
releases announces.
OfflineIMAP v6.3.4-rc3 (2011-07-07)
===================================
Notes
-----
Here is a surprising release. :-)
As expected we have a lot bug fixes in this round (see git log for details),
including a fix for a bug we had for ages (details below) which is a very good
news.
What makes this cycle so unusual is that I merged a feature to support StartTLS
automatically (thanks Sebastian!). Another very good news.
We usually don't do much changes so late in a cycle. Now, things are highly
calming down and I hope a lot of people will test this release. Next one could
be the stable!
New Features
------------
* Added StartTLS support, it will automatically be used if the server
supports it.
Bug Fixes
---------
* We protect more robustly against asking for inexistent messages from the
IMAP server, when someone else deletes or moves messages while we sync.
OfflineIMAP v6.3.4-rc2 (2011-06-15)
===================================

View File

@ -1,22 +1,21 @@
__all__ = ['OfflineImap']
__productname__ = 'OfflineIMAP'
__version__ = "6.3.4-rc2"
__copyright__ = "Copyright (C) 2002 - 2010 John Goerzen"
__version__ = "6.3.4-rc3"
__copyright__ = "Copyright 2002-2011 John Goerzen & contributors"
__author__ = "John Goerzen"
__author_email__= "john@complete.org"
__description__ = "Disconnected Universal IMAP Mail Synchronization/Reader Support"
__license__ = "Licensed under the GNU GPL v2+ (v2 or any later version)"
__bigcopyright__ = """%(__productname__)s %(__version__)s
%(__copyright__)s <%(__author_email__)s>""" % locals()
banner = __bigcopyright__ + """
This software comes with ABSOLUTELY NO WARRANTY; see the file
COPYING for details. This is free software, and you are welcome
to distribute it under the conditions laid out in COPYING."""
%(__copyright__)s.
%(__license__)s.
""" % locals()
__homepage__ = "http://github.com/nicolas33/offlineimap"
__license__ = "Licensed under the GNU GPL v2+ (v2 or any later version)."
banner = __bigcopyright__
from offlineimap.error import OfflineImapError
# put this last, so we don't run into circular dependencies using

View File

@ -23,7 +23,7 @@ import re
import time
from copy import copy
from Base import BaseFolder
from offlineimap import imaputil, imaplibutil
from offlineimap import imaputil, imaplibutil, OfflineImapError
class IMAPFolder(BaseFolder):
def __init__(self, imapserver, name, visiblename, accountname, repository):
@ -137,7 +137,7 @@ class IMAPFolder(BaseFolder):
search_condition += date_search_str
if(maxsize != -1):
if(maxage != 1): #There are two conditions - add a space
if(maxage != -1): #There are two conditions - add a space
search_condition += " "
search_condition += "SMALLER " + self.config.getdefault("Account " + self.accountname, "maxsize", -1)
@ -195,13 +195,24 @@ class IMAPFolder(BaseFolder):
def getmessage(self, uid):
"""Retrieve message with UID from the IMAP server (incl body)
:returns: the message body
:returns: the message body or throws and OfflineImapError
(probably severity MESSAGE) if e.g. no message with
this UID could be found.
"""
imapobj = self.imapserver.acquireconnection()
try:
imapobj.select(self.getfullname(), readonly = 1)
res_type, data = imapobj.uid('fetch', '%d' % uid, '(BODY.PEEK[])')
assert res_type == 'OK', "Fetching message with UID '%d' failed" % uid
res_type, data = imapobj.uid('fetch', str(uid), '(BODY.PEEK[])')
if data == [None] or res_type != 'OK':
#IMAP server says bad request or UID does not exist
severity = OfflineImapError.ERROR.MESSAGE
reason = "IMAP server '%s' responded with '%s' to fetching "\
"message UID '%d'" % (self.getrepository(), res_type, uid)
if data == [None]:
#IMAP server did not find a message with this UID
reason = "IMAP server '%s' does not have a message "\
"with UID '%s'" % (self.getrepository(), uid)
raise OfflineImapError(reason, severity)
# data looks now e.g. [('320 (UID 17061 BODY[]
# {2565}','msgbody....')] we only asked for one message,
# and that msg is in data[0]. msbody is in [0][1]

View File

@ -20,8 +20,11 @@ from threading import *
from IMAP import IMAPFolder
import os.path
class MappingFolderMixIn:
"""Helper class to map between Folder() instances where both side assign a uid
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.):
r2l: dict mapping message uids: self.r2l[remoteuid]=localuid
@ -29,10 +32,13 @@ class MappingFolderMixIn:
#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 _initmapping(self):
def __init__(self, *args, **kwargs):
IMAPFolder.__init__(self, *args, **kwargs)
self.maplock = Lock()
(self.diskr2l, self.diskl2r) = self._loadmaps()
self._mb = self.__class__.__bases__[1]
self._mb = IMAPFolder(*args, **kwargs)
"""Representing the local IMAP Folder using local UIDs"""
def _getmapfilename(self):
return os.path.join(self.repository.getmapdir(),
@ -81,8 +87,8 @@ class MappingFolderMixIn:
return [mapping[x] for x in items]
def cachemessagelist(self):
self._mb.cachemessagelist(self)
reallist = self._mb.getmessagelist(self)
self._mb.cachemessagelist()
reallist = self._mb.getmessagelist()
self.maplock.acquire()
try:
@ -137,7 +143,7 @@ class MappingFolderMixIn:
cachemessagelist() before calling this function!"""
retval = {}
localhash = self._mb.getmessagelist(self)
localhash = self._mb.getmessagelist()
self.maplock.acquire()
try:
for key, value in localhash.items():
@ -158,7 +164,7 @@ class MappingFolderMixIn:
def getmessage(self, uid):
"""Returns the content of the specified message."""
return self._mb.getmessage(self, self.r2l[uid])
return self._mb.getmessage(self.r2l[uid])
def savemessage(self, uid, content, flags, rtime):
"""Writes a new message, with the specified uid.
@ -185,7 +191,7 @@ class MappingFolderMixIn:
self.savemessageflags(uid, flags)
return uid
newluid = self._mb.savemessage(self, -1, content, flags, rtime)
newluid = self._mb.savemessage(-1, content, flags, rtime)
if newluid < 1:
raise ValueError("Backend could not find uid for message")
self.maplock.acquire()
@ -197,21 +203,22 @@ class MappingFolderMixIn:
self._savemaps(dolock = 0)
finally:
self.maplock.release()
return uid
def getmessageflags(self, uid):
return self._mb.getmessageflags(self, self.r2l[uid])
return self._mb.getmessageflags(self.r2l[uid])
def getmessagetime(self, uid):
return None
def savemessageflags(self, uid, flags):
self._mb.savemessageflags(self, self.r2l[uid], flags)
self._mb.savemessageflags(self.r2l[uid], flags)
def addmessageflags(self, uid, flags):
self._mb.addmessageflags(self, self.r2l[uid], flags)
self._mb.addmessageflags(self.r2l[uid], flags)
def addmessagesflags(self, uidlist, flags):
self._mb.addmessagesflags(self, self._uidlist(self.r2l, uidlist),
self._mb.addmessagesflags(self._uidlist(self.r2l, uidlist),
flags)
def _mapped_delete(self, uidlist):
@ -232,22 +239,16 @@ class MappingFolderMixIn:
self.maplock.release()
def deletemessageflags(self, uid, flags):
self._mb.deletemessageflags(self, self.r2l[uid], flags)
self._mb.deletemessageflags(self.r2l[uid], flags)
def deletemessagesflags(self, uidlist, flags):
self._mb.deletemessagesflags(self, self._uidlist(self.r2l, uidlist),
self._mb.deletemessagesflags(self._uidlist(self.r2l, uidlist),
flags)
def deletemessage(self, uid):
self._mb.deletemessage(self, self.r2l[uid])
self._mb.deletemessage(self.r2l[uid])
self._mapped_delete([uid])
def deletemessages(self, uidlist):
self._mb.deletemessages(self, self._uidlist(self.r2l, uidlist))
self._mb.deletemessages(self._uidlist(self.r2l, uidlist))
self._mapped_delete(uidlist)
# Define a class for local part of IMAP.
class MappedIMAPFolder(MappingFolderMixIn, IMAPFolder):
def __init__(self, *args, **kwargs):
IMAPFolder.__init__(self, *args, **kwargs)
self._initmapping()

View File

@ -33,11 +33,9 @@ except ImportError:
pass
class UsefulIMAPMixIn:
def getstate(self):
return self.state
def getselectedfolder(self):
if self.getstate() == 'SELECTED':
return self.selectedfolder
if self.state == 'SELECTED':
return self.mailbox
return None
def select(self, mailbox='INBOX', readonly=None, force = 0):
@ -58,10 +56,6 @@ class UsefulIMAPMixIn:
(mailbox, result)
severity = OfflineImapError.ERROR.FOLDER
raise OfflineImapError(errstr, severity)
if self.getstate() == 'SELECTED':
self.selectedfolder = mailbox
else:
self.selectedfolder = None
return result
def _mesg(self, s, tn=None, secs=None):

View File

@ -24,7 +24,6 @@ import offlineimap.accounts
import hmac
import socket
import base64
import errno
from socket import gaierror
try:
@ -32,6 +31,7 @@ try:
except ImportError:
# Protect against python<2.6, use dummy and won't get SSL errors.
SSLError = None
try:
# do we have a recent pykerberos?
have_gss = False
@ -219,6 +219,7 @@ class IMAPServer:
try:
# Try GSSAPI and continue if it fails
if 'AUTH=GSSAPI' in imapobj.capabilities and have_gss:
self.connectionlock.acquire()
self.ui.debug('imap',
'Attempting GSSAPI authentication')
try:
@ -229,15 +230,26 @@ class IMAPServer:
'GSSAPI Authentication failed')
else:
self.gssapi = True
kerberos.authGSSClientClean(self.gss_vc)
self.gss_vc = None
self.gss_step = self.GSS_STATE_STEP
#if we do self.password = None then the next attempt cannot try...
#self.password = None
self.connectionlock.release()
if not self.gssapi:
if 'STARTTLS' in imapobj.capabilities and not\
self.usessl:
self.ui.debug('imap',
'Using STARTTLS connection')
imapobj.starttls()
if 'AUTH=CRAM-MD5' in imapobj.capabilities:
self.ui.debug('imap',
'Attempting CRAM-MD5 authentication')
try:
imapobj.authenticate('CRAM-MD5', self.md5handler)
imapobj.authenticate('CRAM-MD5',
self.md5handler)
except imapobj.error, val:
self.plainauth(imapobj)
else:
@ -285,9 +297,8 @@ class IMAPServer:
if(self.connectionlock.locked()):
self.connectionlock.release()
# now, check for known errors and throw OfflineImapErrors
severity = OfflineImapError.ERROR.REPO
if isinstance(e, gaierror):
if type(e) == gaierror:
#DNS related errors. Abort Repo sync
#TODO: special error msg for e.errno == 2 "Name or service not known"?
reason = "Could not resolve name '%s' for repository "\
@ -296,7 +307,7 @@ class IMAPServer:
(self.hostname, self.reposname)
raise OfflineImapError(reason, severity)
elif isinstance(e, SSLError) and e.errno == 1:
elif SSLError and isinstance(e, SSLError) and e.errno == 1:
# SSL unknown protocol error
# happens e.g. when connecting via SSL to a non-SSL service
if self.port != 443:
@ -322,8 +333,7 @@ class IMAPServer:
if str(e)[:24] == "can't open socket; error":
raise OfflineImapError("Could not connect to remote server '%s' "\
"for repository '%s'. Remote does not answer."
% (self.hostname, self.reposname),
OfflineImapError.ERROR.REPO)
% (self.hostname, self.reposname), severity)
else:
# re-raise all other errors
raise

View File

@ -143,7 +143,6 @@ def imapsplit(imapstring):
elif splitslen == 0:
# There was not even an unquoted word.
break
debug("imapsplit() returning:", retval)
return retval
flagmap = [('\\Seen', 'S'),

View File

@ -343,6 +343,7 @@ class OfflineImap:
t.start()
threadutil.exitnotifymonitorloop(threadutil.threadexited)
ui.terminate()
except KeyboardInterrupt:
ui.terminate(1, errormsg = 'CTRL-C pressed, aborting...')
return

View File

@ -100,7 +100,7 @@ class MaildirRepository(BaseRepository):
except OSError, e:
if e.errno == 17 and os.path.isdir(full_path):
self.debug("makefolder: '%s' already has subdir %s" %
(foldername, sudir))
(foldername, subdir))
else:
raise
# Invalidate the folder cache

View File

@ -21,7 +21,6 @@ import time
import sys
import traceback
import threading
from StringIO import StringIO
import offlineimap
debugtypes = {'':'Other offlineimap related sync messages',
@ -309,10 +308,8 @@ class UIBase:
s.terminate(100)
def getMainExceptionString(s):
sbuf = StringIO()
traceback.print_exc(file = sbuf)
return "Main program terminated with exception:\n" + \
sbuf.getvalue() + "\n" + \
return "Main program terminated with exception:\n%s\n" %\
traceback.format_exc() + \
s.getThreadDebugLog(threading.currentThread())
def mainException(s):