Skip to content

Commit 2540334

Browse files
committed
Added initial XOAUTH support for SMTP and IMAP. Not working quite yet.
1 parent a278cb7 commit 2540334

File tree

4 files changed

+109
-62
lines changed

4 files changed

+109
-62
lines changed

oauth2/__init__.py

Lines changed: 81 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,22 @@ def build_authenticate_header(realm=''):
6363
return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
6464

6565

66+
def build_xoauth_string(url, consumer, token=None):
67+
"""Build an XOAUTH string for use in SMTP/IMPA authentication."""
68+
request = oauth.Request.from_consumer_and_token(consumer, token,
69+
"GET", url)
70+
71+
signing_method = oauth.SignatureMethod_HMAC_SHA1()
72+
request.sign_request(signing_method, consumer, token)
73+
74+
params = []
75+
for k,v in sorted(request.iteritems()):
76+
if v is not None:
77+
params.append('%s="%s"' % (k, oauth.escape(v)))
78+
79+
return "%s %s %s" % ("GET", url, ','.join(params))
80+
81+
6682
def escape(s):
6783
"""Escape a URL including any /."""
6884
return urllib.quote(s, safe='~')
@@ -473,6 +489,69 @@ def _split_url_string(param_str):
473489
return parameters
474490

475491

492+
class Client(httplib2.Http):
493+
"""OAuthClient is a worker to attempt to execute a request."""
494+
495+
def __init__(self, consumer, token=None, cache=None, timeout=None,
496+
proxy_info=None):
497+
498+
if consumer is not None and not isinstance(consumer, Consumer):
499+
raise ValueError("Invalid consumer.")
500+
501+
if token is not None and not isinstance(token, Token):
502+
raise ValueError("Invalid token.")
503+
504+
self.consumer = consumer
505+
self.token = token
506+
self.method = SignatureMethod_HMAC_SHA1()
507+
508+
httplib2.Http.__init__(self, cache=cache, timeout=timeout,
509+
proxy_info=proxy_info)
510+
511+
def set_signature_method(self, method):
512+
if not isinstance(method, SignatureMethod):
513+
raise ValueError("Invalid signature method.")
514+
515+
self.method = method
516+
517+
def request(self, uri, method="GET", body=None, headers=None,
518+
redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None):
519+
DEFAULT_CONTENT_TYPE = 'application/x-www-form-urlencoded'
520+
521+
if not isinstance(headers, dict):
522+
headers = {}
523+
524+
is_multipart = method == 'POST' and headers.get('Content-Type',
525+
DEFAULT_CONTENT_TYPE) != DEFAULT_CONTENT_TYPE
526+
527+
if body and method == "POST" and not is_multipart:
528+
parameters = dict(parse_qsl(body))
529+
else:
530+
parameters = None
531+
532+
req = Request.from_consumer_and_token(self.consumer,
533+
token=self.token, http_method=method, http_url=uri,
534+
parameters=parameters)
535+
536+
req.sign_request(self.method, self.consumer, self.token)
537+
538+
if method == "POST":
539+
headers['Content-Type'] = headers.get('Content-Type',
540+
DEFAULT_CONTENT_TYPE)
541+
if is_multipart:
542+
headers.update(req.to_header())
543+
else:
544+
body = req.to_postdata()
545+
elif method == "GET":
546+
uri = req.to_url()
547+
else:
548+
headers.update(req.to_header())
549+
550+
return httplib2.Http.request(self, uri, method=method, body=body,
551+
headers=headers, redirections=redirections,
552+
connection_type=connection_type)
553+
554+
476555
class Server(object):
477556
"""A skeletal implementation of a service provider, providing protected
478557
resources to requests from authorized consumers.
@@ -564,68 +643,8 @@ def _check_timestamp(self, timestamp):
564643
lapsed = now - timestamp
565644
if lapsed > self.timestamp_threshold:
566645
raise Error('Expired timestamp: given %d and now %s has a '
567-
'greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold))
568-
569-
570-
class Client(httplib2.Http):
571-
"""OAuthClient is a worker to attempt to execute a request."""
572-
573-
def __init__(self, consumer, token=None, cache=None, timeout=None,
574-
proxy_info=None):
575-
576-
if consumer is not None and not isinstance(consumer, Consumer):
577-
raise ValueError("Invalid consumer.")
578-
579-
if token is not None and not isinstance(token, Token):
580-
raise ValueError("Invalid token.")
581-
582-
self.consumer = consumer
583-
self.token = token
584-
self.method = SignatureMethod_HMAC_SHA1()
585-
586-
httplib2.Http.__init__(self, cache=cache, timeout=timeout,
587-
proxy_info=proxy_info)
588-
589-
def set_signature_method(self, method):
590-
if not isinstance(method, SignatureMethod):
591-
raise ValueError("Invalid signature method.")
592-
593-
self.method = method
594-
595-
def request(self, uri, method="GET", body=None, headers=None,
596-
redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None):
597-
DEFAULT_CONTENT_TYPE = 'application/x-www-form-urlencoded'
598-
599-
if not isinstance(headers, dict):
600-
headers = {}
601-
602-
is_multipart = method == 'POST' and headers.get('Content-Type', DEFAULT_CONTENT_TYPE) != DEFAULT_CONTENT_TYPE
603-
604-
if body and method == "POST" and not is_multipart:
605-
parameters = dict(parse_qsl(body))
606-
else:
607-
parameters = None
608-
609-
req = Request.from_consumer_and_token(self.consumer, token=self.token,
610-
http_method=method, http_url=uri, parameters=parameters)
611-
612-
req.sign_request(self.method, self.consumer, self.token)
613-
614-
615-
if method == "POST":
616-
headers['Content-Type'] = headers.get('Content-Type', DEFAULT_CONTENT_TYPE)
617-
if is_multipart:
618-
headers.update(req.to_header())
619-
else:
620-
body = req.to_postdata()
621-
elif method == "GET":
622-
uri = req.to_url()
623-
else:
624-
headers.update(req.to_header())
625-
626-
return httplib2.Http.request(self, uri, method=method, body=body,
627-
headers=headers, redirections=redirections,
628-
connection_type=connection_type)
646+
'greater difference than threshold %d' % (timestamp, now,
647+
self.timestamp_threshold))
629648

630649

631650
class SignatureMethod(object):

oauth2/clients/__init__.py

Whitespace-only changes.

oauth2/clients/imaplib.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import oauth2
2+
import imaplib
3+
4+
class IMAP4_SSL(imaplib.IMAP4_SSL):
5+
def authenticate(self, url, consumer, token):
6+
if consumer is not None and not isinstance(consumer, oauth2.Consumer):
7+
raise ValueError("Invalid consumer.")
8+
9+
if token is not None and not isinstance(token, oauth2.Token):
10+
raise ValueError("Invalid token.")
11+
12+
imaplib.IMAP4_SSL.authenticate(self, 'XOAUTH',
13+
lambda x: oauth2.build_xoauth_string(url, consumer, token))
14+

oauth2/clients/smtplib.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import oauth2
2+
import smtplib
3+
4+
class SMTP(smtplib.SMTP, oauth2.XOAuth):
5+
def authenticate(self, url, consumer, token):
6+
if consumer is not None and not isinstance(consumer, oauth2.Consumer):
7+
raise ValueError("Invalid consumer.")
8+
9+
if token is not None and not isinstance(token, oauth2.Token):
10+
raise ValueError("Invalid token.")
11+
12+
smtp_conn.docmd('AUTH', 'XOAUTH %s' + \
13+
base64.b64encode(oauth2.build_xoauth_string(url, consumer, token))
14+

0 commit comments

Comments
 (0)