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