Skip to content

Commit 946d8f1

Browse files
committed
Merge branch 'main' into plugins
2 parents 25cd2a0 + 48773b0 commit 946d8f1

File tree

3 files changed

+41
-28
lines changed

3 files changed

+41
-28
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ It is often helpful to be able to view the raw connection details when debugging
172172
This can be achieved using `telnet`, [PuTTY](https://www.chiark.greenend.org.uk/~sgtatham/putty/) or similar.
173173
For example, to test the Office 365 IMAP server from the [example configuration](https://github.com/simonrob/email-oauth2-proxy/blob/main/emailproxy.config), first open a connection using `telnet 127.0.0.1 1993`, and then send a login command: `a1 login [email protected] password`, replacing `[email protected]` with your email address, and `password` with any value you like during testing (see above for why the password is irrelevant).
174174
If you have already authorised your account with the proxy you should see a response starting with `a1 OK`; if not, this command should trigger a notification from the proxy about authorising your account.
175+
Note that POP and SMTP are different protocols, and while they can be tested in this way, they require different commands to be sent – see [this issue comment](https://github.com/simonrob/email-oauth2-proxy/issues/251#issuecomment-2133976839) for further details.
175176

176177
If you are using a [secure local connection](https://github.com/simonrob/email-oauth2-proxy/blob/main/emailproxy.config) the interaction with the remote email server is the same as above, but you will need to use a local debugging tool that supports encryption.
177178
The easiest approach here is to use [OpenSSL](https://www.openssl.org/): `openssl s_client -crlf -connect 127.0.0.1:1993`.

emailproxy.config

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ documentation = This is a sample Email OAuth 2.0 Proxy configuration file. Confi
33
[Server setup] and [Account setup] sections below. You may delete any servers or accounts that you do not intend to
44
use. Documentation is provided inline, with example setups for Gmail and Office 365 (though you will need to enter
55
your own desktop app API client credentials in the accounts section). Use the `Reload configuration file` menu
6-
option or send a SIGHUP signal (or restart the proxy) to apply any changes.
6+
option or send a SIGHUP signal (or quit the proxy before editing, then restart) to apply any changes.
7+
format = This file's format is documented at docs.python.org/library/configparser#supported-ini-file-structure. Values
8+
that span multiple lines should be indented deeper than the first line of their key (as in this comment). Quoting
9+
of values is not required. Documentation sections can be removed if needed (though it is advisable to leave these
10+
in place for reference) - thw only required sections are the individual server and account items of your setup.
711
warning = Do not commit changes to this file into a public repository (e.g., GitHub, etc). While the proxy encrypts the
812
OAuth 2.0 tokens it obtains and saves on your behalf, it cannot protect these against offline brute-force attacks.
913

@@ -143,6 +147,13 @@ documentation = Accounts are specified using your email address as the section h
143147
in the example below) in order to allow the proxy to refresh its access token on your behalf. The proxy will still
144148
work if this parameter is not included, but you will need to re-authenticate extremely often (about once per hour).
145149

150+
- The example Office 365 configuration entries below use an OAuth 2.0 scope that clearly specifies IMAP, POP and
151+
SMTP permission. If you do not require one or more of these protocols, you may remove the relevant values to ensure
152+
the access tokens obtained on your behalf are as precisely-targeted as possible. Conversely, it is also possible to
153+
replace these specific scopes with the more generic `https://outlook.office365.com/.default`. Switching to a broader
154+
scope value may also be needed if you are using a version of O365 delivered by a regional provider (e.g., 21Vianet).
155+
See: https://github.com/simonrob/email-oauth2-proxy/issues/255 for more details and discussion.
156+
146157
- By default, new Entra (Azure AD) clients are accessible only within your own tenant. If you are registering a new
147158
client to use with the proxy (and do not want to make it available outside your own organisation) you will need to
148159
replace `common` with your tenant ID in the Office 365 `permission_url` and `token_url` values below. Alternatively,

emailproxy.py

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
__author__ = 'Simon Robinson'
77
__copyright__ = 'Copyright (c) 2024 Simon Robinson'
88
__license__ = 'Apache 2.0'
9-
__version__ = '2024-05-25' # ISO 8601 (YYYY-MM-DD)
9+
__version__ = '2024-06-28' # ISO 8601 (YYYY-MM-DD)
1010
__package_version__ = '.'.join([str(int(i)) for i in __version__.split('-')]) # for pyproject.toml usage only
1111

1212
import abc
@@ -322,7 +322,7 @@ def format_host_port(address):
322322
host, port, *_ = address
323323
with contextlib.suppress(ValueError):
324324
ip = ipaddress.ip_address(host)
325-
host = '[%s]' % host if type(ip) is ipaddress.IPv6Address else host
325+
host = '[%s]' % host if isinstance(ip, ipaddress.IPv6Address) else host
326326
return '%s:%d' % (host, port)
327327

328328
@staticmethod
@@ -365,13 +365,13 @@ def _get_boto3_client(store_id):
365365
except ModuleNotFoundError:
366366
Log.error('Unable to load AWS SDK - please install the `boto3` module: `python -m pip install boto3`')
367367
return None, None
368-
else:
369-
# allow a profile to be chosen by prefixing the store_id - the separator used (`||`) will not be in an ARN
370-
# or secret name (see: https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_CreateSecret.html)
371-
split_id = store_id.split('||', maxsplit=1)
372-
if '||' in store_id:
373-
return split_id[1], boto3.session.Session(profile_name=split_id[0]).client('secretsmanager')
374-
return store_id, boto3.client(service_name='secretsmanager')
368+
369+
# allow a profile to be chosen by prefixing the store_id - the separator used (`||`) will not be in an ARN
370+
# or secret name (see: https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_CreateSecret.html)
371+
split_id = store_id.split('||', maxsplit=1)
372+
if '||' in store_id:
373+
return split_id[1], boto3.session.Session(profile_name=split_id[0]).client('secretsmanager')
374+
return store_id, boto3.client(service_name='secretsmanager')
375375

376376
@staticmethod
377377
def _create_secret(aws_client, store_id):
@@ -824,7 +824,7 @@ def get_oauth2_credentials(username, password, reload_remote_accounts=True):
824824
headers={
825825
'x5t#S256': base64.urlsafe_b64encode(jwt_certificate_fingerprint).decode('utf-8')
826826
})
827-
except FileNotFoundError:
827+
except (FileNotFoundError, OSError): # catch OSError due to GitHub issue 257 (quoted paths)
828828
return (False, 'Unable to create credentials assertion for account %s - please check that the '
829829
'`jwt_certificate_path` and `jwt_key_path` values are correct' % username)
830830

@@ -912,7 +912,7 @@ def get_oauth2_credentials(username, password, reload_remote_accounts=True):
912912

913913
except OAuth2Helper.TokenRefreshError as e:
914914
# always clear access tokens - can easily request another via the refresh token (with no user interaction)
915-
has_access_token = True if config.get(username, 'access_token', fallback=None) else False
915+
has_access_token = bool(config.get(username, 'access_token', fallback=None))
916916
config.remove_option(username, 'access_token')
917917
config.remove_option(username, 'access_token_expiry')
918918

@@ -986,6 +986,7 @@ def start_redirection_receiver_server(token_request):
986986
Log.format_host_port((parsed_uri.hostname, parsed_port)))
987987

988988
class LoggingWSGIRequestHandler(wsgiref.simple_server.WSGIRequestHandler):
989+
# pylint: disable=arguments-differ
989990
def log_message(self, _format_string, *args):
990991
Log.debug('Local server auth mode (%s): received authentication response' % Log.format_host_port(
991992
(parsed_uri.hostname, parsed_port)), *args)
@@ -1150,22 +1151,22 @@ def get_service_account_authorisation_token(key_type, key_path_or_contents, oaut
11501151
import requests
11511152
import google.oauth2.service_account
11521153
import google.auth.transport.requests
1153-
except ModuleNotFoundError:
1154-
raise Exception('Unable to load Google Auth SDK - please install the `requests` and `google-auth` modules: '
1155-
'`python -m pip install requests google-auth`')
1154+
except ModuleNotFoundError as e:
1155+
raise ModuleNotFoundError('Unable to load Google Auth SDK - please install the `requests` and '
1156+
'`google-auth` modules: `python -m pip install requests google-auth`') from e
11561157

11571158
if key_type == 'file':
11581159
try:
1159-
with open(key_path_or_contents) as key_file:
1160+
with open(key_path_or_contents, mode='r', encoding='utf-8') as key_file:
11601161
service_account = json.load(key_file)
11611162
except IOError as e:
1162-
raise FileNotFoundError('Unable to open service account key file %s for account %s',
1163+
raise FileNotFoundError('Unable to open service account key file %s for account %s' %
11631164
(key_path_or_contents, username)) from e
11641165
elif key_type == 'key':
11651166
service_account = json.loads(key_path_or_contents)
11661167
else:
1167-
raise Exception('Service account key type not specified for account %s - `client_id` must be set to '
1168-
'`file` or `key`' % username)
1168+
raise KeyError('Service account key type not specified for account %s - `client_id` must be set to '
1169+
'`file` or `key`' % username)
11691170

11701171
credentials = google.oauth2.service_account.Credentials.from_service_account_info(service_account)
11711172
credentials = credentials.with_scopes(oauth2_scope.split(' '))
@@ -2044,7 +2045,7 @@ def process_data(self, byte_data):
20442045
if not re.search(' SASL-IR', updated_response, re.IGNORECASE):
20452046
updated_response = updated_response.replace(' AUTH=PLAIN', ' AUTH=PLAIN SASL-IR')
20462047
updated_response = re.sub(' LOGINDISABLED', '', updated_response, count=1, flags=re.IGNORECASE)
2047-
byte_data = (b'%s\r\n' % updated_response.encode('utf-8'))
2048+
byte_data = b'%s\r\n' % updated_response.encode('utf-8')
20482049

20492050
super().process_data(byte_data)
20502051

@@ -2842,9 +2843,9 @@ def create_config_menu(self):
28422843
if len(self.proxies) <= 0:
28432844
# note that we don't actually allow no servers when loading the config, so no need to generate a menu
28442845
return items # (avoids creating and then immediately regenerating the menu when servers are loaded)
2845-
else:
2846-
for server_type in ['IMAP', 'POP', 'SMTP']:
2847-
items.extend(App.get_config_menu_servers(self.proxies, server_type))
2846+
2847+
for server_type in ['IMAP', 'POP', 'SMTP']:
2848+
items.extend(App.get_config_menu_servers(self.proxies, server_type))
28482849

28492850
config_accounts = AppConfig.accounts()
28502851
items.append(pystray.MenuItem('Accounts (+ last authenticated activity):', None, enabled=False))
@@ -2977,7 +2978,7 @@ def create_authorisation_window(self, request):
29772978
# pywebview 3.6+ moved window events to a separate namespace in a non-backwards-compatible way
29782979
# noinspection PyDeprecation
29792980
pywebview_version = pkg_resources.parse_version(pkg_resources.get_distribution('pywebview').version)
2980-
# the version zero check is due to a bug in the Ubuntu 22.04 python-pywebview package - see GitHub #242
2981+
# the version zero check is due to a bug in the Ubuntu 24.04 python-pywebview package - see GitHub #242
29812982
# noinspection PyDeprecation
29822983
if pkg_resources.parse_version('0') < pywebview_version < pkg_resources.parse_version('3.6'):
29832984
# noinspection PyUnresolvedReferences
@@ -3174,10 +3175,10 @@ def macos_launchctl(command):
31743175
output = subprocess.check_output(['/bin/launchctl', command, proxy_command], stderr=subprocess.STDOUT)
31753176
except subprocess.CalledProcessError:
31763177
return False
3177-
else:
3178-
if output and command != 'list':
3179-
return False # load/unload gives no output unless unsuccessful (return code is always 0 regardless)
3180-
return True
3178+
3179+
if output and command != 'list':
3180+
return False # load/unload gives no output unless unsuccessful (return code is always 0 regardless)
3181+
return True
31813182

31823183
@staticmethod
31833184
def started_at_login(_):

0 commit comments

Comments
 (0)