forked from mirror/django-mailbox
Merge pull request #18 from alexlovelltroy/add_gmail_auth
Adding gmail oauth2 authentication. Thanks @alexlovelltroy!
This commit is contained in:
commit
e50408e365
6 changed files with 215 additions and 9 deletions
126
django_mailbox/google_utils.py
Normal file
126
django_mailbox/google_utils.py
Normal file
|
|
@ -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
|
||||||
|
|
@ -15,7 +15,7 @@ from django.core.files.base import ContentFile
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django_mailbox.transports import Pop3Transport, ImapTransport,\
|
from django_mailbox.transports import Pop3Transport, ImapTransport,\
|
||||||
MaildirTransport, MboxTransport, BabylTransport, MHTransport, \
|
MaildirTransport, MboxTransport, BabylTransport, MHTransport, \
|
||||||
MMDFTransport
|
MMDFTransport, GmailImapTransport
|
||||||
from django_mailbox.signals import message_received
|
from django_mailbox.signals import message_received
|
||||||
import six
|
import six
|
||||||
from six.moves.urllib.parse import parse_qs, unquote, urlparse
|
from six.moves.urllib.parse import parse_qs, unquote, urlparse
|
||||||
|
|
@ -168,6 +168,14 @@ class Mailbox(models.Model):
|
||||||
archive=self.archive
|
archive=self.archive
|
||||||
)
|
)
|
||||||
conn.connect(self.username, self.password)
|
conn.connect(self.username, self.password)
|
||||||
|
elif self.type == 'gmail':
|
||||||
|
conn = GmailImapTransport(
|
||||||
|
self.location,
|
||||||
|
port=self.port if self.port else None,
|
||||||
|
ssl=True,
|
||||||
|
archive=self.archive
|
||||||
|
)
|
||||||
|
conn.connect(self.username, self.password)
|
||||||
elif self.type == 'pop3':
|
elif self.type == 'pop3':
|
||||||
conn = Pop3Transport(
|
conn = Pop3Transport(
|
||||||
self.location,
|
self.location,
|
||||||
|
|
|
||||||
|
|
@ -5,3 +5,4 @@ from django_mailbox.transports.mbox import MboxTransport
|
||||||
from django_mailbox.transports.babyl import BabylTransport
|
from django_mailbox.transports.babyl import BabylTransport
|
||||||
from django_mailbox.transports.mh import MHTransport
|
from django_mailbox.transports.mh import MHTransport
|
||||||
from django_mailbox.transports.mmdf import MMDFTransport
|
from django_mailbox.transports.mmdf import MMDFTransport
|
||||||
|
from django_mailbox.transports.gmail import GmailImapTransport
|
||||||
|
|
|
||||||
49
django_mailbox/transports/gmail.py
Normal file
49
django_mailbox/transports/gmail.py
Normal file
|
|
@ -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) as e:
|
||||||
|
print " Couldn't do oauth2 because %s" % 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()
|
||||||
|
|
@ -7,18 +7,19 @@ POP3 and IMAP as well as local file-based mailboxes.
|
||||||
|
|
||||||
.. table:: 'Protocol' Options
|
.. table:: 'Protocol' Options
|
||||||
|
|
||||||
============ ============== =================================================================================================================================================================
|
============ ================ =================================================================================================================================================================
|
||||||
Mailbox Type 'Protocol':// Notes
|
Mailbox Type 'Protocol':// Notes
|
||||||
============ ============== =================================================================================================================================================================
|
============ ================ =================================================================================================================================================================
|
||||||
POP3 ``pop3://`` Can also specify SSL with ``pop3+ssl://``
|
POP3 ``pop3://`` Can also specify SSL with ``pop3+ssl://``
|
||||||
IMAP ``imap://`` Can also specify SSL with ``imap+ssl://``, or specify a folder to save processed messages into by appending ``?archive=my_archive_folder`` to the end of the URI.
|
IMAP ``imap://`` Can also specify SSL with ``imap+ssl://``, or specify a folder to save processed messages into by appending ``?archive=my_archive_folder`` to the end of the URI.
|
||||||
|
Gmail IMAP ``gmail+ssl://`` Password is only used as a fallback if oauth2 fails
|
||||||
Maildir ``maildir://``
|
Maildir ``maildir://``
|
||||||
Mbox ``mbox://``
|
Mbox ``mbox://``
|
||||||
Babyl ``babyl://``
|
Babyl ``babyl://``
|
||||||
MH ``mh://``
|
MH ``mh://``
|
||||||
MMDF ``mmdf://``
|
MMDF ``mmdf://``
|
||||||
Piped Mail *empty* See :ref:`receiving-mail-from-exim4-or-postfix`
|
Piped Mail *empty* See :ref:`receiving-mail-from-exim4-or-postfix`
|
||||||
============ ============== =================================================================================================================================================================
|
============ ================ =================================================================================================================================================================
|
||||||
|
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
|
|
@ -61,6 +62,20 @@ would enter the following as your URI::
|
||||||
|
|
||||||
imap+ssl://youremailaddress%40gmail.com:1234@imap.gmail.com?archive=Archived
|
imap+ssl://youremailaddress%40gmail.com:1234@imap.gmail.com?archive=Archived
|
||||||
|
|
||||||
|
Gmail IMAP with Oauth2 authentication
|
||||||
|
-------------------------------------
|
||||||
|
|
||||||
|
Gmail supports using oauth2 for authentication_ which is more secure.
|
||||||
|
To handle the handshake and storing the credentials, use python-social-auth_.
|
||||||
|
|
||||||
|
.. _authentication: https://developers.google.com/gmail/xoauth2_protocol
|
||||||
|
.. _python-social-auth: http://psa.matiasaguirre.net/
|
||||||
|
|
||||||
|
The Gmail Mailbox is also a regular IMAP mailbox, but the password will be ignored if oauth2 succeeds. It can fall back to password as needed.
|
||||||
|
Build your URI accordingly::
|
||||||
|
|
||||||
|
gmail+ssl://youremailaddress%40gmail.com:oauth2@imap.gmail.com?archive=Archived
|
||||||
|
|
||||||
|
|
||||||
Local File-based Mailboxes
|
Local File-based Mailboxes
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|
|
||||||
9
setup.py
9
setup.py
|
|
@ -5,6 +5,10 @@ tests_require = [
|
||||||
'mock',
|
'mock',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
gmail_oauth2_require = [
|
||||||
|
'python-social-auth',
|
||||||
|
]
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='django-mailbox',
|
name='django-mailbox',
|
||||||
version='3.3',
|
version='3.3',
|
||||||
|
|
@ -16,7 +20,10 @@ setup(
|
||||||
author='Adam Coddington',
|
author='Adam Coddington',
|
||||||
author_email='me@adamcoddington.net',
|
author_email='me@adamcoddington.net',
|
||||||
tests_require=tests_require,
|
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',
|
test_suite='django_mailbox.runtests.runtests',
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Framework :: Django',
|
'Framework :: Django',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue