]> code.delx.au - offlineimap/blobdiff - offlineimap/imapserver.py
Added check for IDLE in capabilities
[offlineimap] / offlineimap / imapserver.py
index 6ea25ebd5bf1912b47e4f0f0ca597eaf7d61148c..c850e99a01fc3963f8becdecea8dea783bbd004d 100644 (file)
 #    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):
@@ -32,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)
@@ -44,33 +56,71 @@ class UsefulIMAPMixIn:
         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)
-
-class UsefulIMAP4_SSL(UsefulIMAPMixIn, imaplib.IMAP4_SSL):
-    def open(self, host = '', port = imaplib.IMAP4_SSL_PORT):
-        imaplibutil.new_open_ssl(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):
+    # 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:
@@ -85,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
 
@@ -109,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)
@@ -129,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
@@ -172,7 +257,8 @@ 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)
@@ -181,17 +267,32 @@ class IMAPServer:
 
             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
@@ -245,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
@@ -257,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
@@ -299,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
         
@@ -311,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:
@@ -318,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)