From e9c4de40207aad96d3fdd18aa4059cb70dd74b45 Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Sun, 17 Sep 2023 13:35:17 -0400 Subject: [PATCH] Fixed Email password handling when `user=` specified (#947) --- apprise/URLBase.py | 15 +++++++++ apprise/plugins/NotifyEmail.py | 8 ++--- test/test_plugin_email.py | 61 ++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 4 deletions(-) diff --git a/apprise/URLBase.py b/apprise/URLBase.py index 4b33920e..998c5c46 100644 --- a/apprise/URLBase.py +++ b/apprise/URLBase.py @@ -650,6 +650,21 @@ class URLBase: if 'user' in results['qsd']: results['user'] = results['qsd']['user'] + # parse_url() always creates a 'password' and 'user' entry in the + # results returned. Entries are set to None if they weren't specified + if results['password'] is None and 'user' in results['qsd']: + # Handle cases where the user= provided in 2 locations, we want + # the original to fall back as a being a password (if one wasn't + # otherwise defined) + # e.g. + # mailtos://PASSWORD@hostname?user=admin@mail-domain.com + # - the PASSWORD gets lost in the parse url() since a user= + # over-ride is specified. + presults = parse_url(results['url']) + if presults: + # Store our Password + results['password'] = presults['user'] + # Store our socket read timeout if specified if 'rto' in results['qsd']: results['rto'] = results['qsd']['rto'] diff --git a/apprise/plugins/NotifyEmail.py b/apprise/plugins/NotifyEmail.py index cc69489b..b7fe6789 100644 --- a/apprise/plugins/NotifyEmail.py +++ b/apprise/plugins/NotifyEmail.py @@ -1040,6 +1040,10 @@ class NotifyEmail(NotifyBase): # add one to ourselves results['targets'] = NotifyEmail.split_path(results['fullpath']) + # Attempt to detect 'to' email address + if 'to' in results['qsd'] and len(results['qsd']['to']): + results['targets'].append(results['qsd']['to']) + # Attempt to detect 'from' email address if 'from' in results['qsd'] and len(results['qsd']['from']): from_addr = NotifyEmail.unquote(results['qsd']['from']) @@ -1058,10 +1062,6 @@ class NotifyEmail(NotifyBase): # Extract from name to associate with from address from_addr = NotifyEmail.unquote(results['qsd']['name']) - # Attempt to detect 'to' email address - if 'to' in results['qsd'] and len(results['qsd']['to']): - results['targets'].append(results['qsd']['to']) - # Store SMTP Host if specified if 'smtp' in results['qsd'] and len(results['qsd']['smtp']): # Extract the smtp server diff --git a/test/test_plugin_email.py b/test/test_plugin_email.py index 6b7009b1..1ac83843 100644 --- a/test/test_plugin_email.py +++ b/test/test_plugin_email.py @@ -124,6 +124,12 @@ TEST_URLS = ( ('mailtos://user:pass@nuxref.com:567?to=l2g@nuxref.com', { 'instance': NotifyEmail, }), + ('mailtos://user:pass@domain.com?user=admin@mail-domain.com', { + 'instance': NotifyEmail, + }), + ('mailtos://%20@domain.com?user=admin@mail-domain.com', { + 'instance': NotifyEmail, + }), ('mailtos://user:pass@nuxref.com:567/l2g@nuxref.com', { 'instance': NotifyEmail, }), @@ -1387,6 +1393,61 @@ def test_plugin_email_url_parsing(mock_smtp, mock_smtp_ssl): assert pw == 'abc123' assert user == 'joe@mydomain.nl' + mock_smtp.reset_mock() + mock_smtp_ssl.reset_mock() + response.reset_mock() + + # Issue github.com/caronc/apprise/issue/941 + + # mail domain = mail-domain.com + # host domain = domain.subdomain.com + # PASSWORD needs to be fetched since a user= was provided + # - this is an edge case that is tested here + results = NotifyEmail.parse_url( + 'mailtos://PASSWORD@domain.subdomain.com:587?' + 'user=admin@mail-domain.com&to=mail@mail-domain.com') + assert isinstance(results, dict) + # From_Addr could not be detected at this stage, but will be + # handled during instantiation + assert '' == results['from_addr'] + assert 'admin@mail-domain.com' == results['user'] + assert results['port'] == 587 + assert 'domain.subdomain.com' == results['host'] + assert 'PASSWORD' == results['password'] + assert 'mail@mail-domain.com' in results['targets'] + + obj = Apprise.instantiate(results, suppress_exceptions=False) + assert isinstance(obj, NotifyEmail) is True + + # Not that our from_address takes on 'admin@domain.subdomain.com' + assert obj.from_addr == ['Apprise', 'admin@domain.subdomain.com'] + + assert mock_smtp.call_count == 0 + assert mock_smtp_ssl.call_count == 0 + assert response.starttls.call_count == 0 + assert obj.notify("test") is True + assert mock_smtp.call_count == 1 + assert response.starttls.call_count == 1 + assert mock_smtp_ssl.call_count == 0 + assert response.login.call_count == 1 + assert response.sendmail.call_count == 1 + # Store our Sent Arguments + # Syntax is: + # sendmail(from_addr, to_addrs, msg, mail_options=(), rcpt_options=()) + # [0] [1] [2] + _from = response.sendmail.call_args[0][0] + _to = response.sendmail.call_args[0][1] + _msg = response.sendmail.call_args[0][2] + assert _from == 'admin@domain.subdomain.com' + assert isinstance(_to, list) + assert len(_to) == 1 + assert _to[0] == 'mail@mail-domain.com' + assert _msg.split('\n')[-3] == 'test' + + user, pw = response.login.call_args[0] + assert user == 'admin@mail-domain.com' + assert pw == 'PASSWORD' + @mock.patch('smtplib.SMTP_SSL') @mock.patch('smtplib.SMTP')