From 9b25faca743b21d05e4fbe2a620e6abcb94ddbca Mon Sep 17 00:00:00 2001
From: Simon Robinson
Date: Tue, 24 Sep 2024 09:48:27 +0100
Subject: [PATCH 1/8] Better missing GUI requirements message on macOS Fixes
#286
---
emailproxy.py | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)
diff --git a/emailproxy.py b/emailproxy.py
index c991c43..d4738b3 100644
--- a/emailproxy.py
+++ b/emailproxy.py
@@ -6,7 +6,7 @@
__author__ = 'Simon Robinson'
__copyright__ = 'Copyright (c) 2024 Simon Robinson'
__license__ = 'Apache 2.0'
-__version__ = '2024-09-12' # ISO 8601 (YYYY-MM-DD)
+__version__ = '2024-09-24' # ISO 8601 (YYYY-MM-DD)
__package_version__ = '.'.join([str(int(i)) for i in __version__.split('-')]) # for pyproject.toml usage only
import abc
@@ -2601,7 +2601,8 @@ def __init__(self, args=None):
if self.args.gui and len(MISSING_GUI_REQUIREMENTS) > 0:
Log.error('Unable to load all GUI requirements:', MISSING_GUI_REQUIREMENTS, '- did you mean to run in',
- '`--no-gui` mode? If not, please run `python -m pip install -r requirements-gui.txt`')
+ '`--no-gui` mode? If not, please run `python -m pip install -r requirements-gui.txt` or install',
+ 'from PyPI with GUI requirements included: `python -m pip install emailproxy[gui]`')
self.exit(None)
return
@@ -2622,6 +2623,9 @@ def __init__(self, args=None):
# noinspection PyUnresolvedReferences,PyAttributeOutsideInit
def init_platforms(self):
if sys.platform == 'darwin' and self.args.gui:
+ if len(MISSING_GUI_REQUIREMENTS) > 0:
+ return # skip - we will exit anyway due to missing requirements (with a more helpful error message)
+
# hide dock icon (but not LSBackgroundOnly as we need input via webview)
info = AppKit.NSBundle.mainBundle().infoDictionary()
info['LSUIElement'] = '1'
@@ -3417,7 +3421,8 @@ def exit(self, icon, restart_callback=None):
AppConfig.save()
- if sys.platform == 'darwin' and self.args.gui:
+ # attribute existence check is needed here and below because we may exit before init_platforms() has run
+ if sys.platform == 'darwin' and self.args.gui and hasattr(self, 'macos_reachability_target'):
# noinspection PyUnresolvedReferences
SystemConfiguration.SCNetworkReachabilityUnscheduleFromRunLoop(self.macos_reachability_target,
SystemConfiguration.CFRunLoopGetCurrent(),
@@ -3451,7 +3456,8 @@ def exit(self, icon, restart_callback=None):
restart_callback()
# macOS Launch Agents need reloading when changed; unloading exits immediately so this must be our final action
- if sys.platform == 'darwin' and self.args.gui and self.macos_unload_plist_on_exit:
+ if sys.platform == 'darwin' and self.args.gui and (
+ hasattr(self, 'macos_unload_plist_on_exit') and self.macos_unload_plist_on_exit):
self.macos_launchctl('unload')
From 9f2b988f8462c82a9b73c3f8f996f6787157a463 Mon Sep 17 00:00:00 2001
From: Simon Robinson
Date: Tue, 24 Sep 2024 13:02:38 +0100
Subject: [PATCH 2/8] Escape square brackets in PyPI GUI variant command See
#286
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 0eaabb8..0a296a7 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@ Begin by downloading the proxy via one of the following methods:
- Pick a pre-built release for your platform (macOS or Windows; no installation needed); or,
- - Install from PyPI: set up using
python -m pip install emailproxy[gui]
, download the sample emailproxy.config
file, then python -m emailproxy
to run; or,
+ - Install from PyPI: set up using
python -m pip install emailproxy\[gui\]
, download the sample emailproxy.config
file, then python -m emailproxy
to run; or,
- Clone or download (and star :-) the GitHub repository, then:
python -m pip install -r requirements-core.txt -r requirements-gui.txt
to install requirements, and python emailproxy.py
to run.
From e4dbd6490258a42197df690110a66e8edf7be4b1 Mon Sep 17 00:00:00 2001
From: Simon Robinson
Date: Fri, 4 Oct 2024 14:09:37 +0100
Subject: [PATCH 3/8] Minor lint improvements
---
emailproxy.py | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/emailproxy.py b/emailproxy.py
index d4738b3..28c8910 100644
--- a/emailproxy.py
+++ b/emailproxy.py
@@ -6,7 +6,7 @@
__author__ = 'Simon Robinson'
__copyright__ = 'Copyright (c) 2024 Simon Robinson'
__license__ = 'Apache 2.0'
-__version__ = '2024-09-24' # ISO 8601 (YYYY-MM-DD)
+__version__ = '2024-10-04' # ISO 8601 (YYYY-MM-DD)
__package_version__ = '.'.join([str(int(i)) for i in __version__.split('-')]) # for pyproject.toml usage only
import abc
@@ -1153,7 +1153,7 @@ def get_service_account_authorisation_token(key_type, key_path_or_contents, oaut
"""Requests an authorisation token via a Service Account key (currently Google Cloud only)"""
import json
try:
- import requests
+ import requests # noqa: F401 - requests is required as the default transport for google-auth
import google.oauth2.service_account
import google.auth.transport.requests
except ModuleNotFoundError as e:
@@ -1359,8 +1359,8 @@ def handle_error(self):
'CERTIFICATE_VERIFY_FAILED', 'TLSV1_ALERT_PROTOCOL_VERSION', 'TLSV1_ALERT_UNKNOWN_CA',
'UNSUPPORTED_PROTOCOL', 'record layer failure', APP_PACKAGE]
error_type, value = Log.get_last_error()
- if error_type == OSError and value.errno == 0 or issubclass(error_type, ssl.SSLError) and \
- any(i in value.args[1] for i in ssl_errors) or error_type == FileNotFoundError:
+ if error_type is OSError and value.errno == 0 or issubclass(error_type, ssl.SSLError) and \
+ any(i in value.args[1] for i in ssl_errors) or error_type is FileNotFoundError:
Log.error('Caught connection error in', self.info_string(), ':', error_type, 'with message:', value)
if hasattr(self, 'custom_configuration') and hasattr(self, 'proxy_type'):
if self.proxy_type == 'SMTP':
@@ -1485,7 +1485,7 @@ def handle_close(self):
error_type, value = Log.get_last_error()
if error_type and value:
message = 'Caught connection error (client)'
- if error_type == ConnectionResetError:
+ if error_type is ConnectionResetError:
message = '%s [ Are you attempting an encrypted connection to a non-encrypted server? ]' % message
Log.info(self.info_string(), message, '-', error_type.__name__, ':', value)
self.close()
@@ -1943,9 +1943,9 @@ def send(self, byte_data, censor_log=False):
def handle_error(self):
error_type, value = Log.get_last_error()
- if error_type == TimeoutError and value.errno == errno.ETIMEDOUT or \
+ if error_type is TimeoutError and value.errno == errno.ETIMEDOUT or \
issubclass(error_type, ConnectionError) and value.errno in [errno.ECONNRESET, errno.ECONNREFUSED] or \
- error_type == OSError and value.errno in [0, errno.ENETDOWN, errno.ENETUNREACH, errno.EHOSTDOWN,
+ error_type is OSError and value.errno in [0, errno.ENETDOWN, errno.ENETUNREACH, errno.EHOSTDOWN,
errno.EHOSTUNREACH]:
# TimeoutError 60 = 'Operation timed out'; ConnectionError 54 = 'Connection reset by peer', 61 = 'Connection
# refused; OSError 0 = 'Error' (typically network failure), 50 = 'Network is down', 51 = 'Network is
@@ -1965,7 +1965,7 @@ def handle_close(self):
error_type, value = Log.get_last_error()
if error_type and value:
message = 'Caught connection error (server)'
- if error_type == OSError and value.errno in [errno.ENOTCONN, 10057]:
+ if error_type is OSError and value.errno in [errno.ENOTCONN, 10057]:
# OSError 57 or 10057 = 'Socket is not connected'
message = '%s [ Client attempted to send command without waiting for server greeting ]' % message
Log.info(self.info_string(), message, '-', error_type.__name__, ':', value)
@@ -2382,9 +2382,9 @@ def restart(self):
def handle_error(self):
error_type, value = Log.get_last_error()
if error_type == socket.gaierror and value.errno in [-2, 8, 11001] or \
- error_type == TimeoutError and value.errno == errno.ETIMEDOUT or \
+ error_type is TimeoutError and value.errno == errno.ETIMEDOUT or \
issubclass(error_type, ConnectionError) and value.errno in [errno.ECONNRESET, errno.ECONNREFUSED] or \
- error_type == OSError and value.errno in [0, errno.EINVAL, errno.ENETDOWN, errno.EHOSTUNREACH]:
+ error_type is OSError and value.errno in [0, errno.EINVAL, errno.ENETDOWN, errno.EHOSTUNREACH]:
# gaierror -2 or 8 = 'nodename nor servname provided, or not known' / 11001 = 'getaddrinfo failed' (caused
# by getpeername() failing due to no connection); TimeoutError 60 = 'Operation timed out'; ConnectionError
# 54 = 'Connection reset by peer', 61 = 'Connection refused; OSError 0 = 'Error' (local SSL failure),
@@ -2874,7 +2874,7 @@ def create_authorisation_menu(self):
else:
usernames = []
for request in self.authorisation_requests:
- if not request['username'] in usernames:
+ if request['username'] not in usernames:
items.append(pystray.MenuItem(request['username'], self.authorise_account))
usernames.append(request['username'])
items.append(pystray.Menu.SEPARATOR)
From 31eac06104e5c47daf3a9a3863d1d172f4818710 Mon Sep 17 00:00:00 2001
From: Simon Robinson
Date: Mon, 21 Oct 2024 11:21:46 +0100
Subject: [PATCH 4/8] Readme improvements; minor lint fixes
---
README.md | 25 ++++++++++++++-----------
emailproxy.config | 11 +++++++++--
emailproxy.py | 6 +++++-
3 files changed, 28 insertions(+), 14 deletions(-)
diff --git a/README.md b/README.md
index 0a296a7..af830c3 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ The proxy works in the background with a menu bar/taskbar helper or as a headles
### Example use-cases
- You need to use an Office 365 email account, but don't get on with Outlook.
-The email client you like doesn't support OAuth 2.0, which became mandatory [in January 2023](https://techcommunity.microsoft.com/t5/exchange-team-blog/basic-authentication-deprecation-in-exchange-online-september/ba-p/3609437) ([September 2025 for SMTP](https://techcommunity.microsoft.com/t5/exchange-team-blog/exchange-online-to-retire-basic-auth-for-client-submission-smtp/ba-p/4114750)).
+The email client you like doesn't support OAuth 2.0, which became mandatory [in January 2023](https://techcommunity.microsoft.com/t5/exchange-team-blog/basic-authentication-deprecation-in-exchange-online-september/ba-p/3609437) ([September 2024 for free Hotmail/Outlook accounts](https://support.microsoft.com/en-us/office/modern-authentication-methods-now-needed-to-continue-syncing-outlook-email-in-non-microsoft-email-apps-c5d65390-9676-4763-b41f-d7986499a90d); [September 2025 for O365 SMTP](https://techcommunity.microsoft.com/t5/exchange-team-blog/exchange-online-to-retire-basic-auth-for-client-submission-smtp/ba-p/4114750)).
- You used to use Gmail via IMAP/POP/SMTP with your raw account credentials (i.e., your real password), but cannot do this now that Google has disabled this method, and don't want to use an [App Password](https://support.google.com/accounts/answer/185833) (or cannot enable this option).
- You have an account already set up in an email client, and you need to switch it to OAuth 2.0 authentication.
You can edit the server details, but the client forces you to delete and re-add the account to enable OAuth 2.0, and you don't want to do this.
@@ -32,7 +32,7 @@ Begin by downloading the proxy via one of the following methods:
Next, edit the sample `emailproxy.config` file to add configuration details for each email server and account that you want to use with the proxy.
-[Guidance and example account configurations](https://github.com/simonrob/email-oauth2-proxy/blob/main/emailproxy.config) are provided for Office 365, Gmail and several other providers, though you will need to insert your own client credentials for each one (see the [client credentials documentation](#oauth-20-client-credentials) for guidance).
+[Guidance and example account configurations](https://github.com/simonrob/email-oauth2-proxy/blob/main/emailproxy.config) are provided for Office 365, Gmail and several other providers, though you will need to insert your own client credentials for each one (see the [client credentials documentation](#oauth-20-client-credentials) below for guidance).
You can remove details from the sample configuration file for services you don't use, or add additional ones for any other OAuth 2.0-authenticated IMAP/POP/SMTP servers you would like to use with the proxy.
You can now start the proxy: depending on which installation option you chose, either launch the application or use the appropriate run command listed above.
@@ -71,7 +71,8 @@ The [sample configuration file](https://github.com/simonrob/email-oauth2-proxy/b
- Office 365: register a new [Microsoft identity application](https://learn.microsoft.com/entra/identity-platform/quickstart-register-app)
- Gmail / Google Workspace: register a [Google API desktop app client](https://developers.google.com/identity/protocols/oauth2/native-app)
-- AOL and Yahoo Mail (and subproviders such as AT&T) are not currently allowing new client registrations with the OAuth email scope – the only option here is to reuse the credentials from an existing client that does have this permission.
+- Outlook / Hotmail (free accounts): because you are not the administrator for these Microsoft-operated domains, the only option is to reuse an existing client ID – see, for example, [Thunderbird](https://blog.thunderbird.net/2023/01/important-message-for-microsoft-office-365-enterprise-users/), or the links above
+- AOL and Yahoo Mail (and subproviders such as AT&T) are not currently allowing new client registrations with the OAuth email scope – the only option here is to reuse the credentials from an existing client that does have this permission
The proxy supports [Google Cloud service accounts](https://cloud.google.com/iam/docs/service-account-overview) for access to Google Workspace Gmail.
It also supports the [client credentials grant (CCG)](https://learn.microsoft.com/entra/identity-platform/v2-oauth2-client-creds-grant-flow) and [resource owner password credentials grant (ROPCG)](https://learn.microsoft.com/entra/identity-platform/v2-oauth-ropc) OAuth 2.0 flows, and [certificate credentials (JWT)](https://learn.microsoft.com/entra/identity-platform/certificate-credentials).
@@ -134,8 +135,8 @@ See the [optional arguments and configuration](#optional-arguments-and-configura
If your network requires connections to use an existing proxy, you can instruct the script to use this by setting the [proxy handler](https://docs.python.org/3/library/urllib.request.html#urllib.request.ProxyHandler) environment variable `https_proxy` (and/or `http_proxy`) – for example, `https_proxy=localhost python -m emailproxy`.
-After installing its requirements, the proxy script can be packaged as a single self-contained executable using [pyinstaller](https://pyinstaller.org/) if desired: `pyinstaller --onefile emailproxy.py`.
-If you are using the GUI version of the proxy, you may need to add `--hidden-import timeago.locales.en_short` until [this `timeago` issue](https://github.com/hustcc/timeago/issues/40) is resolved.
+After installing its requirements, the proxy script can be packaged as a single self-contained executable using [Nuitka](https://nuitka.net/) (`nuitka --standalone --macos-create-app-bundle emailproxy.py`) or [pyinstaller](https://pyinstaller.org/) (`pyinstaller --onefile emailproxy.py`).
+If you are using pyinstaller and the GUI version of the proxy, you may need to add `--hidden-import timeago.locales.en_short` until [this `timeago` issue](https://github.com/hustcc/timeago/issues/40) is resolved.
Python 3.7 or later is required to run the proxy.
The [python2 branch](https://github.com/simonrob/email-oauth2-proxy/tree/python2) provides minimal compatibility with python 2.7, but with a limited feature set, and no ongoing maintenance.
@@ -151,7 +152,7 @@ The method to achieve this differs depending on whether you are using macOS, Win
On macOS, the file `~/Library/LaunchAgents/ac.robinson.email-oauth2-proxy.plist` is used to configure automatic starting of the proxy.
If you stop the proxy's service (i.e., `Quit Email OAuth 2.0 Proxy` from the menu bar), you can restart it using `launchctl start ac.robinson.email-oauth2-proxy` from a terminal.
-You can stop, disable or remove the service from your startup items either via the menu bar icon option, or using `launchctl unload [plist path]`.
+You can stop, disable or remove the service from your startup items either via the menu bar icon option, or using `launchctl unload `_`[plist path]`_.
If you edit the plist file manually, make sure you `unload` and then `load` it to update the system with your changes.
If the `Start at login` option appears not to be working for you on macOS, see the [known issues section](#known-issues) for potential solutions.
@@ -189,7 +190,7 @@ The easiest approach here is to use [OpenSSL](https://www.openssl.org/): `openss
If you are having trouble actually connecting to the proxy, it is always worth double-checking the `local_address` values that you are using.
The [sample configuration file](https://github.com/simonrob/email-oauth2-proxy/blob/main/emailproxy.config) sets this parameter to `127.0.0.1` for all servers.
If you remove this value and do not provide your own, the proxy defaults to `::` – in most cases this resolves to `localhost` for both IPv4 and IPv6 configurations, but it is possible that this differs depending on your environment.
-If you are unable to connect to the proxy from your client, it is always worth first specifying this value explicitly – see the [sample configuration file](https://github.com/simonrob/email-oauth2-proxy/blob/main/emailproxy.config) for further details about how to do this.
+If you are unable to connect to the proxy from your email client, first try specifying this value explicitly – see the [sample configuration file](https://github.com/simonrob/email-oauth2-proxy/blob/main/emailproxy.config) for further details about how to do this.
Please try setting and connecting to both IPv4 (i.e., `127.0.0.1`) and IPv6 (i.e., `::1`) loopback addresses before reporting any connection issues with the proxy.
### Dependencies and setup
@@ -202,7 +203,7 @@ This is caused by missing dependencies for [pystray](https://github.com/moses-pa
See the [pywebview dependencies](https://pywebview.flowrl.com/guide/installation.html#dependencies) and [pystray FAQ](https://pystray.readthedocs.io/en/latest/faq.html) pages and [existing](https://github.com/simonrob/email-oauth2-proxy/issues/1#issuecomment-831746642) [closed issues](https://github.com/simonrob/email-oauth2-proxy/issues/136#issuecomment-1430417456) in this repository for a summary and suggestions about how to resolve this.
A similar issue may occur on Windows with the [pythonnet](https://github.com/pythonnet/pythonnet) package, which is required by [pywebview](https://github.com/r0x0r/pywebview).
-If you are unable to resolve this by following the [pythonnet installation instructions](https://github.com/pythonnet/pythonnet/wiki/Installation), you may find that installing a [prebuilt wheel](https://www.lfd.uci.edu/~gohlke/pythonlibs/#pythonnet) helps fix the issue.
+The [pythonnet installation instructions](https://github.com/pythonnet/pythonnet/wiki/Installation) may offer alternative ways to install this package if the default installation fails.
Note that the public releases of pythonnet can take some time to be compatible with the latest major python release, so it can be worth using a slightly older version of python, or a pre-release version of pythonnet.
### Known issues
@@ -219,7 +220,7 @@ Once this has been approved, the proxy's menu bar icon will appear as normal.
In some cases — particularly when running the proxy in a virtual environment, or using the built-in macOS python, rather than the python.org version, or installations managed by, e.g., homebrew, pyenv, etc. — the permission prompt does not appear.
If this happens it is worth first trying to `unload` and then `load` the service via `launchctl`.
If this still does not cause the prompt to appear, the only currently-known resolution is to run the proxy outside of a virtual environment and manually grant Full Disk Access to your python executable via the privacy settings in the macOS System Preferences.
-You may also need to edit the proxy's launch agent plist file, which is found at the location given in the command above, to set the path to your python executable – it must be the real path rather than a symlink (the `readlink` command can help here).
+You may also need to edit the proxy's launch agent plist file, which is found at the location given [in the command above](#starting-the-proxy-automatically), to set the path to your python executable – it must be the real path rather than a symlink (the `readlink` command can help here).
Fortunately this is a one-time fix, and once the proxy loads successfully via this method you will not need to adjust its startup configuration again (except perhaps when upgrading to a newer major macOS version, in which case just repeat the procedure).
### Other problems
@@ -227,11 +228,13 @@ Please feel free to [open an issue](https://github.com/simonrob/email-oauth2-pro
## Advanced features
-The [plugins variant](https://github.com/simonrob/email-oauth2-proxy/tree/plugins) has an additional feature that enables the use of separate scripts to modify IMAP/POP/SMTP commands when they are received from the client or server before passing through to the other side of the connection.
+The [plugins variant of the proxy](https://github.com/simonrob/email-oauth2-proxy/tree/plugins) has an additional feature that enables the use of separate scripts to modify IMAP/POP/SMTP commands when they are received from the client or server before passing through to the other side of the connection.
This allows a wide range of additional capabilities or triggers to be added the proxy.
+
For example, the [IMAPIgnoreSentMessageUpload plugin](https://github.com/simonrob/email-oauth2-proxy/blob/plugins/plugins/IMAPIgnoreSentMessageUpload.py) intercepts any client commands to add emails to the IMAP sent messages mailbox, which resolves message duplication issues for servers that automatically do this when emails are received via SMTP (e.g., Office 365, Gmail, etc.).
-The [IMAPCleanO365ATPLinks plugin](https://github.com/simonrob/email-oauth2-proxy/blob/plugins/plugins/IMAPCleanO365ATPLinks.py) restores "Safe Links" modified by Microsoft Defender for Office 365 to their original URLs.
+The [IMAPCleanO365ATPLinks plugin](https://github.com/simonrob/email-oauth2-proxy/blob/plugins/plugins/IMAPCleanO365ATPLinks.py) restores "Safe Links" modified by Microsoft Defender for Office 365 to their original URLs, while the [IMAPRegexContentReplacer plugin](https://github.com/simonrob/email-oauth2-proxy/blob/plugins/plugins/IMAPRegexContentReplacer.py) lets you match and remove/replace any content in the message.
The [SMTPBlackHole plugin](https://github.com/simonrob/email-oauth2-proxy/blob/plugins/plugins/SMTPBlackHole.py) gives the impression emails are being sent but actually silently discards them, which is useful for testing email sending tools.
+
See the [documentation and examples](https://github.com/simonrob/email-oauth2-proxy/tree/plugins/plugins) for further details, additional sample plugins and setup instructions.
diff --git a/emailproxy.config b/emailproxy.config
index 18b9aa6..d84f226 100644
--- a/emailproxy.config
+++ b/emailproxy.config
@@ -199,6 +199,13 @@ redirect_uri = http://localhost
client_id = *** your client id here ***
client_secret = *** your client secret here ***
+[your.free.outlook.or.hotmail.address@outlook.com]
+permission_url = https://login.microsoftonline.com/common/oauth2/v2.0/authorize
+token_url = https://login.microsoftonline.com/common/oauth2/v2.0/token
+oauth2_scope = https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/POP.AccessAsUser.All https://outlook.office.com/SMTP.Send offline_access
+client_id = *** your client id here - note that as you are not the administrator of Hotmail.com / Outlook.com, you will need to reuse an existing client ID (see the proxy's readme) ***
+redirect_uri = https://localhost
+
[your.email@gmail.com]
permission_url = https://accounts.google.com/o/oauth2/auth
token_url = https://oauth2.googleapis.com/token
@@ -212,7 +219,7 @@ permission_url = https://api.login.yahoo.com/oauth2/request_auth
token_url = https://api.login.yahoo.com/oauth2/get_token
oauth2_scope = mail-w
redirect_uri = http://localhost
-client_id = *** your client id here ***
+client_id = *** your client id here - note that as new client registrations are not permitted for Yahoo, you will need to reuse an existing client ID (see the proxy's readme) ***
client_secret = *** your client secret here ***
[your.email@aol.com]
@@ -220,7 +227,7 @@ permission_url = https://api.login.aol.com/oauth2/request_auth
token_url = https://api.login.aol.com/oauth2/get_token
oauth2_scope = mail-w
redirect_uri = http://localhost
-client_id = *** your client id here ***
+client_id = *** your client id here - note that as new client registrations are not permitted for AOL, you will need to reuse an existing client ID (see the proxy's readme) ***
client_secret = *** your client secret here ***
[ccg.flow.configured.address@your-tenant.com]
diff --git a/emailproxy.py b/emailproxy.py
index 28c8910..5a9d2e5 100644
--- a/emailproxy.py
+++ b/emailproxy.py
@@ -6,7 +6,7 @@
__author__ = 'Simon Robinson'
__copyright__ = 'Copyright (c) 2024 Simon Robinson'
__license__ = 'Apache 2.0'
-__version__ = '2024-10-04' # ISO 8601 (YYYY-MM-DD)
+__version__ = '2024-10-21' # ISO 8601 (YYYY-MM-DD)
__package_version__ = '.'.join([str(int(i)) for i in __version__.split('-')]) # for pyproject.toml usage only
import abc
@@ -1105,6 +1105,7 @@ def get_oauth2_authorisation_code(permission_url, redirect_uri, redirect_listen_
time.sleep(1)
@staticmethod
+ # pylint: disable-next=too-many-positional-arguments
def get_oauth2_authorisation_tokens(token_url, redirect_uri, client_id, client_secret, jwt_client_assertion,
authorisation_code, oauth2_scope, oauth2_flow, username, password):
"""Requests OAuth 2.0 access and refresh tokens from token_url using the given client_id, client_secret,
@@ -1182,6 +1183,7 @@ def get_service_account_authorisation_token(key_type, key_path_or_contents, oaut
return {'access_token': credentials.token, 'expires_in': int(credentials.expiry.timestamp() - time.time())}
@staticmethod
+ # pylint: disable-next=too-many-positional-arguments
def refresh_oauth2_access_token(token_url, client_id, client_secret, jwt_client_assertion, username, refresh_token):
"""Obtains a new access token from token_url using the given client_id, client_secret and refresh token,
returning a dict with 'access_token', 'expires_in', and 'refresh_token' on success; exception on failure"""
@@ -1385,6 +1387,7 @@ class OAuth2ClientConnection(SSLAsyncoreDispatcher):
"""The base client-side connection that is subclassed to handle IMAP/POP/SMTP client interaction (note that there
is some protocol-specific code in here, but it is not essential, and only used to avoid logging credentials)"""
+ # pylint: disable-next=too-many-positional-arguments
def __init__(self, proxy_type, connection_socket, socket_map, proxy_parent, custom_configuration):
SSLAsyncoreDispatcher.__init__(self, connection_socket=connection_socket, socket_map=socket_map)
self.receive_buffer = b''
@@ -1829,6 +1832,7 @@ def send_authentication_request(self):
class OAuth2ServerConnection(SSLAsyncoreDispatcher):
"""The base server-side connection that is subclassed to handle IMAP/POP/SMTP server interaction"""
+ # pylint: disable-next=too-many-positional-arguments
def __init__(self, proxy_type, connection_socket, socket_map, proxy_parent, custom_configuration):
SSLAsyncoreDispatcher.__init__(self, socket_map=socket_map) # note: establish connection later due to STARTTLS
self.receive_buffer = b''
From f0bf373efc78be6717b385c12f17bed3a39b0af1 Mon Sep 17 00:00:00 2001
From: Simon Robinson
Date: Tue, 5 Nov 2024 21:39:38 +0000
Subject: [PATCH 5/8] Add support for device authorisation grant - see #302
---
emailproxy.config | 13 +++-
emailproxy.py | 186 ++++++++++++++++++++++++++++++++++------------
2 files changed, 150 insertions(+), 49 deletions(-)
diff --git a/emailproxy.config b/emailproxy.config
index d84f226..dde604f 100644
--- a/emailproxy.config
+++ b/emailproxy.config
@@ -155,6 +155,10 @@ documentation = Accounts are specified using your email address as the section h
attempts before the first valid login, pre-encrypting account entries is highly recommended. See the example
script at https://github.com/simonrob/email-oauth2-proxy/issues/61#issuecomment-1259110336.
+ - The proxy supports the device authorisation grant (DAG) OAuth 2.0 flow (RFC 8628), which may better suit headless
+ systems. To use this flow, set `oauth2_flow = device`. With this flow, the proxy receives authorisation responses
+ directly from the service provider, so no `redirect_uri` is needed. An example account configuration is given below.
+
Gmail customisation:
- The proxy supports the use of service accounts with Gmail for Google Workspace (note: normal Gmail accounts do not
support this method). To use this option, add an account entry as normal, but do not add a `permission_url` value
@@ -203,7 +207,7 @@ client_secret = *** your client secret here ***
permission_url = https://login.microsoftonline.com/common/oauth2/v2.0/authorize
token_url = https://login.microsoftonline.com/common/oauth2/v2.0/token
oauth2_scope = https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/POP.AccessAsUser.All https://outlook.office.com/SMTP.Send offline_access
-client_id = *** your client id here - note that as you are not the administrator of Hotmail.com / Outlook.com, you will need to reuse an existing client ID (see the proxy's readme) ***
+client_id = *** your client id here - note that as you are not the administrator of Hotmail.com / Outlook.com, you will likely need to reuse an existing client ID (see the proxy's readme) ***
redirect_uri = https://localhost
[your.email@gmail.com]
@@ -230,6 +234,13 @@ redirect_uri = http://localhost
client_id = *** your client id here - note that as new client registrations are not permitted for AOL, you will need to reuse an existing client ID (see the proxy's readme) ***
client_secret = *** your client secret here ***
+[dag.flow.configured.address@outlook.com]
+permission_url = https://login.microsoftonline.com/common/oauth2/v2.0/devicecode
+token_url = https://login.microsoftonline.com/common/oauth2/v2.0/token
+oauth2_scope = https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/POP.AccessAsUser.All https://outlook.office.com/SMTP.Send offline_access
+oauth2_flow = device
+client_id = *** your client id here ***
+
[ccg.flow.configured.address@your-tenant.com]
documentation = *** note: this is an advanced O365 account example; in most cases you want the version above instead ***
token_url = https://login.microsoftonline.com/*** your tenant id here ***/oauth2/v2.0/token
diff --git a/emailproxy.py b/emailproxy.py
index 5a9d2e5..0b60db2 100644
--- a/emailproxy.py
+++ b/emailproxy.py
@@ -6,7 +6,7 @@
__author__ = 'Simon Robinson'
__copyright__ = 'Copyright (c) 2024 Simon Robinson'
__license__ = 'Apache 2.0'
-__version__ = '2024-10-21' # ISO 8601 (YYYY-MM-DD)
+__version__ = '2024-11-05' # ISO 8601 (YYYY-MM-DD)
__package_version__ = '.'.join([str(int(i)) for i in __version__.split('-')]) # for pyproject.toml usage only
import abc
@@ -210,32 +210,38 @@ class NSObject:
Ovp0yY9EkQZ8XELHSa+x0S9OAm75cT+F+UFm+vhbmClQLCtF+SnMNAji11lcz5orzCQopo21KJIn3FB37iuaJ9yRd+4zuicsSINViSesyEgbMtQcZgIE
TyNBsIQrXgdVS3h2hGdf+Apf4eIIF+ub16FYBhQd4ci3IiAOBP8/z+kNGUS6hBN6UlIAAAAASUVORK5CYII=''' # 22px SF Symbols lock.fill
-EXTERNAL_AUTH_HTML = '''
- Login authorisation request for %s
+ document.execCommand('copy');document.body.removeChild(copySource);source.innerText='✔';
+ window.setTimeout(()=>source.innerText='⧉',1000)}
+ Login authorisation request for %s
Click the following link to open your browser and approve the request:
%s
- ⧉
- After logging in and successfully authorising your account, paste and submit the
- resulting URL from the browser's address bar using the box at the bottom of this page to allow the %s script to
- transparently handle login requests on your behalf in future.
- Note that your browser may show a navigation error (e.g., "localhost refused to connect") after
+ ⧉
'''
+
+EXTERNAL_AUTH_HTML = EXTERNAL_AUTH_HTML_BASE + '''After logging in and successfully
+ authorising your account, paste and submit the resulting URL from the browser's address bar using the box at the
+ bottom of this page to allow the %s script to transparently handle login requests on your behalf in future.
+ Note that your browser may show a navigation error (e.g., “localhost refused to connect”) after
successfully logging in, but the final URL is the only important part, and as long as this begins with the
correct redirection URI and contains a valid authorisation code your email client's request will succeed.''' + (
' If you are using Windows, submitting can take a few seconds.' if sys.platform == 'win32' else '') + '''
According to your proxy configuration file, the expected URL will be of the form:
- %s [...] code=[code] [...]
- '''
+EXTERNAL_AUTH_DAG_HTML = EXTERNAL_AUTH_HTML_BASE + '''Enter the following code when
+ prompted:
%s
⧉
+ You can close this window once authorisation is complete.