]> code.delx.au - offlineimap/blobdiff - offlineimap/imapserver.py
Add support for ssl client certificates
[offlineimap] / offlineimap / imapserver.py
index 6ea25ebd5bf1912b47e4f0f0ca597eaf7d61148c..7d99e19de2c7e9d6b13aa36f88db25ea425ab62a 100644 (file)
@@ -21,7 +21,16 @@ from offlineimap import imaplibutil, imaputil, threadutil
 from offlineimap.ui import UIBase
 from threading import *
 import thread, hmac, os
+import base64
 
+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 +41,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)
@@ -51,26 +60,31 @@ 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):
+class UsefulIMAP4_SSL(UsefulIMAPMixIn, imaplibutil.WrappedIMAP4_SSL):
     def open(self, host = '', port = imaplib.IMAP4_SSL_PORT):
         imaplibutil.new_open_ssl(self, host, port)
 
 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):
         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 +99,14 @@ class IMAPServer:
         self.semaphore = BoundedSemaphore(self.maxconnections)
         self.connectionlock = Lock()
         self.reference = reference
+        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 +129,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 +150,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 +220,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 +230,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
@@ -299,6 +363,8 @@ 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()
         server = None
         password = None
@@ -318,4 +384,6 @@ class ConfigedIMAPServer(IMAPServer):
             IMAPServer.__init__(self, self.config, self.repos.getname(),
                                 user, password, host, port, ssl,
                                 self.repos.getmaxconnections(),
-                                reference = reference)
+                                reference = reference,
+                                sslclientcert = sslclientcert,
+                                sslclientkey = sslclientkey)