Email improvements; name= and from= now synonymous (#738)

This commit is contained in:
Chris Caron 2022-11-05 16:37:57 -04:00 committed by GitHub
parent e7255df1da
commit 32992fa641
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 300 additions and 93 deletions

View File

@ -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

View File

@ -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

View File

@ -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,29 +877,59 @@ 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)
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)
@ -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

View File

@ -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'] = \

View File

@ -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

View File

@ -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'

View File

@ -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: "@ <user@nuxref.com>"
('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<jason@gmail.com>', {
'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<noreply@example.ca>")
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()

View File

@ -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<jason@gmail.com>'.format(
'a' * 32, 'b' * 8, 'c' * 8), {
'instance': NotifyMailgun}),
# headers
('mailgun://user@localhost.localdomain/{}-{}-{}'
'?+X-Customer-Campaign-ID=Apprise'.format(