Merge branch 'Frizlab-gmail-oauth-2' into next
This commit is contained in:
commit
ebb2124dbb
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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),
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user