diff --git a/django_mailbox/migrations/0017_auto__add_field_message_cc_header__add_field_message_bcc_header.py b/django_mailbox/migrations/0017_auto__add_field_message_cc_header__add_field_message_bcc_header.py new file mode 100644 index 0000000..65aa137 --- /dev/null +++ b/django_mailbox/migrations/0017_auto__add_field_message_cc_header__add_field_message_bcc_header.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Message.cc_header' + db.add_column(u'django_mailbox_message', 'cc_header', + self.gf('django.db.models.fields.TextField')(default=''), + keep_default=False) + + # Adding field 'Message.bcc_header' + db.add_column(u'django_mailbox_message', 'bcc_header', + self.gf('django.db.models.fields.TextField')(default=''), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Message.cc_header' + db.delete_column(u'django_mailbox_message', 'cc_header') + + # Deleting field 'Message.bcc_header' + db.delete_column(u'django_mailbox_message', 'bcc_header') + + + models = { + u'django_mailbox.mailbox': { + 'Meta': {'object_name': 'Mailbox'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'from_email': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'uri': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}) + }, + u'django_mailbox.message': { + 'Meta': {'object_name': 'Message'}, + 'bcc_header': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'body': ('django.db.models.fields.TextField', [], {}), + 'cc_header': ('django.db.models.fields.TextField', [], {'default': "''"}), + 'encoded': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'in_reply_to': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'replies'", 'null': 'True', 'to': u"orm['django_mailbox.Message']"}), + 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': u"orm['django_mailbox.Mailbox']"}), + 'message_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'outgoing': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'processed': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'read': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'to_header': ('django.db.models.fields.TextField', [], {}) + }, + u'django_mailbox.messageattachment': { + 'Meta': {'object_name': 'MessageAttachment'}, + 'document': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'headers': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attachments'", 'null': 'True', 'to': u"orm['django_mailbox.Message']"}) + } + } + + complete_apps = ['django_mailbox'] \ No newline at end of file diff --git a/django_mailbox/models.py b/django_mailbox/models.py index f4b5dc6..0899239 100644 --- a/django_mailbox/models.py +++ b/django_mailbox/models.py @@ -15,7 +15,7 @@ from django.core.files.base import ContentFile from django.db import models from django_mailbox.transports import Pop3Transport, ImapTransport,\ MaildirTransport, MboxTransport, BabylTransport, MHTransport, \ - MMDFTransport + MMDFTransport, GmailTransport from django_mailbox.signals import message_received import six from six.moves.urllib.parse import parse_qs, unquote, urlparse @@ -175,6 +175,13 @@ class Mailbox(models.Model): ssl=self.use_ssl ) conn.connect(self.username, self.password) + elif self.type == "gmail": + conn = GmailTransport( + self.location, + port=self.port if self.port else None, + ssl=self.use_ssl + ) + conn.connect(self.username, self.password) elif self.type == 'maildir': conn = MaildirTransport(self.location) elif self.type == 'mbox': @@ -286,6 +293,10 @@ class Mailbox(models.Model): msg.from_header = convert_header_to_unicode(message['from']) if 'to' in message: msg.to_header = convert_header_to_unicode(message['to']) + if 'cc' in message: + msg.cc_header = convert_header_to_unicode(message['cc']) + if 'bcc' in message: + msg.bcc_header = convert_header_to_unicode(message['bcc']) msg.save() message = self._get_dehydrated_message(message, msg) msg.set_body(message.as_string()) @@ -351,6 +362,8 @@ class Message(models.Model): max_length=255, ) to_header = models.TextField() + cc_header = models.TextField(default='') + bcc_header = models.TextField(default='') outgoing = models.BooleanField( default=False, blank=True, @@ -398,6 +411,30 @@ class Message(models.Model): else: return [] + @property + def cc_addresses(self): + addresses = [] + for address in self.cc_header.split(','): + if address: + addresses.append( + parseaddr( + address + )[1].lower() + ) + return addresses + + @property + def bcc_addresses(self): + addresses = [] + for address in self.bcc_header.split(','): + if address: + addresses.append( + parseaddr( + address + )[1].lower() + ) + return addresses + @property def to_addresses(self): addresses = [] diff --git a/django_mailbox/transports/__init__.py b/django_mailbox/transports/__init__.py index fc17082..27b587e 100644 --- a/django_mailbox/transports/__init__.py +++ b/django_mailbox/transports/__init__.py @@ -1,4 +1,5 @@ from django_mailbox.transports.imap import ImapTransport +from django_mailbox.transports.gmail import GmailTransport from django_mailbox.transports.pop3 import Pop3Transport from django_mailbox.transports.maildir import MaildirTransport from django_mailbox.transports.mbox import MboxTransport diff --git a/django_mailbox/transports/gmail.py b/django_mailbox/transports/gmail.py new file mode 100644 index 0000000..8c90169 --- /dev/null +++ b/django_mailbox/transports/gmail.py @@ -0,0 +1,126 @@ +from imaplib import IMAP4, IMAP4_SSL + +from .base import EmailTransport, MessageParseError + + +class GmailTransport(EmailTransport): + def __init__(self, hostname, port=None, ssl=True): + self.hostname = hostname + self.port = port + self.exclusive = False + self.MAX_MSG_SIZE = 2000000 + if ssl: + self.transport = IMAP4_SSL + if not self.port: + self.port = 993 + else: + self.transport = IMAP4 + if not self.port: + self.port = 143 + + 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", 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 google_api import ( + get_google_access_token, + fetch_user_info) + + except ImportError: + raise ValueError( + "Install python-social-auth to use oauth2 auth for gmail" + ) + + try: + access_token = get_google_access_token(username) + google_email_address = fetch_user_info(username)[u'email'] + except TypeError: + # Sometimes we have to try again + access_token = get_google_access_token(username) + google_email_address = fetch_user_info(username)[u'email'] + + 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() + + def _get_all_message_ids(self): + response, message_ids = self.server.search(None, 'ALL', ) + return message_ids[0].split(' ') + + def _get_unread_message_ids(self): + response, message_ids = self.server.uid('search', None, 'UNSEEN', ) + return message_ids[0].split(' ') + + def _archive_message(self, uid): + # Move to "[Gmail]/All Mail" folder + pass + + def _trash_message(self, uid): + # Move to "[Gmail]/Trash" + pass + + def _delete_message(self, uid): + # add Deleted Flag + self.server.store(uid, "+FLAGS", "\\Deleted") + + def _get_small_message_ids(self, message_ids): + safe_message_ids = [] + + status, data = self.server.uid( + 'fetch', + ','.join(message_ids), + '(BODY.PEEK[HEADER] RFC822.SIZE BODYSTRUCTURE)' + ) + + for each_msg in data: + if isinstance(each_msg, tuple): + try: + metadata, structure = each_msg[0].split(' BODYSTRUCTURE ') + uid = metadata.split('(')[1].split(' ')[1] + size = metadata.split('(')[1].split(' ')[3] + if int(size) <= int(self.MAX_MSG_SIZE): + safe_message_ids.append(uid) + except ValueError as e: + # print "ValueError: %s working on %s" % (e, each_msg[0]) + # print each_msg + pass + return safe_message_ids + + def get_message(self): + # Fetch a list of message uids to process + if self.exclusive: + message_ids = self._get_all_message_ids() + else: + message_ids = self._get_unread_message_ids() + # print "There are %s messages: %s" % (len(message_ids), message_ids) + if self.MAX_MSG_SIZE: + message_ids = self._get_small_message_ids(message_ids) + + if not message_ids: + return + + for uid in message_ids: + try: + typ, msg_contents = self.server.uid('fetch', uid, '(RFC822)') + message = self.get_email_from_bytes(msg_contents[0][1]) + yield message + except MessageParseError: + continue + if self.exclusive: + self.server.store(uid, "+FLAGS", "\\Deleted") + self.server.expunge() + return diff --git a/django_mailbox/transports/google_api.py b/django_mailbox/transports/google_api.py new file mode 100644 index 0000000..9900b70 --- /dev/null +++ b/django_mailbox/transports/google_api.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/oauth2.py b/django_mailbox/transports/oauth2.py new file mode 100644 index 0000000..9ffbeb1 --- /dev/null +++ b/django_mailbox/transports/oauth2.py @@ -0,0 +1,335 @@ +#!/usr/bin/python +# +# Copyright 2012 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Performs client tasks for testing IMAP OAuth2 authentication. + +To use this script, you'll need to have registered with Google as an OAuth +application and obtained an OAuth client ID and client secret. +See http://code.google.com/apis/accounts/docs/OAuth2.html for instructions on +registering and for documentation of the APIs invoked by this code. + +This script has 3 modes of operation. + +1. The first mode is used to generate and authorize an OAuth2 token, the +first step in logging in via OAuth2. + + oauth2 --user=xxx@gmail.com \ + --client_id=1038[...].apps.googleusercontent.com \ + --client_secret=VWFn8LIKAMC-MsjBMhJeOplZ \ + --generate_oauth2_token + +The script will converse with Google and generate an oauth request +token, then present you with a URL you should visit in your browser to +authorize the token. Once you get the verification code from the Google +website, enter it into the script to get your OAuth access token. The output +from this command will contain the access token, a refresh token, and some +metadata about the tokens. The access token can be used until it expires, and +the refresh token lasts indefinitely, so you should record these values for +reuse. + +2. The script will generate new access tokens using a refresh token. + + oauth2 --user=xxx@gmail.com \ + --client_id=1038[...].apps.googleusercontent.com \ + --client_secret=VWFn8LIKAMC-MsjBMhJeOplZ \ + --refresh_token=1/Yzm6MRy4q1xi7Dx2DuWXNgT6s37OrP_DW_IoyTum4YA + +3. The script will generate an OAuth2 string that can be fed +directly to IMAP or SMTP. This is triggered with the --generate_oauth2_string +option. + + oauth2 --generate_oauth2_string --user=xxx@gmail.com \ + --access_token=ya29.AGy[...]ezLg + +The output of this mode will be a base64-encoded string. To use it, connect to a +IMAPFE and pass it as the second argument to the AUTHENTICATE command. + + a AUTHENTICATE XOAUTH2 a9sha9sfs[...]9dfja929dk== +""" + +import base64 +import imaplib +import json +from optparse import OptionParser +import smtplib +import sys +import urllib + + +def SetupOptionParser(): + # Usage message is the module's docstring. + parser = OptionParser(usage=__doc__) + parser.add_option('--generate_oauth2_token', + action='store_true', + dest='generate_oauth2_token', + help='generates an OAuth2 token for testing') + parser.add_option('--generate_oauth2_string', + action='store_true', + dest='generate_oauth2_string', + help='generates an initial client response string for ' + 'OAuth2') + parser.add_option('--client_id', + default=None, + help='Client ID of the application that is authenticating. ' + 'See OAuth2 documentation for details.') + parser.add_option('--client_secret', + default=None, + help='Client secret of the application that is ' + 'authenticating. See OAuth2 documentation for ' + 'details.') + parser.add_option('--access_token', + default=None, + help='OAuth2 access token') + parser.add_option('--refresh_token', + default=None, + help='OAuth2 refresh token') + parser.add_option('--scope', + default='https://mail.google.com/', + help='scope for the access token. Multiple scopes can be ' + 'listed separated by spaces with the whole argument ' + 'quoted.') + parser.add_option('--test_imap_authentication', + action='store_true', + dest='test_imap_authentication', + help='attempts to authenticate to IMAP') + parser.add_option('--test_smtp_authentication', + action='store_true', + dest='test_smtp_authentication', + help='attempts to authenticate to SMTP') + parser.add_option('--user', + default=None, + help='email address of user whose account is being ' + 'accessed') + return parser + + +# The URL root for accessing Google Accounts. +GOOGLE_ACCOUNTS_BASE_URL = 'https://accounts.google.com' + + +# Hardcoded dummy redirect URI for non-web apps. +REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob' + + +def AccountsUrl(command): + """Generates the Google Accounts URL. + + Args: + command: The command to execute. + + Returns: + A URL for the given command. + """ + return '%s/%s' % (GOOGLE_ACCOUNTS_BASE_URL, command) + + +def UrlEscape(text): + # See OAUTH 5.1 for a definition of which characters need to be escaped. + return urllib.quote(text, safe='~-._') + + +def UrlUnescape(text): + # See OAUTH 5.1 for a definition of which characters need to be escaped. + return urllib.unquote(text) + + +def FormatUrlParams(params): + """Formats parameters into a URL query string. + + Args: + params: A key-value map. + + Returns: + A URL query string version of the given parameters. + """ + param_fragments = [] + for param in sorted(params.iteritems(), key=lambda x: x[0]): + param_fragments.append('%s=%s' % (param[0], UrlEscape(param[1]))) + return '&'.join(param_fragments) + + +def GeneratePermissionUrl(client_id, scope='https://mail.google.com/'): + """Generates the URL for authorizing access. + + This uses the "OAuth2 for Installed Applications" flow described at + https://developers.google.com/accounts/docs/OAuth2InstalledApp + + Args: + client_id: Client ID obtained by registering your app. + scope: scope for access token, e.g. 'https://mail.google.com' + Returns: + A URL that the user should visit in their browser. + """ + params = {} + params['client_id'] = client_id + params['redirect_uri'] = REDIRECT_URI + params['scope'] = scope + params['response_type'] = 'code' + return '%s?%s' % (AccountsUrl('o/oauth2/auth'), + FormatUrlParams(params)) + + +def AuthorizeTokens(client_id, client_secret, authorization_code): + """Obtains OAuth access token and refresh token. + + This uses the application portion of the "OAuth2 for Installed Applications" + flow at https://developers.google.com/accounts/docs/OAuth2InstalledApp#handlingtheresponse + + Args: + client_id: Client ID obtained by registering your app. + client_secret: Client secret obtained by registering your app. + authorization_code: code generated by Google Accounts after user grants + permission. + Returns: + The decoded response from the Google Accounts server, as a dict. Expected + fields include 'access_token', 'expires_in', and 'refresh_token'. + """ + params = {} + params['client_id'] = client_id + params['client_secret'] = client_secret + params['code'] = authorization_code + params['redirect_uri'] = REDIRECT_URI + params['grant_type'] = 'authorization_code' + request_url = AccountsUrl('o/oauth2/token') + + response = urllib.urlopen(request_url, urllib.urlencode(params)).read() + return json.loads(response) + + +def RefreshToken(client_id, client_secret, refresh_token): + """Obtains a new token given a refresh token. + + See https://developers.google.com/accounts/docs/OAuth2InstalledApp#refresh + + Args: + client_id: Client ID obtained by registering your app. + client_secret: Client secret obtained by registering your app. + refresh_token: A previously-obtained refresh token. + Returns: + The decoded response from the Google Accounts server, as a dict. Expected + fields include 'access_token', 'expires_in', and 'refresh_token'. + """ + params = {} + params['client_id'] = client_id + params['client_secret'] = client_secret + params['refresh_token'] = refresh_token + params['grant_type'] = 'refresh_token' + request_url = AccountsUrl('o/oauth2/token') + + response = urllib.urlopen(request_url, urllib.urlencode(params)).read() + return json.loads(response) + + +def GenerateOAuth2String(username, access_token, base64_encode=True): + """Generates an IMAP OAuth2 authentication string. + + See https://developers.google.com/google-apps/gmail/oauth2_overview + + Args: + username: the username (email address) of the account to authenticate + access_token: An OAuth2 access token. + base64_encode: Whether to base64-encode the output. + + Returns: + The SASL argument for the OAuth2 mechanism. + """ + auth_string = 'user=%s\1auth=Bearer %s\1\1' % (username, access_token) + if base64_encode: + auth_string = base64.b64encode(auth_string) + return auth_string + + +def TestImapAuthentication(user, auth_string): + """Authenticates to IMAP with the given auth_string. + + Prints a debug trace of the attempted IMAP connection. + + Args: + user: The Gmail username (full email address) + auth_string: A valid OAuth2 string, as returned by GenerateOAuth2String. + Must not be base64-encoded, since imaplib does its own base64-encoding. + """ + print + imap_conn = imaplib.IMAP4_SSL('imap.gmail.com') + imap_conn.debug = 4 + imap_conn.authenticate('XOAUTH2', lambda x: auth_string) + imap_conn.select('INBOX') + + +def TestSmtpAuthentication(user, auth_string): + """Authenticates to SMTP with the given auth_string. + + Args: + user: The Gmail username (full email address) + auth_string: A valid OAuth2 string, not base64-encoded, as returned by + GenerateOAuth2String. + """ + print + smtp_conn = smtplib.SMTP('smtp.gmail.com', 587) + smtp_conn.set_debuglevel(True) + smtp_conn.ehlo('test') + smtp_conn.starttls() + smtp_conn.docmd('AUTH', 'XOAUTH2 ' + base64.b64encode(auth_string)) + + +def RequireOptions(options, *args): + missing = [arg for arg in args if getattr(options, arg) is None] + if missing: + print 'Missing options: %s' % ' '.join(missing) + sys.exit(-1) + + +def main(argv): + options_parser = SetupOptionParser() + (options, args) = options_parser.parse_args() + if options.refresh_token: + RequireOptions(options, 'client_id', 'client_secret') + response = RefreshToken(options.client_id, options.client_secret, + options.refresh_token) + print 'Access Token: %s' % response['access_token'] + print 'Access Token Expiration Seconds: %s' % response['expires_in'] + elif options.generate_oauth2_string: + RequireOptions(options, 'user', 'access_token') + print ('OAuth2 argument:\n' + + GenerateOAuth2String(options.user, options.access_token)) + elif options.generate_oauth2_token: + RequireOptions(options, 'client_id', 'client_secret') + print 'To authorize token, visit this url and follow the directions:' + print ' %s' % GeneratePermissionUrl(options.client_id, options.scope) + authorization_code = raw_input('Enter verification code: ') + response = AuthorizeTokens(options.client_id, options.client_secret, + authorization_code) + print 'Refresh Token: %s' % response['refresh_token'] + print 'Access Token: %s' % response['access_token'] + print 'Access Token Expiration Seconds: %s' % response['expires_in'] + elif options.test_imap_authentication: + RequireOptions(options, 'user', 'access_token') + TestImapAuthentication(options.user, + GenerateOAuth2String(options.user, options.access_token, + base64_encode=False)) + elif options.test_smtp_authentication: + RequireOptions(options, 'user', 'access_token') + TestSmtpAuthentication(options.user, + GenerateOAuth2String(options.user, options.access_token, + base64_encode=False)) + else: + options_parser.print_help() + print 'Nothing to do, exiting.' + return + + +if __name__ == '__main__': + main(sys.argv)