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 Pending for the next major release
================================== ==================================

View File

@ -12,6 +12,38 @@ ChangeLog
releases announces. 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) OfflineIMAP v6.3.4-rc2 (2011-06-15)
=================================== ===================================

View File

@ -1,22 +1,21 @@
__all__ = ['OfflineImap'] __all__ = ['OfflineImap']
__productname__ = 'OfflineIMAP' __productname__ = 'OfflineIMAP'
__version__ = "6.3.4-rc2" __version__ = "6.3.4-rc3"
__copyright__ = "Copyright (C) 2002 - 2010 John Goerzen" __copyright__ = "Copyright 2002-2011 John Goerzen & contributors"
__author__ = "John Goerzen" __author__ = "John Goerzen"
__author_email__= "john@complete.org" __author_email__= "john@complete.org"
__description__ = "Disconnected Universal IMAP Mail Synchronization/Reader Support" __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 __bigcopyright__ = """%(__productname__)s %(__version__)s
%(__copyright__)s <%(__author_email__)s>""" % locals() %(__copyright__)s.
%(__license__)s.
banner = __bigcopyright__ + """ """ % locals()
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."""
__homepage__ = "http://github.com/nicolas33/offlineimap" __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 from offlineimap.error import OfflineImapError
# put this last, so we don't run into circular dependencies using # put this last, so we don't run into circular dependencies using

View File

@ -23,7 +23,7 @@ import re
import time import time
from copy import copy from copy import copy
from Base import BaseFolder from Base import BaseFolder
from offlineimap import imaputil, imaplibutil from offlineimap import imaputil, imaplibutil, OfflineImapError
class IMAPFolder(BaseFolder): class IMAPFolder(BaseFolder):
def __init__(self, imapserver, name, visiblename, accountname, repository): def __init__(self, imapserver, name, visiblename, accountname, repository):
@ -137,7 +137,7 @@ class IMAPFolder(BaseFolder):
search_condition += date_search_str search_condition += date_search_str
if(maxsize != -1): 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 += " "
search_condition += "SMALLER " + self.config.getdefault("Account " + self.accountname, "maxsize", -1) search_condition += "SMALLER " + self.config.getdefault("Account " + self.accountname, "maxsize", -1)
@ -195,13 +195,24 @@ class IMAPFolder(BaseFolder):
def getmessage(self, uid): def getmessage(self, uid):
"""Retrieve message with UID from the IMAP server (incl body) """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() imapobj = self.imapserver.acquireconnection()
try: try:
imapobj.select(self.getfullname(), readonly = 1) imapobj.select(self.getfullname(), readonly = 1)
res_type, data = imapobj.uid('fetch', '%d' % uid, '(BODY.PEEK[])') res_type, data = imapobj.uid('fetch', str(uid), '(BODY.PEEK[])')
assert res_type == 'OK', "Fetching message with UID '%d' failed" % uid 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[] # data looks now e.g. [('320 (UID 17061 BODY[]
# {2565}','msgbody....')] we only asked for one message, # {2565}','msgbody....')] we only asked for one message,
# and that msg is in data[0]. msbody is in [0][1] # 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 from IMAP import IMAPFolder
import os.path import os.path
class MappingFolderMixIn: class MappedIMAPFolder(IMAPFolder):
"""Helper class to map between Folder() instances where both side assign a uid """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.): Instance variables (self.):
r2l: dict mapping message uids: self.r2l[remoteuid]=localuid r2l: dict mapping message uids: self.r2l[remoteuid]=localuid
@ -29,10 +32,13 @@ class MappingFolderMixIn:
#TODO: what is the difference, how are they used? #TODO: what is the difference, how are they used?
diskr2l: dict mapping message uids: self.r2l[remoteuid]=localuid diskr2l: dict mapping message uids: self.r2l[remoteuid]=localuid
diskl2r: dict mapping message uids: self.r2l[localuid]=remoteuid""" 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.maplock = Lock()
(self.diskr2l, self.diskl2r) = self._loadmaps() (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): def _getmapfilename(self):
return os.path.join(self.repository.getmapdir(), return os.path.join(self.repository.getmapdir(),
@ -81,8 +87,8 @@ class MappingFolderMixIn:
return [mapping[x] for x in items] return [mapping[x] for x in items]
def cachemessagelist(self): def cachemessagelist(self):
self._mb.cachemessagelist(self) self._mb.cachemessagelist()
reallist = self._mb.getmessagelist(self) reallist = self._mb.getmessagelist()
self.maplock.acquire() self.maplock.acquire()
try: try:
@ -137,7 +143,7 @@ class MappingFolderMixIn:
cachemessagelist() before calling this function!""" cachemessagelist() before calling this function!"""
retval = {} retval = {}
localhash = self._mb.getmessagelist(self) localhash = self._mb.getmessagelist()
self.maplock.acquire() self.maplock.acquire()
try: try:
for key, value in localhash.items(): for key, value in localhash.items():
@ -158,7 +164,7 @@ class MappingFolderMixIn:
def getmessage(self, uid): def getmessage(self, uid):
"""Returns the content of the specified message.""" """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): def savemessage(self, uid, content, flags, rtime):
"""Writes a new message, with the specified uid. """Writes a new message, with the specified uid.
@ -185,7 +191,7 @@ class MappingFolderMixIn:
self.savemessageflags(uid, flags) self.savemessageflags(uid, flags)
return uid return uid
newluid = self._mb.savemessage(self, -1, content, flags, rtime) newluid = self._mb.savemessage(-1, content, flags, rtime)
if newluid < 1: if newluid < 1:
raise ValueError("Backend could not find uid for message") raise ValueError("Backend could not find uid for message")
self.maplock.acquire() self.maplock.acquire()
@ -197,21 +203,22 @@ class MappingFolderMixIn:
self._savemaps(dolock = 0) self._savemaps(dolock = 0)
finally: finally:
self.maplock.release() self.maplock.release()
return uid
def getmessageflags(self, uid): def getmessageflags(self, uid):
return self._mb.getmessageflags(self, self.r2l[uid]) return self._mb.getmessageflags(self.r2l[uid])
def getmessagetime(self, uid): def getmessagetime(self, uid):
return None return None
def savemessageflags(self, uid, flags): 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): 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): def addmessagesflags(self, uidlist, flags):
self._mb.addmessagesflags(self, self._uidlist(self.r2l, uidlist), self._mb.addmessagesflags(self._uidlist(self.r2l, uidlist),
flags) flags)
def _mapped_delete(self, uidlist): def _mapped_delete(self, uidlist):
@ -232,22 +239,16 @@ class MappingFolderMixIn:
self.maplock.release() self.maplock.release()
def deletemessageflags(self, uid, flags): 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): def deletemessagesflags(self, uidlist, flags):
self._mb.deletemessagesflags(self, self._uidlist(self.r2l, uidlist), self._mb.deletemessagesflags(self._uidlist(self.r2l, uidlist),
flags) flags)
def deletemessage(self, uid): def deletemessage(self, uid):
self._mb.deletemessage(self, self.r2l[uid]) self._mb.deletemessage(self.r2l[uid])
self._mapped_delete([uid]) self._mapped_delete([uid])
def deletemessages(self, uidlist): 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) 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 pass
class UsefulIMAPMixIn: class UsefulIMAPMixIn:
def getstate(self):
return self.state
def getselectedfolder(self): def getselectedfolder(self):
if self.getstate() == 'SELECTED': if self.state == 'SELECTED':
return self.selectedfolder return self.mailbox
return None return None
def select(self, mailbox='INBOX', readonly=None, force = 0): def select(self, mailbox='INBOX', readonly=None, force = 0):
@ -58,10 +56,6 @@ class UsefulIMAPMixIn:
(mailbox, result) (mailbox, result)
severity = OfflineImapError.ERROR.FOLDER severity = OfflineImapError.ERROR.FOLDER
raise OfflineImapError(errstr, severity) raise OfflineImapError(errstr, severity)
if self.getstate() == 'SELECTED':
self.selectedfolder = mailbox
else:
self.selectedfolder = None
return result return result
def _mesg(self, s, tn=None, secs=None): def _mesg(self, s, tn=None, secs=None):

View File

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

View File

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

View File

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

View File

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

View File

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