Merge branch 'next'
This commit is contained in:
commit
ce901800ff
@ -20,6 +20,7 @@ Bug Fixes
|
||||
---------
|
||||
|
||||
|
||||
|
||||
Pending for the next major release
|
||||
==================================
|
||||
|
||||
|
@ -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)
|
||||
===================================
|
||||
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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'),
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user