From cc4e72305d1f441ec45a72453c11110c605efad3 Mon Sep 17 00:00:00 2001 From: Alex Lovell-Troy Date: Wed, 28 May 2014 20:10:45 -0600 Subject: [PATCH] Adding gmail oauth2 authentication python-social-auth is an optional dependency --- django_mailbox/google_utils.py | 126 ++++++++++++++++++++++++++ django_mailbox/transports/__init__.py | 1 + django_mailbox/transports/gmail.py | 49 ++++++++++ setup.py | 9 +- 4 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 django_mailbox/google_utils.py create mode 100644 django_mailbox/transports/gmail.py diff --git a/django_mailbox/google_utils.py b/django_mailbox/google_utils.py new file mode 100644 index 0000000..9900b70 --- /dev/null +++ b/django_mailbox/google_utils.py @@ -0,0 +1,126 @@ +from social.apps.django_app.default.models import UserSocialAuth +import requests +from django.conf import settings + + +class AccessTokenNotFound(Exception): + pass + + +class RefreshTokenNotFound(Exception): + pass + + +def get_google_consumer_key(): + return settings.SOCIAL_AUTH_GOOGLE_OAUTH2_KEY + + +def get_google_consumer_secret(): + return settings.SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET + + +def get_google_access_token(email): + # TODO: This should be cacheable + try: + me = UserSocialAuth.objects.get(uid=email, provider="google-oauth2") + return me.extra_data['access_token'] + except (UserSocialAuth.DoesNotExist, KeyError): + raise AccessTokenNotFound + + +def update_google_extra_data(email, extra_data): + try: + me = UserSocialAuth.objects.get(uid=email, provider="google-oauth2") + me.extra_data = extra_data + me.save() + except (UserSocialAuth.DoesNotExist, KeyError): + raise AccessTokenNotFound + + +def get_google_refresh_token(email): + try: + me = UserSocialAuth.objects.get(uid=email, provider="google-oauth2") + return me.extra_data['refresh_token'] + except (UserSocialAuth.DoesNotExist, KeyError): + raise RefreshTokenNotFound + + +def google_api_get(email, url): + headers = dict( + Authorization="Bearer %s" % get_google_access_token(email), + ) + r = requests.get(url, headers=headers) + print "I got a %s" % r.status_code + if r.status_code == 401: + # Go use the refresh token + refresh_authorization(email) + r = requests.get(url, headers=headers) + print "I got a %s" % r.status_code + if r.status_code == 200: + try: + return r.json() + except ValueError: + print "returning text" + return r.text + + +def google_api_post(email, url, post_data, authorized=True): + # TODO: Make this a lot less ugly. especially the 401 handling + headers = dict() + if authorized is True: + headers.update(dict( + Authorization="Bearer %s" % get_google_access_token(email), + )) + r = requests.post(url, headers=headers, data=post_data) + if r.status_code == 401: + refresh_authorization(email) + r = requests.post(url, headers=headers, data=post_data) + if r.status_code == 200: + try: + return r.json() + except ValueError: + print "returning text" + return r.text + + +def refresh_authorization(email): + refresh_token = get_google_refresh_token(email) + post_data = dict( + refresh_token=refresh_token, + client_id=get_google_consumer_key(), + client_secret=get_google_consumer_secret(), + grant_type='refresh_token', + ) + results = google_api_post( + email, + "https://accounts.google.com/o/oauth2/token?access_type=offline", + post_data, + authorized=False) + results.update({'refresh_token': refresh_token}) + update_google_extra_data(email, results) + + +def fetch_user_info(email): + result = google_api_get( + email, + "https://www.googleapis.com/oauth2/v1/userinfo?alt=json" + ) + return result + + +def fetch_google_contacts(email, limit=10000): + result = google_api_get( + email, + "https://www.google.com/m8/feeds/contacts/default/full?v=3.0&alt=json&max-results=%s" % limit + ) + entries = result['feed']['entry'] + valid_entries = [x for x in entries if u'gd$email' in x.keys() and u'gd$name' in x.keys()] + contacts = [] + for each in valid_entries: + try: + name = each[u'gd$name'][u'gd$fullName'][u'$t'] + except KeyError: + name = None + for each_email in each[u'gd$email']: + contacts.append(dict(name=name, email=each_email[u'address'])) + return contacts diff --git a/django_mailbox/transports/__init__.py b/django_mailbox/transports/__init__.py index fc17082..03b31f0 100644 --- a/django_mailbox/transports/__init__.py +++ b/django_mailbox/transports/__init__.py @@ -5,3 +5,4 @@ from django_mailbox.transports.mbox import MboxTransport from django_mailbox.transports.babyl import BabylTransport from django_mailbox.transports.mh import MHTransport from django_mailbox.transports.mmdf import MMDFTransport +from django_mailbox.transports.gmail import GmailImapTransport diff --git a/django_mailbox/transports/gmail.py b/django_mailbox/transports/gmail.py new file mode 100644 index 0000000..223ee51 --- /dev/null +++ b/django_mailbox/transports/gmail.py @@ -0,0 +1,49 @@ +from django_mailbox.transports.imap import ImapTransport + +class GmailImapTransport(ImapTransport): + + def connect(self, username, password): + # Try to use oauth2 first. It's much safer + try: + self._connect_oauth(username) + except (TypeError, ValueError), e: + print " Couldn't do oauth2", e + self.server = self.transport(self.hostname, self.port) + typ, msg = self.server.login(username, password) + self.server.select() + + def _connect_oauth(self, username): + # username should be an email address that has already been authorized + # for gmail access + try: + from django_mailbox.google_utils import ( + get_google_access_token, + fetch_user_info, + AccessTokenNotFound, + ) + except ImportError: + raise ValueError( + "Install python-social-auth to use oauth2 auth for gmail" + ) + + access_token = None + while access_token is None: + try: + access_token = get_google_access_token(username) + google_email_address = fetch_user_info(username)['email'] + except TypeError: + # This means that the google process took too long + # Trying again is the right thing to do + pass + except AccessTokenNotFound: + raise ValueError( + "No Token available in python-social-auth for %s" % username + ) + + auth_string = 'user=%s\1auth=Bearer %s\1\1' % ( + google_email_address, + access_token + ) + self.server = self.transport(self.hostname, self.port) + self.server.authenticate('XOAUTH2', lambda x: auth_string) + self.server.select() diff --git a/setup.py b/setup.py index c080a93..1f3800b 100755 --- a/setup.py +++ b/setup.py @@ -5,6 +5,10 @@ tests_require = [ 'mock', ] +gmail_oauth2_require = [ + 'python-social-auth', +] + setup( name='django-mailbox', version='3.3', @@ -16,7 +20,10 @@ setup( author='Adam Coddington', author_email='me@adamcoddington.net', tests_require=tests_require, - extras_require={'test': tests_require}, + extras_require={ + 'test': tests_require, + 'gmail-oauth2': gmail_oauth2_require + }, test_suite='django_mailbox.runtests.runtests', classifiers=[ 'Framework :: Django',