[PATCH] Add support for SASL XOAUTH2 mechanism

Mike Jager mike at mikej.net.nz
Thu Nov 8 12:24:48 GMT 2012


Signed-off-by: Mike Jager <mike at mikej.net.nz>
---
 offlineimap.conf                |   17 ++++++++++++++++
 offlineimap/imapserver.py       |   42 ++++++++++++++++++++++++++++++++++++++-
 offlineimap/repository/Gmail.py |   10 ++++++++++
 offlineimap/repository/IMAP.py  |   14 +++++++++++++
 4 files changed, 82 insertions(+), 1 deletion(-)

diff --git a/offlineimap.conf b/offlineimap.conf
index fccceab..1b6660c 100644
--- a/offlineimap.conf
+++ b/offlineimap.conf
@@ -372,6 +372,23 @@ remoteuser = username
 #    This method can be used to design more elaborate setups, e.g. by
 #    querying the gnome-keyring via its python bindings.
 
+# As an alternative to authenticating with a password, the SASL XOAUTH2
+# mechanism can be used. The specification for this is available at:
+# https://developers.google.com/google-apps/gmail/xoauth2_protocol
+#
+# This support was originally added for use with Gmail. Documentation for
+# Google's OAuth 2.0 endpoint is available at:
+# https://developers.google.com/accounts/docs/OAuth2InstalledApp
+#
+# First, enable OAuth 2.0 authentication
+# oauth2 = True
+#
+# If OAuth 2.0 is in use, the following additional configuration is required:
+# oauth2_url (a default URL is provided for repositories of type "Gmail".)
+# oauth2_client_id
+# oauth2_client_secret
+# oauth2_refresh_token
+
 ########## Advanced settings
 
 # Some IMAP servers need a "reference" which often refers to the "folder
diff --git a/offlineimap/imapserver.py b/offlineimap/imapserver.py
index 22c5c16..1c19e3d 100644
--- a/offlineimap/imapserver.py
+++ b/offlineimap/imapserver.py
@@ -24,6 +24,8 @@ import socket
 import base64
 import time
 import errno
+import json
+import urllib
 from sys import exc_info
 from socket import gaierror
 from ssl import SSLError, cert_time_to_seconds
@@ -56,6 +58,11 @@ class IMAPServer:
         self.usessl = repos.getssl()
         self.username = None if self.tunnel else repos.getuser()
         self.password = None
+        self.oauth2 = repos.getoauth2()
+        self.oauth2url = repos.getoauth2url() if self.oauth2 else None
+        self.oauth2clientid = repos.getoauth2clientid() if self.oauth2 else None
+        self.oauth2clientsecret = repos.getoauth2clientsecret() if self.oauth2 else None
+        self.oauth2refreshtoken = repos.getoauth2refreshtoken() if self.oauth2 else None
         self.passworderror = None
         self.goodpassword = None
         self.hostname = None if self.tunnel else repos.gethost()
@@ -127,6 +134,29 @@ class IMAPServer:
         self.ui.debug('imap', 'md5handler: returning %s' % retval)
         return retval
 
+    def oauth2handler(self, response):
+        data = {
+            'client_id': self.oauth2clientid,
+            'client_secret': self.oauth2clientsecret,
+            'refresh_token': self.oauth2refreshtoken,
+            'grant_type': 'refresh_token'
+        }
+        try:
+            http = urllib.urlopen(self.oauth2url, urllib.urlencode(data))
+        except IOError:
+            raise OfflineImapError("Couldn't connect to OAUTH2 server",
+                OfflineImapError.ERROR.REPO)
+
+        try:
+            http = json.load(http)
+            retval = 'user=%s\001auth=Bearer %s\001\001' % (self.username, http['access_token'])
+        except ValueError, KeyError:
+            raise OfflineImapError("Malformed JSON response from OAUTH2 server",
+                OfflineImapError.ERROR.REPO)
+
+        self.ui.debug('imap', 'oauth2handler: returning %s' % retval)
+        return retval
+
     def plainauth(self, imapobj):
         self.ui.debug('imap', 'Attempting plain authentication')
         imapobj.login(self.username, self.getpassword())
@@ -248,7 +278,17 @@ class IMAPServer:
                                               'Using STARTTLS connection')
                                 imapobj.starttls()
 
-                            if 'AUTH=CRAM-MD5' in imapobj.capabilities:
+                            if 'AUTH=XOAUTH2' in imapobj.capabilities and self.oauth2:
+                                self.ui.debug('imap',
+                                           'Attempting XOAUTH2 authentication')
+                                try:
+                                    imapobj.authenticate('XOAUTH2',
+                                                         self.oauth2handler)
+                                except imapobj.error as val:
+                                    raise OfflineImapError("Error while XOAUTH2ing:"
+                                        "%s" % (val),
+                                        OfflineImapError.ERROR.REPO)
+                            elif 'AUTH=CRAM-MD5' in imapobj.capabilities:
                                 self.ui.debug('imap',
                                            'Attempting CRAM-MD5 authentication')
                                 try:
diff --git a/offlineimap/repository/Gmail.py b/offlineimap/repository/Gmail.py
index f4260c0..356e8f0 100644
--- a/offlineimap/repository/Gmail.py
+++ b/offlineimap/repository/Gmail.py
@@ -17,6 +17,7 @@
 
 from offlineimap.repository.IMAP import IMAPRepository
 from offlineimap import folder, OfflineImapError
+import ConfigParser
 
 class GmailRepository(IMAPRepository):
     """Gmail IMAP repository.
@@ -28,6 +29,8 @@ class GmailRepository(IMAPRepository):
     HOSTNAME = "imap.gmail.com"
     # Gmail IMAP server port
     PORT = 993
+    # Google Account OAUTH2 URL
+    OAUTH2_URL = "https://accounts.google.com/o/oauth2/token"
     
     def __init__(self, reposname, account):
         """Initialize a GmailRepository object."""
@@ -57,6 +60,13 @@ class GmailRepository(IMAPRepository):
     def getpreauthtunnel(self):
         return None
 
+    def getoauth2url(self):
+        try:
+            return super(GmailRepository, self).getoauth2url()
+        except ConfigParser.NoOptionError:
+            # nothing was configured, return hardcoded one
+            return GmailRepository.OAUTH2_URL
+
     def getfolder(self, foldername):
         return self.getfoldertype()(self.imapserver, foldername,
                                     self)
diff --git a/offlineimap/repository/IMAP.py b/offlineimap/repository/IMAP.py
index be8c858..070fc82 100644
--- a/offlineimap/repository/IMAP.py
+++ b/offlineimap/repository/IMAP.py
@@ -247,6 +247,20 @@ class IMAPRepository(BaseRepository):
         # no strategy yielded a password!
         return None
 
+    def getoauth2(self):
+        return self.getconfboolean('oauth2', False)
+
+    def getoauth2url(self):
+        return self.getconf('oauth2_url')
+
+    def getoauth2clientid(self):
+        return self.getconf('oauth2_client_id')
+
+    def getoauth2clientsecret(self):
+        return self.getconf('oauth2_client_secret')
+
+    def getoauth2refreshtoken(self):
+        return self.getconf('oauth2_refresh_token')
 
     def getfolder(self, foldername):
         return self.getfoldertype()(self.imapserver, foldername, self)
-- 
1.7.9.6 (Apple Git-31.1)




More information about the OfflineIMAP-project mailing list