From 260d1047b18f74fd21f6451dc82b60ef94b50578 Mon Sep 17 00:00:00 2001 From: Maykel Moya Date: Sat, 30 May 2020 04:05:34 +0200 Subject: [PATCH] Add support for OAuth2 accounts --- src/cursed_delta/__init__.py | 40 ++++++++++++---- src/cursed_delta/oauth2.py | 91 ++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 9 deletions(-) create mode 100644 src/cursed_delta/oauth2.py diff --git a/src/cursed_delta/__init__.py b/src/cursed_delta/__init__.py index 6d3450d..edf70c0 100644 --- a/src/cursed_delta/__init__.py +++ b/src/cursed_delta/__init__.py @@ -6,9 +6,11 @@ import os import sys from .event import AccountPlugin +from .oauth2 import is_oauth2, get_authz_code from .ui import CursedDelta -from deltachat import Account, eventlogger +from deltachat import Account, events +import deltachat.const __version__ = '0.1.0' @@ -262,6 +264,9 @@ def main(): "--set-conf", action="store", help="set config option", nargs=2) parser.add_argument( "--get-conf", action="store", help="get config option") + parser.add_argument( + "--port", action="store", help="port to listen for oauth2 callback", + type=int, default='8383') args = parser.parse_args(argv[1:]) @@ -272,18 +277,35 @@ def main(): return if args.show_ffi: - log = eventlogger.FFIEventLogger(ac, "CursedDelta") + log = events.FFIEventLogger(ac, "CursedDelta") ac.add_account_plugin(log) if not ac.is_configured(): - assert args.email and args.password, ( - "you must specify --email and --password once to" - " configure this database/account" + assert args.email, ( + "you must specify --email once" + " to configure this database/account" ) ac.set_config("addr", args.email) - ac.set_config("mail_pw", args.password) - ac.set_config("mvbox_watch", "0") - ac.set_config("sentbox_watch", "0") + + if is_oauth2(ac, args.email): + authz_code = get_authz_code(ac, args.email, args.port) + + ac.set_config("mail_pw", authz_code) + + flags = ac.get_config('server_flags') + flags = int(flags) if flags else 0 + flags |= deltachat.const.DC_LP_AUTH_OAUTH2 + ac.set_config('server_flags', str(flags)) + else: + assert args.password, ( + "you must specify --password once" + " to configure this database/account" + ) + ac.set_config("mail_pw", args.email) + ac.set_config("mvbox_watch", "0") + ac.set_config("sentbox_watch", "0") + + ac.configure() if args.set_conf: ac.set_config(*args.set_conf) @@ -291,7 +313,7 @@ def main(): account = AccountPlugin(ac) ac.add_account_plugin(account) - ac.start() + ac.start_io() keymap = get_keymap() theme = get_theme() diff --git a/src/cursed_delta/oauth2.py b/src/cursed_delta/oauth2.py new file mode 100644 index 0000000..fb63c5e --- /dev/null +++ b/src/cursed_delta/oauth2.py @@ -0,0 +1,91 @@ +from itertools import count +from urllib.parse import parse_qs, urlparse +import webbrowser +import wsgiref.simple_server + +from deltachat.capi import ffi + + +def is_oauth2(ac, addr): + return get_oauth2_url(ac, addr, '') is not None + + +def get_oauth2_url(ac, addr, redirect_uri): + buf = ffi.dlopen(None).dc_get_oauth2_url( + ac._dc_context, + addr.encode('utf-8'), + redirect_uri.encode('utf-8')) + + if buf == ffi.NULL: + return None + + chars = [] + for i in count(): + if buf[i] == b'\x00': + break + chars.append(ord(buf[i])) + + return bytes(chars).decode('utf-8') + + +# borrowed from google-auth-library-python-oauthlib +def get_authz_code(ac, addr, port, + authorization_prompt_message=None, + success_message=None, + open_browser=True): + """Run a local server for receiving the authorization code and return it""" + + if authorization_prompt_message is None: + authorization_prompt_message = "Please visit this URL to authorize this application: {url}" + + if success_message is None: + success_message = "The authorization code was received. You may close this window." + + auth_url = get_oauth2_url(ac, addr, f'http://127.0.0.1:{port}/') + + wsgi_app = _RedirectWSGIApp(success_message) + local_server = wsgiref.simple_server.make_server( + '127.0.0.1', port, wsgi_app + ) + + if open_browser: + webbrowser.open(auth_url, new=1, autoraise=True) + + print(authorization_prompt_message.format(url=auth_url)) + + local_server.handle_request() + + authorization_response = wsgi_app.last_request_uri + + res_params = parse_qs(urlparse(authorization_response).query) + assert 'code' in res_params and res_params['code'], "authorization code not found in url" + + return res_params['code'][0] + + +class _RedirectWSGIApp(object): + """WSGI app to handle the authorization redirect. + Stores the request URI and displays the given success message. + """ + + def __init__(self, success_message): + """ + Args: + success_message (str): The message to display in the web browser + the authorization flow is complete. + """ + self.last_request_uri = None + self._success_message = success_message + + def __call__(self, environ, start_response): + """WSGI Callable. + Args: + environ (Mapping[str, Any]): The WSGI environment. + start_response (Callable[str, list]): The WSGI start_response + callable. + Returns: + Iterable[bytes]: The response body. + """ + start_response("200 OK", [("Content-type", "text/plain")]) + self.last_request_uri = wsgiref.util.request_uri(environ) + return [self._success_message.encode("utf-8")]