# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-import imaplib
-from offlineimap import imaplibutil, imaputil, threadutil
+from offlineimap import imaplib2, imaplibutil, imaputil, threadutil
from offlineimap.ui import UIBase
+from offlineimap.accounts import syncfolder
from threading import *
-import thread, hmac, os
+import thread, hmac, os, time
+import base64
+from StringIO import StringIO
+from platform import system
+
+try:
+ # do we have a recent pykerberos?
+ have_gss = False
+ import kerberos
+ if 'authGSSClientWrap' in dir(kerberos):
+ have_gss = True
+except ImportError:
+ pass
class UsefulIMAPMixIn:
def getstate(self):
else:
self.selectedfolder = None
- def _mesg(self, s, secs=None):
- imaplibutil.new_mesg(self, s, secs)
-
-class UsefulIMAP4(UsefulIMAPMixIn, imaplib.IMAP4):
- def open(self, host = '', port = imaplib.IMAP4_PORT):
- imaplibutil.new_open(self, host, port)
+ def _mesg(self, s, tn=None, secs=None):
+ imaplibutil.new_mesg(self, s, tn, secs)
+
+class UsefulIMAP4(UsefulIMAPMixIn, imaplib2.IMAP4):
+ # This is a hack around Darwin's implementation of realloc() (which
+ # Python uses inside the socket code). On Darwin, we split the
+ # message into 100k chunks, which should be small enough - smaller
+ # might start seriously hurting performance ...
+
+ def read(self, size):
+ if (system() == 'Darwin') and (size>0) :
+ read = 0
+ io = StringIO()
+ while read < size:
+ sz = min(size-read, 8192)
+ data = imaplib2.IMAP4.read (self, sz)
+ read += len(data)
+ io.write(data)
+ if len(data) < sz:
+ break
+ return io.getvalue()
+ else:
+ return imaplib2.IMAP4.read (self, size)
class UsefulIMAP4_SSL(UsefulIMAPMixIn, imaplibutil.WrappedIMAP4_SSL):
- def open(self, host = '', port = imaplib.IMAP4_SSL_PORT):
- imaplibutil.new_open_ssl(self, host, port)
+ # This is the same hack as above, to be used in the case of an SSL
+ # connexion.
+
+ def read(self, size):
+ if (system() == 'Darwin') and (size>0) :
+ read = 0
+ io = StringIO()
+ while read < size:
+ sz = min(size-read,8192)
+ data = imaplibutil.WrappedIMAP4_SSL.read (self, sz)
+ read += len(data)
+ io.write(data)
+ if len(data) < sz:
+ break
+ return io.getvalue()
+ else:
+ return imaplibutil.WrappedIMAP4_SSL.read (self,size)
class UsefulIMAP4_Tunnel(UsefulIMAPMixIn, imaplibutil.IMAP4_Tunnel): pass
class IMAPServer:
+ GSS_STATE_STEP = 0
+ GSS_STATE_WRAP = 1
def __init__(self, config, reposname,
username = None, password = None, hostname = None,
port = None, ssl = 1, maxconnections = 1, tunnel = None,
- reference = '""'):
+ reference = '""', sslclientcert = None, sslclientkey = None,
+ idlefolders = []):
self.reposname = reposname
self.config = config
self.username = username
self.tunnel = tunnel
self.port = port
self.usessl = ssl
+ self.sslclientcert = sslclientcert
+ self.sslclientkey = sslclientkey
self.delim = None
self.root = None
if port == None:
self.semaphore = BoundedSemaphore(self.maxconnections)
self.connectionlock = Lock()
self.reference = reference
+ self.idlefolders = idlefolders
+ self.gss_step = self.GSS_STATE_STEP
+ self.gss_vc = None
+ self.gssapi = False
def getpassword(self):
if self.goodpassword != None:
UIBase.getglobalui().debug('imap',
'Attempting plain authentication')
imapobj.login(self.username, self.getpassword())
-
+
+ def gssauth(self, response):
+ data = base64.b64encode(response)
+ try:
+ if self.gss_step == self.GSS_STATE_STEP:
+ if not self.gss_vc:
+ rc, self.gss_vc = kerberos.authGSSClientInit('imap@' +
+ self.hostname)
+ response = kerberos.authGSSClientResponse(self.gss_vc)
+ rc = kerberos.authGSSClientStep(self.gss_vc, data)
+ if rc != kerberos.AUTH_GSS_CONTINUE:
+ self.gss_step = self.GSS_STATE_WRAP
+ elif self.gss_step == self.GSS_STATE_WRAP:
+ rc = kerberos.authGSSClientUnwrap(self.gss_vc, data)
+ response = kerberos.authGSSClientResponse(self.gss_vc)
+ rc = kerberos.authGSSClientWrap(self.gss_vc, response,
+ self.username)
+ response = kerberos.authGSSClientResponse(self.gss_vc)
+ except kerberos.GSSError, err:
+ # Kerberos errored out on us, respond with None to cancel the
+ # authentication
+ UIBase.getglobalui().debug('imap',
+ '%s: %s' % (err[0][0], err[1][0]))
+ return None
+
+ if not response:
+ response = ''
+ return base64.b64decode(response)
def acquireconnection(self):
"""Fetches a connection from the pool, making sure to create a new one
success = 1
elif self.usessl:
UIBase.getglobalui().connecting(self.hostname, self.port)
- imapobj = UsefulIMAP4_SSL(self.hostname, self.port)
+ imapobj = UsefulIMAP4_SSL(self.hostname, self.port,
+ self.sslclientkey, self.sslclientcert)
else:
UIBase.getglobalui().connecting(self.hostname, self.port)
imapobj = UsefulIMAP4(self.hostname, self.port)
if not self.tunnel:
try:
- if 'AUTH=CRAM-MD5' in imapobj.capabilities:
+ # Try GSSAPI and continue if it fails
+ if 'AUTH=GSSAPI' in imapobj.capabilities and have_gss:
UIBase.getglobalui().debug('imap',
- 'Attempting CRAM-MD5 authentication')
+ 'Attempting GSSAPI authentication')
try:
- imapobj.authenticate('CRAM-MD5', self.md5handler)
+ imapobj.authenticate('GSSAPI', self.gssauth)
except imapobj.error, val:
+ self.gssapi = False
+ UIBase.getglobalui().debug('imap',
+ 'GSSAPI Authentication failed')
+ else:
+ self.gssapi = True
+ self.password = None
+
+ if not self.gssapi:
+ if 'AUTH=CRAM-MD5' in imapobj.capabilities:
+ UIBase.getglobalui().debug('imap',
+ 'Attempting CRAM-MD5 authentication')
+ try:
+ imapobj.authenticate('CRAM-MD5', self.md5handler)
+ except imapobj.error, val:
+ self.plainauth(imapobj)
+ else:
self.plainauth(imapobj)
- else:
- self.plainauth(imapobj)
# Would bail by here if there was a failure.
success = 1
self.goodpassword = self.password
self.assignedconnections = []
self.availableconnections = []
self.lastowner = {}
+ # reset kerberos state
+ self.gss_step = self.GSS_STATE_STEP
+ self.gss_vc = None
+ self.gssapi = False
self.connectionlock.release()
def keepalive(self, timeout, event):
ui.debug('imap', 'keepalive thread started')
while 1:
ui.debug('imap', 'keepalive: top of loop')
- event.wait(timeout)
- ui.debug('imap', 'keepalive: after wait')
if event.isSet():
ui.debug('imap', 'keepalive: event is set; exiting')
return
self.connectionlock.release()
ui.debug('imap', 'keepalive: connectionlock released')
threads = []
- imapobjs = []
for i in range(numconnections):
ui.debug('imap', 'keepalive: processing connection %d of %d' % (i, numconnections))
- imapobj = self.acquireconnection()
- ui.debug('imap', 'keepalive: connection %d acquired' % i)
- imapobjs.append(imapobj)
- thr = threadutil.ExitNotifyThread(target = imapobj.noop)
- thr.setDaemon(1)
- thr.start()
- threads.append(thr)
+ if len(self.idlefolders) > i:
+ idler = IdleThread(self, self.idlefolders[i])
+ else:
+ idler = IdleThread(self)
+ idler.start()
+ threads.append(idler)
ui.debug('imap', 'keepalive: thread started')
+ ui.debug('imap', 'keepalive: waiting for timeout')
+ event.wait(timeout)
+
ui.debug('imap', 'keepalive: joining threads')
- for thr in threads:
+ for idler in threads:
# Make sure all the commands have completed.
- thr.join()
-
- ui.debug('imap', 'keepalive: releasing connections')
-
- for imapobj in imapobjs:
- self.releaseconnection(imapobj)
+ idler.stop()
+ idler.join()
ui.debug('imap', 'keepalive: bottom of loop')
+class IdleThread(object):
+ def __init__(self, parent, folder=None):
+ self.parent = parent
+ self.folder = folder
+ self.event = Event()
+ if folder is None:
+ self.thread = Thread(target=self.noop)
+ else:
+ self.thread = Thread(target=self.idle)
+ self.thread.setDaemon(1)
+
+ def start(self):
+ self.thread.start()
+
+ def stop(self):
+ self.event.set()
+
+ def join(self):
+ self.thread.join()
+
+ def noop(self):
+ imapobj = self.parent.acquireconnection()
+ imapobj.noop()
+ self.event.wait()
+ self.parent.releaseconnection(imapobj)
+
+ def dosync(self):
+ remoterepos = self.parent.repos
+ account = remoterepos.account
+ localrepos = account.localrepos
+ remoterepos = account.remoterepos
+ statusrepos = account.statusrepos
+ remotefolder = remoterepos.getfolder(self.folder)
+ syncfolder(account.name, remoterepos, remotefolder, localrepos, statusrepos, quick=False)
+ ui = UIBase.getglobalui()
+ ui.unregisterthread(currentThread())
+
+ def idle(self):
+ imapobj = self.parent.acquireconnection()
+ imapobj.select(self.folder)
+ self.parent.releaseconnection(imapobj)
+ while True:
+ if self.event.isSet():
+ return
+ self.needsync = False
+ def callback(args):
+ if not self.event.isSet():
+ self.needsync = True
+ self.event.set()
+ imapobj = self.parent.acquireconnection()
+ if "IDLE" in imapobj.capabilities:
+ imapobj.idle(callback=callback)
+ else:
+ imapobj.noop()
+ self.event.wait()
+ if self.event.isSet():
+ imapobj.noop()
+ self.parent.releaseconnection(imapobj)
+ if self.needsync:
+ self.event.clear()
+ self.dosync()
+
class ConfigedIMAPServer(IMAPServer):
"""This class is designed for easier initialization given a ConfigParser
object and an account name. The passwordhash is used if
user = self.repos.getuser()
port = self.repos.getport()
ssl = self.repos.getssl()
+ sslclientcert = self.repos.getsslclientcert()
+ sslclientkey = self.repos.getsslclientkey()
reference = self.repos.getreference()
+ idlefolders = self.repos.getidlefolders()
server = None
password = None
IMAPServer.__init__(self, self.config, self.repos.getname(),
tunnel = usetunnel,
reference = reference,
+ idlefolders = idlefolders,
maxconnections = self.repos.getmaxconnections())
else:
if not password:
IMAPServer.__init__(self, self.config, self.repos.getname(),
user, password, host, port, ssl,
self.repos.getmaxconnections(),
- reference = reference)
+ reference = reference,
+ idlefolders = idlefolders,
+ sslclientcert = sslclientcert,
+ sslclientkey = sslclientkey)