X-Git-Url: https://code.delx.au/offlineimap/blobdiff_plain/8f9f59dd4d72de390bdf45b6003a396c646d5973..17ec4df02a8d1440aac5634b2516555a5f841046:/offlineimap/imapserver.py diff --git a/offlineimap/imapserver.py b/offlineimap/imapserver.py index 9ebdb2e..c850e99 100644 --- a/offlineimap/imapserver.py +++ b/offlineimap/imapserver.py @@ -1,5 +1,5 @@ # IMAP server support -# Copyright (C) 2002, 2003 John Goerzen +# Copyright (C) 2002 - 2007 John Goerzen # # # This program is free software; you can redistribute it and/or modify @@ -16,11 +16,24 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -from offlineimap import imaplib, 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): @@ -31,8 +44,8 @@ class UsefulIMAPMixIn: return None def select(self, mailbox='INBOX', readonly=None, force = 0): - if (not force) and self.getselectedfolder() == mailbox: - self.is_readonly = readonly + if (not force) and self.getselectedfolder() == mailbox \ + and self.is_readonly == readonly: # No change; return. return result = self.__class__.__bases__[1].select(self, mailbox, readonly) @@ -43,24 +56,71 @@ class UsefulIMAPMixIn: else: self.selectedfolder = None -class UsefulIMAP4(UsefulIMAPMixIn, imaplib.IMAP4): pass -class UsefulIMAP4_SSL(UsefulIMAPMixIn, imaplib.IMAP4_SSL): pass -class UsefulIMAP4_Tunnel(UsefulIMAPMixIn, imaplib.IMAP4_Tunnel): pass + 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): + # 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.password = password self.passworderror = None + self.goodpassword = None self.hostname = hostname self.tunnel = tunnel self.port = port self.usessl = ssl + self.sslclientcert = sslclientcert + self.sslclientkey = sslclientkey self.delim = None self.root = None if port == None: @@ -75,8 +135,15 @@ class IMAPServer: 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: + return self.goodpassword + if self.password != None and self.passworderror == None: return self.password @@ -99,6 +166,7 @@ class IMAPServer: def releaseconnection(self, connection): + """Releases a connection, returning it to the pool.""" self.connectionlock.acquire() self.assignedconnections.remove(connection) self.availableconnections.append(connection) @@ -119,7 +187,34 @@ class IMAPServer: 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 @@ -162,24 +257,42 @@ class IMAPServer: 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) + imapobj.mustquote = imaplibutil.mustquote + 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: + 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 except imapobj.error, val: self.passworderror = str(val) self.password = None @@ -233,8 +346,6 @@ class IMAPServer: 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 @@ -245,32 +356,91 @@ class IMAPServer: 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 @@ -287,7 +457,10 @@ class ConfigedIMAPServer(IMAPServer): 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 @@ -299,6 +472,7 @@ class ConfigedIMAPServer(IMAPServer): IMAPServer.__init__(self, self.config, self.repos.getname(), tunnel = usetunnel, reference = reference, + idlefolders = idlefolders, maxconnections = self.repos.getmaxconnections()) else: if not password: @@ -306,4 +480,7 @@ class ConfigedIMAPServer(IMAPServer): 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)