[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