Merge branch 'Frizlab-gmail-oauth-2' into next

This commit is contained in:
Nicolas Sebrecht 2015-10-06 14:25:15 +02:00
commit ebb2124dbb
5 changed files with 118 additions and 4 deletions

View File

@ -59,6 +59,7 @@ Bugs, issues and contributions can be requested to both the mailing list or the
* Python v2.7 * Python v2.7
* Python SQlite (optional while recommended) * Python SQlite (optional while recommended)
* Python json and urllib (used for XOAuth2 authentication)
## Documentation ## Documentation

View File

@ -688,9 +688,42 @@ remoteuser = username
# limitations, if GSSAPI is set, it will be tried first, no matter where it was # limitations, if GSSAPI is set, it will be tried first, no matter where it was
# specified in the list. # specified in the list.
# #
#auth_mechanisms = GSSAPI, CRAM-MD5, PLAIN, LOGIN #auth_mechanisms = GSSAPI, CRAM-MD5, XOAUTH2, PLAIN, LOGIN
# This option stands in the [Repository RemoteExample] section.
#
# XOAuth2 authentication (for instance, to use with Gmail).
#
# This feature is currently EXPERIMENTAL (tested on Gmail only, but should work
# with type = IMAP for compatible servers).
#
# Mandatory parameters are "oauth2_client_id", "oauth2_client_secret" and
# "oauth2_refresh_token". See below to learn how to get those.
#
# Specify the OAuth2 client id and secret to use for the connection..
# Here's how to register an OAuth2 client for Gmail, as of 10-2-2016:
# - Go to the Google developer console
# https://console.developers.google.com/project
# - Create a new project
# - In API & Auth, select Credentials
# - Setup the OAuth Consent Screen
# - Then add Credentials of type OAuth 2.0 Client ID
# - Choose application type Other; type in a name for your client
# - You now have a client ID and client secret
#
#oauth2_client_id = YOUR_CLIENT_ID
#oauth2_client_secret = YOUR_CLIENT_SECRET
# Specify the refresh token to use for the connection to the mail server.
# Here's an example of a way to get a refresh token:
# - Clone this project: https://github.com/google/gmail-oauth2-tools
# - Type the following command-line in a terminal and follow the instructions
# python python/oauth2.py --generate_oauth2_token \
# --client_id=YOUR_CLIENT_ID --client_secret=YOUR_CLIENT_SECRET
#
#oauth2_refresh_token = REFRESH_TOKEN
########## Passwords ########## Passwords
# There are six ways to specify the password for the IMAP server: # There are six ways to specify the password for the IMAP server:

View File

@ -19,6 +19,10 @@ from threading import Lock, BoundedSemaphore, Thread, Event, currentThread
import hmac import hmac
import socket import socket
import base64 import base64
import json
import urllib
import time import time
import errno import errno
from sys import exc_info from sys import exc_info
@ -89,6 +93,12 @@ class IMAPServer:
self.fingerprint = repos.get_ssl_fingerprint() self.fingerprint = repos.get_ssl_fingerprint()
self.sslversion = repos.getsslversion() self.sslversion = repos.getsslversion()
self.oauth2_refresh_token = repos.getoauth2_refresh_token()
self.oauth2_client_id = repos.getoauth2_client_id()
self.oauth2_client_secret = repos.getoauth2_client_secret()
self.oauth2_request_url = repos.getoauth2_request_url()
self.oauth2_access_token = None
self.delim = None self.delim = None
self.root = None self.root = None
self.maxconnections = repos.getmaxconnections() self.maxconnections = repos.getmaxconnections()
@ -199,7 +209,33 @@ class IMAPServer:
return retval return retval
# XXX: describe function def __xoauth2handler(self, response):
if self.oauth2_refresh_token is None:
return None
if self.oauth2_access_token is None:
# need to move these to config
# generate new access token
params = {}
params['client_id'] = self.oauth2_client_id
params['client_secret'] = self.oauth2_client_secret
params['refresh_token'] = self.oauth2_refresh_token
params['grant_type'] = 'refresh_token'
self.ui.debug('imap', 'xoauth2handler: url "%s"' % self.oauth2_request_url)
self.ui.debug('imap', 'xoauth2handler: params "%s"' % params)
response = urllib.urlopen(self.oauth2_request_url, urllib.urlencode(params)).read()
resp = json.loads(response)
self.ui.debug('imap', 'xoauth2handler: response "%s"' % resp)
self.oauth2_access_token = resp['access_token']
self.ui.debug('imap', 'xoauth2handler: access_token "%s"' % self.oauth2_access_token)
auth_string = 'user=%s\1auth=Bearer %s\1\1' % (self.username, self.oauth2_access_token)
#auth_string = base64.b64encode(auth_string)
self.ui.debug('imap', 'xoauth2handler: returning "%s"' % auth_string)
return auth_string
def __gssauth(self, response): def __gssauth(self, response):
data = base64.b64encode(response) data = base64.b64encode(response)
try: try:
@ -283,6 +319,10 @@ class IMAPServer:
imapobj.authenticate('PLAIN', self.__plainhandler) imapobj.authenticate('PLAIN', self.__plainhandler)
return True return True
def __authn_xoauth2(self, imapobj):
imapobj.authenticate('XOAUTH2', self.__xoauth2handler)
return True
def __authn_login(self, imapobj): def __authn_login(self, imapobj):
# Use LOGIN command, unless LOGINDISABLED is advertized # Use LOGIN command, unless LOGINDISABLED is advertized
# (per RFC 2595) # (per RFC 2595)
@ -314,6 +354,7 @@ class IMAPServer:
auth_methods = { auth_methods = {
"GSSAPI": (self.__authn_gssapi, False, True), "GSSAPI": (self.__authn_gssapi, False, True),
"CRAM-MD5": (self.__authn_cram_md5, True, True), "CRAM-MD5": (self.__authn_cram_md5, True, True),
"XOAUTH2": (self.__authn_xoauth2, True, True),
"PLAIN": (self.__authn_plain, True, True), "PLAIN": (self.__authn_plain, True, True),
"LOGIN": (self.__authn_login, True, False), "LOGIN": (self.__authn_login, True, False),
} }

View File

@ -29,6 +29,8 @@ class GmailRepository(IMAPRepository):
# Gmail IMAP server port # Gmail IMAP server port
PORT = 993 PORT = 993
OAUTH2_URL = 'https://accounts.google.com/o/oauth2/token'
def __init__(self, reposname, account): def __init__(self, reposname, account):
"""Initialize a GmailRepository object.""" """Initialize a GmailRepository object."""
# Enforce SSL usage # Enforce SSL usage
@ -49,6 +51,18 @@ class GmailRepository(IMAPRepository):
self._host = GmailRepository.HOSTNAME self._host = GmailRepository.HOSTNAME
return self._host return self._host
def getoauth2_request_url(self):
"""Return the server name to connect to.
Gmail implementation first checks for the usual IMAP settings
and falls back to imap.gmail.com if not specified."""
try:
return super(GmailRepository, self).getoauth2_request_url()
except OfflineImapError:
# nothing was configured, cache and return hardcoded one
self._oauth2_request_url = GmailRepository.OAUTH2_URL
return self._oauth2_request_url
def getport(self): def getport(self):
return GmailRepository.PORT return GmailRepository.PORT

View File

@ -34,6 +34,7 @@ class IMAPRepository(BaseRepository):
BaseRepository.__init__(self, reposname, account) BaseRepository.__init__(self, reposname, account)
# self.ui is being set by the BaseRepository # self.ui is being set by the BaseRepository
self._host = None self._host = None
self._oauth2_request_url = None
self.imapserver = imapserver.IMAPServer(self) self.imapserver = imapserver.IMAPServer(self)
self.folders = None self.folders = None
# Only set the newmail_hook in an IMAP repository. # Only set the newmail_hook in an IMAP repository.
@ -130,12 +131,12 @@ class IMAPRepository(BaseRepository):
return self.getconf('remote_identity', default=None) return self.getconf('remote_identity', default=None)
def get_auth_mechanisms(self): def get_auth_mechanisms(self):
supported = ["GSSAPI", "CRAM-MD5", "PLAIN", "LOGIN"] supported = ["GSSAPI", "XOAUTH2", "CRAM-MD5", "PLAIN", "LOGIN"]
# Mechanisms are ranged from the strongest to the # Mechanisms are ranged from the strongest to the
# weakest ones. # weakest ones.
# TODO: we need DIGEST-MD5, it must come before CRAM-MD5 # TODO: we need DIGEST-MD5, it must come before CRAM-MD5
# TODO: due to the chosen-plaintext resistance. # TODO: due to the chosen-plaintext resistance.
default = ["GSSAPI", "CRAM-MD5", "PLAIN", "LOGIN"] default = ["GSSAPI", "XOAUTH2", "CRAM-MD5", "PLAIN", "LOGIN"]
mechs = self.getconflist('auth_mechanisms', r',\s*', mechs = self.getconflist('auth_mechanisms', r',\s*',
default) default)
@ -257,6 +258,30 @@ class IMAPRepository(BaseRepository):
value = self.getconf('cert_fingerprint', "") value = self.getconf('cert_fingerprint', "")
return [f.strip().lower() for f in value.split(',') if f] return [f.strip().lower() for f in value.split(',') if f]
def getoauth2_request_url(self):
if self._oauth2_request_url: # use cached value if possible
return self._oauth2_request_url
oauth2_request_url = self.getconf('oauth2_request_url', None)
if oauth2_request_url != None:
self._oauth2_request_url = oauth2_request_url
return self._oauth2_request_url
# no success
raise OfflineImapError("No remote oauth2_request_url for repository "\
"'%s' specified." % self,
OfflineImapError.ERROR.REPO)
return self.getconf('oauth2_request_url', None)
def getoauth2_refresh_token(self):
return self.getconf('oauth2_refresh_token', None)
def getoauth2_client_id(self):
return self.getconf('oauth2_client_id', None)
def getoauth2_client_secret(self):
return self.getconf('oauth2_client_secret', None)
def getpreauthtunnel(self): def getpreauthtunnel(self):
return self.getconf('preauthtunnel', None) return self.getconf('preauthtunnel', None)