From 32992fa64102afe611732fade72f78159f9adb24 Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Sat, 5 Nov 2022 16:37:57 -0400 Subject: [PATCH] Email improvements; name= and from= now synonymous (#738) --- apprise/URLBase.py | 5 +- apprise/plugins/NotifyBase.py | 5 +- apprise/plugins/NotifyEmail.py | 146 ++++++++++++++++++------------ apprise/plugins/NotifyMailgun.py | 41 +++++---- apprise/utils.py | 14 ++- test/test_apprise_utils.py | 27 +++++- test/test_plugin_email.py | 149 ++++++++++++++++++++++++++++--- test/test_plugin_mailgun.py | 6 ++ 8 files changed, 300 insertions(+), 93 deletions(-) diff --git a/apprise/URLBase.py b/apprise/URLBase.py index eb4a379e..7affae3b 100644 --- a/apprise/URLBase.py +++ b/apprise/URLBase.py @@ -598,7 +598,7 @@ class URLBase: } @staticmethod - def parse_url(url, verify_host=True): + def parse_url(url, verify_host=True, plus_to_space=False): """Parses the URL and returns it broken apart into a dictionary. This is very specific and customized for Apprise. @@ -618,7 +618,8 @@ class URLBase: """ results = parse_url( - url, default_schema='unknown', verify_host=verify_host) + url, default_schema='unknown', verify_host=verify_host, + plus_to_space=plus_to_space) if not results: # We're done; we failed to parse our url diff --git a/apprise/plugins/NotifyBase.py b/apprise/plugins/NotifyBase.py index 25a3da7b..9b2514de 100644 --- a/apprise/plugins/NotifyBase.py +++ b/apprise/plugins/NotifyBase.py @@ -429,7 +429,7 @@ class NotifyBase(BASE_OBJECT): return params @staticmethod - def parse_url(url, verify_host=True): + def parse_url(url, verify_host=True, plus_to_space=False): """Parses the URL and returns it broken apart into a dictionary. This is very specific and customized for Apprise. @@ -447,7 +447,8 @@ class NotifyBase(BASE_OBJECT): A dictionary is returned containing the URL fully parsed if successful, otherwise None is returned. """ - results = URLBase.parse_url(url, verify_host=verify_host) + results = URLBase.parse_url( + url, verify_host=verify_host, plus_to_space=plus_to_space) if not results: # We're done; we failed to parse our url diff --git a/apprise/plugins/NotifyEmail.py b/apprise/plugins/NotifyEmail.py index 00b2de8a..405bf820 100644 --- a/apprise/plugins/NotifyEmail.py +++ b/apprise/plugins/NotifyEmail.py @@ -43,6 +43,7 @@ from ..common import NotifyFormat, NotifyType from ..conversion import convert_between from ..utils import is_email, parse_emails from ..AppriseLocale import gettext_lazy as _ +from ..logger import logger # Globally Default encoding mode set to Quoted Printable. charset.add_charset('utf-8', charset.QP, charset.QP, 'utf-8') @@ -382,7 +383,7 @@ class NotifyEmail(NotifyBase): 'name': { 'name': _('From Name'), 'type': 'string', - 'map_to': 'from_name', + 'map_to': 'from_addr', }, 'cc': { 'name': _('Carbon Copy'), @@ -419,9 +420,9 @@ class NotifyEmail(NotifyBase): }, } - def __init__(self, smtp_host=None, from_name=None, - from_addr=None, secure_mode=None, targets=None, cc=None, - bcc=None, reply_to=None, headers=None, **kwargs): + def __init__(self, smtp_host=None, from_addr=None, secure_mode=None, + targets=None, cc=None, bcc=None, reply_to=None, headers=None, + **kwargs): """ Initialize Email Object @@ -460,31 +461,35 @@ class NotifyEmail(NotifyBase): # Now we want to construct the To and From email # addresses from the URL provided - self.from_addr = from_addr + self.from_addr = [False, ''] - if self.user and not self.from_addr: - # detect our email address - self.from_addr = '{}@{}'.format( + if self.user and self.host: + # Prepare the bases of our email + self.from_addr = [self.app_id, '{}@{}'.format( re.split(r'[\s@]+', self.user)[0], self.host, - ) + )] - result = is_email(self.from_addr) + if from_addr: + result = is_email(from_addr) + if result: + self.from_addr = ( + result['name'] if result['name'] else False, + result['full_email']) + else: + self.from_addr[0] = from_addr + + result = is_email(self.from_addr[1]) if not result: # Parse Source domain based on from_addr - msg = 'Invalid ~From~ email specified: {}'.format(self.from_addr) + msg = 'Invalid ~From~ email specified: {}'.format( + '{} <{}>'.format(self.from_addr[0], self.from_addr[1]) + if self.from_addr[0] else '{}'.format(self.from_addr[1])) self.logger.warning(msg) raise TypeError(msg) - # Store our email address - self.from_addr = result['full_email'] - - # Set our from name - self.from_name = from_name if from_name else result['name'] - # Store our lookup - self.names[self.from_addr] = \ - self.from_name if self.from_name else False + self.names[self.from_addr[1]] = self.from_addr[0] # Now detect the SMTP Server self.smtp_host = \ @@ -517,8 +522,7 @@ class NotifyEmail(NotifyBase): else: # If our target email list is empty we want to add ourselves to it - self.targets.append( - (self.from_name if self.from_name else False, self.from_addr)) + self.targets.append((False, self.from_addr[1])) # Validate recipients (cc:) and drop bad ones: for recipient in parse_emails(cc): @@ -665,9 +669,6 @@ class NotifyEmail(NotifyBase): Perform Email Notification """ - # Initialize our default from name - from_name = self.from_name if self.from_name else self.app_desc - if not self.targets: # There is no one to email; we're done self.logger.warning( @@ -708,7 +709,9 @@ class NotifyEmail(NotifyBase): for addr in reply_to] self.logger.debug( - 'Email From: {} <{}>'.format(from_name, self.from_addr)) + 'Email From: {}'.format( + formataddr(self.from_addr, charset='utf-8'))) + self.logger.debug('Email To: {}'.format(to_addr)) if cc: self.logger.debug('Email Cc: {}'.format(', '.join(cc))) @@ -771,9 +774,7 @@ class NotifyEmail(NotifyBase): base[k] = Header(v, self._get_charset(v)) base['Subject'] = Header(title, self._get_charset(title)) - base['From'] = formataddr( - (from_name if from_name else False, self.from_addr), - charset='utf-8') + base['From'] = formataddr(self.from_addr, charset='utf-8') base['To'] = formataddr((to_name, to_addr), charset='utf-8') base['Message-ID'] = make_msgid(domain=self.smtp_host) base['Date'] = \ @@ -833,7 +834,7 @@ class NotifyEmail(NotifyBase): for message in messages: try: socket.sendmail( - self.from_addr, + self.from_addr[1], message.to_addrs, message.body) @@ -868,12 +869,7 @@ class NotifyEmail(NotifyBase): """ # Define an URL parameters - params = { - 'from': self.from_addr, - 'mode': self.secure_mode, - 'smtp': self.smtp_host, - 'user': self.user, - } + params = {} # Append our headers into our parameters params.update({'+{}'.format(k): v for k, v in self.headers.items()}) @@ -881,30 +877,60 @@ class NotifyEmail(NotifyBase): # Extend our parameters params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) - if self.from_name: - params['name'] = self.from_name + from_addr = None + if len(self.targets) == 1 and self.targets[0][1] != self.from_addr[1]: + # A custom email was provided + from_addr = self.from_addr[1] + + if self.smtp_host != self.host: + # Apply our SMTP Host only if it differs from the provided hostname + params['smtp'] = self.smtp_host + + if self.secure: + # Mode is only requried if we're dealing with a secure connection + params['mode'] = self.secure_mode + + if self.from_addr[0] and self.from_addr[0] != self.app_id: + # A custom name was provided + params['from'] = self.from_addr[0] if not from_addr else \ + formataddr((self.from_addr[0], from_addr), charset='utf-8') + + elif from_addr: + params['from'] = formataddr((False, from_addr), charset='utf-8') + + elif not self.user: + params['from'] = \ + formataddr((False, self.from_addr[1]), charset='utf-8') if len(self.cc) > 0: # Handle our Carbon Copy Addresses - params['cc'] = ','.join( - ['{}{}'.format( - '' if not e not in self.names - else '{}:'.format(self.names[e]), e) for e in self.cc]) + params['cc'] = ','.join([ + formataddr( + (self.names[e] if e in self.names else False, e), + # Swap comma for it's escaped url code (if detected) since + # we're using that as a delimiter + charset='utf-8').replace(',', '%2C') + for e in self.cc]) if len(self.bcc) > 0: # Handle our Blind Carbon Copy Addresses - params['bcc'] = ','.join( - ['{}{}'.format( - '' if not e not in self.names - else '{}:'.format(self.names[e]), e) for e in self.bcc]) + params['bcc'] = ','.join([ + formataddr( + (self.names[e] if e in self.names else False, e), + # Swap comma for it's escaped url code (if detected) since + # we're using that as a delimiter + charset='utf-8').replace(',', '%2C') + for e in self.bcc]) if self.reply_to: # Handle our Reply-To Addresses - params['reply'] = ','.join( - ['{}{}'.format( - '' if not e not in self.names - else '{}:'.format(self.names[e]), e) - for e in self.reply_to]) + params['reply'] = ','.join([ + formataddr( + (self.names[e] if e in self.names else False, e), + # Swap comma for it's escaped url code (if detected) since + # we're using that as a delimiter + charset='utf-8').replace(',', '%2C') + for e in self.reply_to]) # pull email suffix from username (if present) user = None if not self.user else self.user.split('@')[0] @@ -931,7 +957,7 @@ class NotifyEmail(NotifyBase): # or not has_targets = \ not (len(self.targets) == 1 - and self.targets[0][1] == self.from_addr) + and self.targets[0][1] == self.from_addr[1]) return '{schema}://{auth}{hostname}{port}/{targets}?{params}'.format( schema=self.secure_protocol if self.secure else self.protocol, @@ -975,14 +1001,24 @@ class NotifyEmail(NotifyBase): if 'from' in results['qsd'] and len(results['qsd']['from']): from_addr = NotifyEmail.unquote(results['qsd']['from']) + if 'name' in results['qsd'] and len(results['qsd']['name']): + # Depricate use of both `from=` and `name=` in the same url as + # they will be synomomus of one another in the future. + from_addr = formataddr( + (NotifyEmail.unquote(results['qsd']['name']), from_addr), + charset='utf-8') + logger.warning( + 'Email name= and from= are synonymous; ' + 'use one or the other.') + + elif 'name' in results['qsd'] and len(results['qsd']['name']): + # 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']) - if 'name' in results['qsd'] and len(results['qsd']['name']): - # Extract from name to associate with from address - results['from_name'] = NotifyEmail.unquote(results['qsd']['name']) - # Store SMTP Host if specified if 'smtp' in results['qsd'] and len(results['qsd']['smtp']): # Extract the smtp server diff --git a/apprise/plugins/NotifyMailgun.py b/apprise/plugins/NotifyMailgun.py index 598577d6..7db387e4 100644 --- a/apprise/plugins/NotifyMailgun.py +++ b/apprise/plugins/NotifyMailgun.py @@ -60,6 +60,7 @@ from ..utils import parse_emails from ..utils import parse_bool from ..utils import is_email from ..utils import validate_regex +from ..logger import logger from ..AppriseLocale import gettext_lazy as _ # Provide some known codes Mailgun uses and what they translate to: @@ -158,7 +159,7 @@ class NotifyMailgun(NotifyBase): 'name': { 'name': _('From Name'), 'type': 'string', - 'map_to': 'from_name', + 'map_to': 'from_addr', }, 'from': { 'alias_of': 'name', @@ -200,7 +201,7 @@ class NotifyMailgun(NotifyBase): }, } - def __init__(self, apikey, targets, cc=None, bcc=None, from_name=None, + def __init__(self, apikey, targets, cc=None, bcc=None, from_addr=None, region_name=None, headers=None, tokens=None, batch=False, **kwargs): """ @@ -266,14 +267,15 @@ class NotifyMailgun(NotifyBase): self.from_addr = [ self.app_id, '{user}@{host}'.format( user=self.user, host=self.host)] - if from_name: - result = is_email(from_name) + + if from_addr: + result = is_email(from_addr) if result: self.from_addr = ( result['name'] if result['name'] else False, result['full_email']) else: - self.from_addr[0] = from_name + self.from_addr[0] = from_addr if not is_email(self.from_addr[1]): # Parse Source domain based on from_addr @@ -589,7 +591,7 @@ class NotifyMailgun(NotifyBase): params.update(self.url_parameters(privacy=privacy, *args, **kwargs)) if self.from_addr[0]: - # from_name specified; pass it back on the url + # from_addr specified; pass it back on the url params['name'] = self.from_addr[0] if self.cc: @@ -644,17 +646,26 @@ class NotifyMailgun(NotifyBase): # We're done - no API Key found results['apikey'] = None - if 'name' in results['qsd'] and len(results['qsd']['name']): - # Extract from name to associate with from address - results['from_name'] = \ - NotifyMailgun.unquote(results['qsd']['name']) - - # Support from= for consistency with `mail://` - elif 'from' in results['qsd'] and len(results['qsd']['from']): - # Extract from name to associate with from address - results['from_name'] = \ + # Attempt to detect 'from' email address + if 'from' in results['qsd'] and len(results['qsd']['from']): + results['from_addr'] = \ NotifyMailgun.unquote(results['qsd']['from']) + if 'name' in results['qsd'] and len(results['qsd']['name']): + # Depricate use of both `from=` and `name=` in the same url as + # they will be synomomus of one another in the future. + results['from_addr'] = formataddr( + (NotifyMailgun.unquote(results['qsd']['name']), + results['from_addr']), charset='utf-8') + logger.warning( + 'Mailgun name= and from= are synonymous; ' + 'use one or the other.') + + elif 'name' in results['qsd'] and len(results['qsd']['name']): + # Extract from name to associate with from address + results['from_addr'] = \ + NotifyMailgun.unquote(results['qsd']['name']) + if 'region' in results['qsd'] and len(results['qsd']['region']): # Extract from name to associate with from address results['region_name'] = \ diff --git a/apprise/utils.py b/apprise/utils.py index f1bbd40a..0f1876b2 100644 --- a/apprise/utils.py +++ b/apprise/utils.py @@ -519,7 +519,7 @@ def tidy_path(path): return path -def parse_qsd(qs, simple=False): +def parse_qsd(qs, simple=False, plus_to_space=False): """ Query String Dictionary Builder @@ -541,6 +541,11 @@ def parse_qsd(qs, simple=False): if simple is set to true, then a ONE dictionary is returned and is not sub-parsed for additional elements + + plus_to_space will cause all `+` references to become a space as + per normal URL Encoded defininition. Normal URL parsing applies + this, but `+` is very actively used character with passwords, + api keys, tokens, etc. So Apprise does not do this by default. """ # Our return result set: @@ -575,7 +580,7 @@ def parse_qsd(qs, simple=False): key = unquote(key) key = '' if not key else key - val = nv[1].replace('+', ' ') + val = nv[1].replace('+', ' ') if plus_to_space else nv[1] val = unquote(val) val = '' if not val else val.strip() @@ -609,7 +614,7 @@ def parse_qsd(qs, simple=False): def parse_url(url, default_schema='http', verify_host=True, strict_port=False, - simple=False): + simple=False, plus_to_space=False): """A function that greatly simplifies the parsing of a url specified by the end user. @@ -722,7 +727,8 @@ def parse_url(url, default_schema='http', verify_host=True, strict_port=False, # Parse Query Arugments ?val=key&key=val # while ensuring that all keys are lowercase if qsdata: - result.update(parse_qsd(qsdata, simple=simple)) + result.update(parse_qsd( + qsdata, simple=simple, plus_to_space=plus_to_space)) # Now do a proper extraction of data; http:// is just substitued in place # to allow urlparse() to function as expected, we'll swap this back to the diff --git a/test/test_apprise_utils.py b/test/test_apprise_utils.py index b034005f..9c877564 100644 --- a/test/test_apprise_utils.py +++ b/test/test_apprise_utils.py @@ -396,6 +396,31 @@ def test_parse_url_general(): assert result['qsd+']['KeY'] == 'ValueA' assert 'kEy' in result['qsd-'] assert result['qsd-']['kEy'] == 'ValueB' + assert result['qsd']['key'] == 'Value +C' + assert result['qsd']['+key'] == result['qsd+']['KeY'] + assert result['qsd']['-key'] == result['qsd-']['kEy'] + + result = utils.parse_url( + 'http://hostname/?+KeY=ValueA&-kEy=ValueB&KEY=Value%20+C&:colon=y', + plus_to_space=True) + assert result['schema'] == 'http' + assert result['host'] == 'hostname' + assert result['port'] is None + assert result['user'] is None + assert result['password'] is None + assert result['fullpath'] == '/' + assert result['path'] == '/' + assert result['query'] is None + assert result['url'] == 'http://hostname/' + assert '+key' in result['qsd'] + assert '-key' in result['qsd'] + assert ':colon' in result['qsd'] + assert result['qsd:']['colon'] == 'y' + assert 'key' in result['qsd'] + assert 'KeY' in result['qsd+'] + assert result['qsd+']['KeY'] == 'ValueA' + assert 'kEy' in result['qsd-'] + assert result['qsd-']['kEy'] == 'ValueB' assert result['qsd']['key'] == 'Value C' assert result['qsd']['+key'] == result['qsd+']['KeY'] assert result['qsd']['-key'] == result['qsd-']['kEy'] @@ -893,7 +918,7 @@ def test_parse_url_simple(): assert '-key' in result['qsd'] assert ':colon' in result['qsd'] assert result['qsd'][':colon'] == 'y' - assert result['qsd']['key'] == 'Value C' + assert result['qsd']['key'] == 'Value +C' assert result['qsd']['+key'] == 'ValueA' assert result['qsd']['-key'] == 'ValueB' diff --git a/test/test_plugin_email.py b/test/test_plugin_email.py index 259f74e5..ad43531a 100644 --- a/test/test_plugin_email.py +++ b/test/test_plugin_email.py @@ -178,9 +178,10 @@ TEST_URLS = ( ('mailtos://user:@nuxref.com', { 'instance': NotifyEmail, }), - # Invalid From Address + # Invalid From Address; but just gets put as the from name instead + # Hence the below generats From: "@ " ('mailtos://user:pass@nuxref.com?from=@', { - 'instance': TypeError, + 'instance': NotifyEmail, }), # Invalid From Address ('mailtos://nuxref.com?user=&pass=.', { @@ -226,6 +227,10 @@ TEST_URLS = ( # is set and tests that we gracfully handle them 'test_smtplib_exceptions': True, }), + # Use of both 'name' and 'from' together; these are synonymous + ('mailtos://user:pass@nuxref.com?' + 'from=jack@gmail.com&name=Jason', { + 'instance': NotifyEmail}), # Test no auth at all ('mailto://localhost?from=test@example.com&to=test@example.com', { 'instance': NotifyEmail, @@ -435,7 +440,8 @@ def test_plugin_email_webbase_lookup(mock_smtp, mock_smtpssl): assert isinstance(obj, NotifyEmail) assert len(obj.targets) == 1 assert (False, 'user@l2g.com') in obj.targets - assert obj.from_addr == 'user@l2g.com' + assert obj.from_addr[0] == obj.app_id + assert obj.from_addr[1] == 'user@l2g.com' assert obj.password == 'pass' assert obj.user == 'user' assert obj.secure is True @@ -569,6 +575,23 @@ def test_plugin_email_smtplib_send_multiple_recipients(mock_smtplib): mock.call().quit(), ] + # No from= used in the above + assert re.match(r'.*from=.*', obj.url()) is None + # No mode= as this isn't a secure connection + assert re.match(r'.*mode=.*', obj.url()) is None + # No smtp= as the SMTP server is the same as the hostname in this case + assert re.match(r'.*smtp=.*', obj.url()) is None + # URL is assembled based on provided user + assert re.match( + r'^mailto://user:pass\@mail.example.org/.*', obj.url()) is not None + + # Verify our added emails are still part of the URL + assert re.match(r'.*/foo%40example.net[/?].*', obj.url()) is not None + assert re.match(r'.*/bar%40example.com[/?].*', obj.url()) is not None + + assert re.match(r'.*bcc=qux%40example.org.*', obj.url()) is not None + assert re.match(r'.*cc=baz%40example.org.*', obj.url()) is not None + @mock.patch('smtplib.SMTP') def test_plugin_email_smtplib_internationalization(mock_smtp): @@ -672,7 +695,7 @@ def test_plugin_email_url_variations(): # Test variations of username required to be an email address # user@example.com obj = Apprise.instantiate( - 'mailto://{user}:{passwd}@example.com'.format( + 'mailto://{user}:{passwd}@example.com?smtp=example.com'.format( user='apprise%40example21.ca', passwd='abcd123'), suppress_exceptions=False) @@ -681,6 +704,17 @@ def test_plugin_email_url_variations(): assert obj.password == 'abcd123' assert obj.user == 'apprise@example21.ca' + # No from= used in the above + assert re.match(r'.*from=.*', obj.url()) is None + # No mode= as this isn't a secure connection + assert re.match(r'.*mode=.*', obj.url()) is None + # No smtp= as the SMTP server is the same as the hostname in this case + # even though it was explicitly specified + assert re.match(r'.*smtp=.*', obj.url()) is None + # URL is assembled based on provided user + assert re.match( + r'^mailto://apprise:abcd123\@example.com/.*', obj.url()) is not None + # test username specified in the url body (as an argument) # this always over-rides the entry at the front of the url obj = Apprise.instantiate( @@ -693,10 +727,20 @@ def test_plugin_email_url_variations(): assert obj.password == 'abcd123' assert obj.user == 'apprise@example21.ca' + # No from= used in the above + assert re.match(r'.*from=.*', obj.url()) is None + # No mode= as this isn't a secure connection + assert re.match(r'.*mode=.*', obj.url()) is None + # No smtp= as the SMTP server is the same as the hostname in this case + assert re.match(r'.*smtp=.*', obj.url()) is None + # URL is assembled based on provided user + assert re.match( + r'^mailto://apprise:abcd123\@example.com/.*', obj.url()) is not None + # test user and password specified in the url body (as an argument) # this always over-rides the entries at the front of the url obj = Apprise.instantiate( - 'mailto://_:_@example.com?user={user}&pass={passwd}'.format( + 'mailtos://_:_@example.com?user={user}&pass={passwd}'.format( user='apprise%40example21.ca', passwd='abcd123'), suppress_exceptions=False) @@ -706,7 +750,20 @@ def test_plugin_email_url_variations(): assert obj.user == 'apprise@example21.ca' assert len(obj.targets) == 1 assert (False, 'apprise@example.com') in obj.targets - assert obj.targets[0][1] == obj.from_addr + assert obj.from_addr[0] == obj.app_id + assert obj.from_addr[1] == 'apprise@example.com' + assert obj.targets[0][0] is False + assert obj.targets[0][1] == obj.from_addr[1] + + # No from= used in the above + assert re.match(r'.*from=.*', obj.url()) is None + # Default mode is starttls + assert re.match(r'.*mode=starttls.*', obj.url()) is not None + # No smtp= as the SMTP server is the same as the hostname in this case + assert re.match(r'.*smtp=.*', obj.url()) is None + # URL is assembled based on provided user + assert re.match( + r'^mailtos://apprise:abcd123\@example.com/.*', obj.url()) is not None # test user and password specified in the url body (as an argument) # this always over-rides the entries at the front of the url @@ -723,13 +780,26 @@ def test_plugin_email_url_variations(): assert obj.user == 'apprise@example21.ca' assert len(obj.targets) == 1 assert (False, 'apprise@example.com') in obj.targets - assert obj.targets[0][1] == obj.from_addr + assert obj.from_addr[0] == obj.app_id + assert obj.from_addr[1] == 'apprise@example.com' + assert obj.targets[0][0] is False + assert obj.targets[0][1] == obj.from_addr[1] assert obj.smtp_host == 'example.com' + # No from= used in the above + assert re.match(r'.*from=.*', obj.url()) is None + # No mode= as this isn't a secure connection + assert re.match(r'.*mode=.*', obj.url()) is None + # No smtp= as the SMTP server is the same as the hostname in this case + assert re.match(r'.*smtp=.*', obj.url()) is None + # URL is assembled based on provided user + assert re.match( + r'^mailto://apprise:abcd123\@example.com/.*', obj.url()) is not None + # test a complicated example obj = Apprise.instantiate( 'mailtos://{user}:{passwd}@{host}:{port}' - '?smtp={smtp_host}&format=text&from={this}&to={that}'.format( + '?smtp={smtp_host}&format=text&from=Charles<{this}>&to={that}'.format( user='apprise%40example21.ca', passwd='abcd123', host='example.com', @@ -747,7 +817,28 @@ def test_plugin_email_url_variations(): assert obj.smtp_host == 'smtp.example.edu' assert len(obj.targets) == 1 assert (False, 'to@example.jp') in obj.targets - assert obj.from_addr == 'from@example.jp' + assert obj.from_addr[0] == 'Charles' + assert obj.from_addr[1] == 'from@example.jp' + assert re.match( + r'.*from=Charles\+%3Cfrom%40example.jp%3E.*', obj.url()) is not None + + # Test Tagging under various urll encodings + for toaddr in ('/john.smith+mytag@domain.com', + '?to=john.smith+mytag@domain.com', + '/john.smith%2Bmytag@domain.com', + '?to=john.smith%2Bmytag@domain.com'): + + obj = Apprise.instantiate( + 'mailto://user:pass@domain.com{}'.format(toaddr)) + assert isinstance(obj, NotifyEmail) is True + assert obj.password == 'pass' + assert obj.user == 'user' + assert obj.host == 'domain.com' + assert obj.from_addr[0] == obj.app_id + assert obj.from_addr[1] == 'user@domain.com' + assert len(obj.targets) == 1 + assert obj.targets[0][0] is False + assert obj.targets[0][1] == 'john.smith+mytag@domain.com' def test_plugin_email_dict_variations(): @@ -784,7 +875,7 @@ def test_plugin_email_url_parsing(mock_smtp, mock_smtp_ssl): 'mailtos://user:pass123@hotmail.com:444' '?to=user2@yahoo.com&name=test%20name') assert isinstance(results, dict) - assert 'test name' == results['from_name'] + assert 'test name' == results['from_addr'] assert 'user' == results['user'] assert 444 == results['port'] assert 'hotmail.com' == results['host'] @@ -832,7 +923,7 @@ def test_plugin_email_url_parsing(mock_smtp, mock_smtp_ssl): 'mailtos://user:pass123@hotmail.com?smtp=override.com' '&name=test%20name&to=user2@yahoo.com&mode=ssl') assert isinstance(results, dict) - assert 'test name' == results['from_name'] + assert 'test name' == results['from_addr'] assert 'user' == results['user'] assert 'hotmail.com' == results['host'] assert 'pass123' == results['password'] @@ -918,11 +1009,22 @@ def test_plugin_email_url_parsing(mock_smtp, mock_smtp_ssl): # Verify our over-rides are in place assert obj.smtp_host == 'smtp.exmail.qq.com' assert obj.port == 465 - assert obj.from_addr == 'abc@xyz.cn' + assert obj.from_addr[0] == obj.app_id + assert obj.from_addr[1] == 'abc@xyz.cn' assert obj.secure_mode == 'ssl' # No entries in the reply_to assert not obj.reply_to + # No from= used in the above + assert re.match(r'.*from=.*', obj.url()) is None + # No Our secure connection is SSL + assert re.match(r'.*mode=ssl.*', obj.url()) is not None + # No smtp= as the SMTP server is the same as the hostname in this case + assert re.match(r'.*smtp=smtp.exmail.qq.com.*', obj.url()) is not None + # URL is assembled based on provided user + assert re.match( + r'^mailtos://abc:password@xyz.cn:465/.*', obj.url()) is not None + results = NotifyEmail.parse_url( "mailtos://abc:password@xyz.cn?" "smtp=smtp.exmail.qq.com&mode=ssl&port=465") @@ -932,7 +1034,8 @@ def test_plugin_email_url_parsing(mock_smtp, mock_smtp_ssl): # Verify our over-rides are in place assert obj.smtp_host == 'smtp.exmail.qq.com' assert obj.port == 465 - assert obj.from_addr == 'abc@xyz.cn' + assert obj.from_addr[0] == obj.app_id + assert obj.from_addr[1] == 'abc@xyz.cn' assert obj.secure_mode == 'ssl' # No entries in the reply_to assert not obj.reply_to @@ -946,9 +1049,27 @@ def test_plugin_email_url_parsing(mock_smtp, mock_smtp_ssl): assert isinstance(obj, NotifyEmail) is True # Verify our over-rides are in place assert obj.smtp_host == 'example.com' - assert obj.from_addr == 'user@example.com' + assert obj.from_addr[0] == obj.app_id + assert obj.from_addr[1] == 'user@example.com' assert obj.secure_mode == 'starttls' assert obj.url().startswith( 'mailtos://user:pass@example.com') # Test that our template over-ride worked assert 'reply=noreply%40example.com' in obj.url() + + # + # Test Reply-To Email with Name Inline + # + results = NotifyEmail.parse_url( + "mailtos://user:pass@example.com?reply=Chris") + obj = Apprise.instantiate(results, suppress_exceptions=False) + assert isinstance(obj, NotifyEmail) is True + # Verify our over-rides are in place + assert obj.smtp_host == 'example.com' + assert obj.from_addr[0] == obj.app_id + assert obj.from_addr[1] == 'user@example.com' + assert obj.secure_mode == 'starttls' + assert obj.url().startswith( + 'mailtos://user:pass@example.com') + # Test that our template over-ride worked + assert 'reply=Chris+%3Cnoreply%40example.ca%3E' in obj.url() diff --git a/test/test_plugin_mailgun.py b/test/test_plugin_mailgun.py index 697fbe63..50b7f744 100644 --- a/test/test_plugin_mailgun.py +++ b/test/test_plugin_mailgun.py @@ -95,6 +95,12 @@ apprise_url_tests = ( 'a' * 32, 'b' * 8, 'c' * 8), { 'instance': TypeError, }), + # Use of both 'name' and 'from' together; these are synonymous + ('mailgun://user@localhost.localdomain/{}-{}-{}?' + 'from=jack@gmail.com&name=Jason'.format( + 'a' * 32, 'b' * 8, 'c' * 8), { + 'instance': NotifyMailgun}), + # headers ('mailgun://user@localhost.localdomain/{}-{}-{}' '?+X-Customer-Campaign-ID=Apprise'.format(