Reformat code with automated tools

Apply standardized code style
This commit is contained in:
medmunds
2023-02-06 12:27:43 -08:00
committed by Mike Edmunds
parent 40891fcb4a
commit b4e22c63b3
94 changed files with 12936 additions and 7443 deletions

View File

@@ -2,9 +2,9 @@ import json
from io import BytesIO
from unittest.mock import patch
import requests
from django.core import mail
from django.test import SimpleTestCase
import requests
from anymail.exceptions import AnymailAPIError
@@ -21,7 +21,15 @@ class RequestsBackendMockAPITestCase(AnymailTestMixin, SimpleTestCase):
class MockResponse(requests.Response):
"""requests.request return value mock sufficient for testing"""
def __init__(self, status_code=200, raw=b"RESPONSE", encoding='utf-8', reason=None, test_case=None):
def __init__(
self,
status_code=200,
raw=b"RESPONSE",
encoding="utf-8",
reason=None,
test_case=None,
):
super().__init__()
self.status_code = status_code
self.encoding = encoding
@@ -31,7 +39,7 @@ class RequestsBackendMockAPITestCase(AnymailTestMixin, SimpleTestCase):
@property
def url(self):
return self.test_case.get_api_call_arg('url', required=False)
return self.test_case.get_api_call_arg("url", required=False)
@url.setter
def url(self, url):
@@ -40,34 +48,45 @@ class RequestsBackendMockAPITestCase(AnymailTestMixin, SimpleTestCase):
def setUp(self):
super().setUp()
self.patch_request = patch('requests.Session.request', autospec=True)
self.patch_request = patch("requests.Session.request", autospec=True)
self.mock_request = self.patch_request.start()
self.addCleanup(self.patch_request.stop)
self.set_mock_response()
def set_mock_response(self, status_code=DEFAULT_STATUS_CODE, raw=UNSET, encoding='utf-8', reason=None):
def set_mock_response(
self, status_code=DEFAULT_STATUS_CODE, raw=UNSET, encoding="utf-8", reason=None
):
if raw is UNSET:
raw = self.DEFAULT_RAW_RESPONSE
mock_response = self.MockResponse(status_code, raw=raw, encoding=encoding, reason=reason, test_case=self)
mock_response = self.MockResponse(
status_code, raw=raw, encoding=encoding, reason=reason, test_case=self
)
self.mock_request.return_value = mock_response
return mock_response
def assert_esp_called(self, url, method="POST"):
"""Verifies the (mock) ESP API was called on endpoint.
url can be partial, and is just checked against the end of the url requested"
url can be partial, and is just checked against the end of the url requested
"""
# This assumes the last (or only) call to requests.Session.request is the API call of interest.
# This assumes the last (or only) call to requests.Session.request
# is the API call of interest.
if self.mock_request.call_args is None:
raise AssertionError("No ESP API was called")
if method is not None:
actual_method = self.get_api_call_arg('method')
actual_method = self.get_api_call_arg("method")
if actual_method != method:
self.fail("API was not called using %s. (%s was used instead.)" % (method, actual_method))
self.fail(
"API was not called using %s. (%s was used instead.)"
% (method, actual_method)
)
if url is not None:
actual_url = self.get_api_call_arg('url')
actual_url = self.get_api_call_arg("url")
if not actual_url.endswith(url):
self.fail("API was not called at %s\n(It was called at %s)" % (url, actual_url))
self.fail(
"API was not called at %s\n(It was called at %s)"
% (url, actual_url)
)
def get_api_call_arg(self, kwarg, required=True):
"""Returns an argument passed to the mock ESP API.
@@ -84,9 +103,24 @@ class RequestsBackendMockAPITestCase(AnymailTestMixin, SimpleTestCase):
try:
# positional arg? This is the order of requests.Session.request params:
pos = ('method', 'url', 'params', 'data', 'headers', 'cookies', 'files', 'auth',
'timeout', 'allow_redirects', 'proxies', 'hooks', 'stream', 'verify', 'cert', 'json',
).index(kwarg)
pos = (
"method",
"url",
"params",
"data",
"headers",
"cookies",
"files",
"auth",
"timeout",
"allow_redirects",
"proxies",
"hooks",
"stream",
"verify",
"cert",
"json",
).index(kwarg)
return args[pos]
except (ValueError, IndexError):
pass
@@ -97,37 +131,38 @@ class RequestsBackendMockAPITestCase(AnymailTestMixin, SimpleTestCase):
def get_api_call_params(self, required=True):
"""Returns the query params sent to the mock ESP API."""
return self.get_api_call_arg('params', required)
return self.get_api_call_arg("params", required)
def get_api_call_data(self, required=True):
"""Returns the raw data sent to the mock ESP API."""
return self.get_api_call_arg('data', required)
return self.get_api_call_arg("data", required)
def get_api_call_json(self, required=True):
"""Returns the data sent to the mock ESP API, json-parsed"""
# could be either the data param (as json str) or the json param (needing formatting)
value = self.get_api_call_arg('data', required=False)
# could be either the data param (as json str)
# or the json param (needing formatting)
value = self.get_api_call_arg("data", required=False)
if value is not None:
return json.loads(value)
else:
return self.get_api_call_arg('json', required)
return self.get_api_call_arg("json", required)
def get_api_call_headers(self, required=True):
"""Returns the headers sent to the mock ESP API"""
return self.get_api_call_arg('headers', required)
return self.get_api_call_arg("headers", required)
def get_api_call_files(self, required=True):
"""Returns the files sent to the mock ESP API"""
return self.get_api_call_arg('files', required)
return self.get_api_call_arg("files", required)
def get_api_call_auth(self, required=True):
"""Returns the auth sent to the mock ESP API"""
return self.get_api_call_arg('auth', required)
return self.get_api_call_arg("auth", required)
def get_api_prepared_request(self):
"""Returns the PreparedRequest that would have been sent"""
(args, kwargs) = self.mock_request.call_args
kwargs.pop('timeout', None) # Session-only param
kwargs.pop("timeout", None) # Session-only param
request = requests.Request(**kwargs)
return request.prepare()
@@ -144,10 +179,10 @@ class SessionSharingTestCases(RequestsBackendMockAPITestCase):
- adding or overriding any tests as appropriate
"""
def __init__(self, methodName='runTest'):
def __init__(self, methodName="runTest"):
if self.__class__ is SessionSharingTestCases:
# don't run these tests on the abstract base implementation
methodName = 'runNoTestsInBaseClass'
methodName = "runNoTestsInBaseClass"
super().__init__(methodName)
def runNoTestsInBaseClass(self):
@@ -155,15 +190,15 @@ class SessionSharingTestCases(RequestsBackendMockAPITestCase):
def setUp(self):
super().setUp()
self.patch_close = patch('requests.Session.close', autospec=True)
self.patch_close = patch("requests.Session.close", autospec=True)
self.mock_close = self.patch_close.start()
self.addCleanup(self.patch_close.stop)
def test_connection_sharing(self):
"""RequestsBackend reuses one requests session when sending multiple messages"""
datatuple = (
('Subject 1', 'Body 1', 'from@example.com', ['to@example.com']),
('Subject 2', 'Body 2', 'from@example.com', ['to@example.com']),
("Subject 1", "Body 1", "from@example.com", ["to@example.com"]),
("Subject 2", "Body 2", "from@example.com", ["to@example.com"]),
)
mail.send_mass_mail(datatuple)
self.assertEqual(self.mock_request.call_count, 2)
@@ -176,11 +211,23 @@ class SessionSharingTestCases(RequestsBackendMockAPITestCase):
"""Calling code can created long-lived connection that it opens and closes"""
connection = mail.get_connection()
connection.open()
mail.send_mail('Subject 1', 'body', 'from@example.com', ['to@example.com'], connection=connection)
mail.send_mail(
"Subject 1",
"body",
"from@example.com",
["to@example.com"],
connection=connection,
)
session1 = self.mock_request.call_args[0]
self.assertEqual(self.mock_close.call_count, 0) # shouldn't be closed yet
mail.send_mail('Subject 2', 'body', 'from@example.com', ['to@example.com'], connection=connection)
mail.send_mail(
"Subject 2",
"body",
"from@example.com",
["to@example.com"],
connection=connection,
)
self.assertEqual(self.mock_close.call_count, 0) # still shouldn't be closed
session2 = self.mock_request.call_args[0]
self.assertEqual(session1, session2) # should have reused same session
@@ -191,13 +238,18 @@ class SessionSharingTestCases(RequestsBackendMockAPITestCase):
def test_session_closed_after_exception(self):
self.set_mock_response(status_code=500)
with self.assertRaises(AnymailAPIError):
mail.send_mail('Subject', 'Message', 'from@example.com', ['to@example.com'])
mail.send_mail("Subject", "Message", "from@example.com", ["to@example.com"])
self.assertEqual(self.mock_close.call_count, 1)
def test_session_closed_after_fail_silently_exception(self):
self.set_mock_response(status_code=500)
sent = mail.send_mail('Subject', 'Message', 'from@example.com', ['to@example.com'],
fail_silently=True)
sent = mail.send_mail(
"Subject",
"Message",
"from@example.com",
["to@example.com"],
fail_silently=True,
)
self.assertEqual(sent, 0)
self.assertEqual(self.mock_close.call_count, 1)
@@ -206,8 +258,13 @@ class SessionSharingTestCases(RequestsBackendMockAPITestCase):
connection.open()
self.set_mock_response(status_code=500)
with self.assertRaises(AnymailAPIError):
mail.send_mail('Subject', 'Message', 'from@example.com', ['to@example.com'],
connection=connection)
mail.send_mail(
"Subject",
"Message",
"from@example.com",
["to@example.com"],
connection=connection,
)
self.assertEqual(self.mock_close.call_count, 0) # wait for us to close it
connection.close()

File diff suppressed because it is too large Load Diff

View File

@@ -15,26 +15,30 @@ from .test_amazon_ses_webhooks import AmazonSESWebhookTestsMixin
from .webhook_cases import WebhookTestCase
@tag('amazon_ses')
@tag("amazon_ses")
class AmazonSESInboundTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
def setUp(self):
super().setUp()
# Mock boto3.session.Session().client('s3').download_fileobj
# (We could also use botocore.stub.Stubber, but mock works well with our test structure)
self.patch_boto3_session = patch('anymail.webhooks.amazon_ses.boto3.session.Session', autospec=True)
# Mock boto3.session.Session().client('s3').download_fileobj. (We could also
# use botocore.stub.Stubber, but mock works well with our test structure.)
self.patch_boto3_session = patch(
"anymail.webhooks.amazon_ses.boto3.session.Session", autospec=True
)
self.mock_session = self.patch_boto3_session.start() # boto3.session.Session
self.addCleanup(self.patch_boto3_session.stop)
def mock_download_fileobj(bucket, key, fileobj):
fileobj.write(self.mock_s3_downloadables[bucket][key])
self.mock_s3_downloadables = {} # bucket: key: bytes
self.mock_client = self.mock_session.return_value.client # boto3.session.Session().client
self.mock_s3 = self.mock_client.return_value # boto3.session.Session().client('s3', ...)
self.mock_s3_downloadables = {} #: bucket: key: bytes
#: boto3.session.Session().client
self.mock_client = self.mock_session.return_value.client
#: boto3.session.Session().client('s3', ...)
self.mock_s3 = self.mock_client.return_value
self.mock_s3.download_fileobj.side_effect = mock_download_fileobj
TEST_MIME_MESSAGE = dedent("""\
TEST_MIME_MESSAGE = dedent(
"""\
Return-Path: <bounce-handler@mail.example.org>
Received: from mail.example.org by inbound-smtp.us-east-1.amazonaws.com...
MIME-Version: 1.0
@@ -59,7 +63,8 @@ class AmazonSESInboundTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
<div dir=3D"ltr">It's a body=E2=80=A6</div>
--94eb2c05e174adb140055b6339c5--
""").replace("\n", "\r\n")
"""
).replace("\n", "\r\n")
def test_inbound_sns_utf8(self):
raw_ses_event = {
@@ -67,27 +72,50 @@ class AmazonSESInboundTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
"mail": {
"timestamp": "2018-03-30T17:21:51.636Z",
"source": "envelope-from@example.org",
"messageId": "jili9m351il3gkburn7o2f0u6788stij94c8ld01", # assigned by Amazon SES
# messageId is assigned by Amazon SES:
"messageId": "jili9m351il3gkburn7o2f0u6788stij94c8ld01",
"destination": ["inbound@example.com", "someone-else@example.org"],
"headersTruncated": False,
"headers": [
# (omitting a few headers that Amazon SES adds on receipt)
{"name": "Return-Path", "value": "<bounce-handler@mail.example.org>"},
{"name": "Received", "value": "from mail.example.org by inbound-smtp.us-east-1.amazonaws.com..."},
{
"name": "Return-Path",
"value": "<bounce-handler@mail.example.org>",
},
{
"name": "Received",
"value": "from mail.example.org by"
" inbound-smtp.us-east-1.amazonaws.com...",
},
{"name": "MIME-Version", "value": "1.0"},
{"name": "Received", "value": "by 10.1.1.1 with HTTP; Fri, 30 Mar 2018 10:21:49 -0700 (PDT)"},
{
"name": "Received",
"value": "by 10.1.1.1 with HTTP;"
" Fri, 30 Mar 2018 10:21:49 -0700 (PDT)",
},
{"name": "From", "value": '"Sender, Inc." <from@example.org>'},
{"name": "Date", "value": "Fri, 30 Mar 2018 10:21:50 -0700"},
{"name": "Message-ID", "value": "<CAEPk3RKsi@mail.example.org>"},
{"name": "Subject", "value": "Test inbound message"},
{"name": "To", "value": "Recipient <inbound@example.com>, someone-else@example.org"},
{"name": "Content-Type", "value": 'multipart/alternative; boundary="94eb2c05e174adb140055b6339c5"'},
{
"name": "To",
"value": "Recipient <inbound@example.com>,"
" someone-else@example.org",
},
{
"name": "Content-Type",
"value": "multipart/alternative;"
' boundary="94eb2c05e174adb140055b6339c5"',
},
],
"commonHeaders": {
"returnPath": "bounce-handler@mail.example.org",
"from": ['"Sender, Inc." <from@example.org>'],
"date": "Fri, 30 Mar 2018 10:21:50 -0700",
"to": ["Recipient <inbound@example.com>", "someone-else@example.org"],
"to": [
"Recipient <inbound@example.com>",
"someone-else@example.org",
],
"messageId": "<CAEPk3RKsi@mail.example.org>",
"subject": "Test inbound message",
},
@@ -119,32 +147,46 @@ class AmazonSESInboundTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
"Timestamp": "2018-03-30T17:17:36.516Z",
"SignatureVersion": "1",
"Signature": "EXAMPLE_SIGNATURE==",
"SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-12345abcde.pem",
"UnsubscribeURL": "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn...",
"SigningCertURL": "https://sns.us-east-1.amazonaws.com"
"/SimpleNotificationService-12345abcde.pem",
"UnsubscribeURL": "https://sns.us-east-1.amazonaws.com"
"/?Action=Unsubscribe&SubscriptionArn=arn...",
}
response = self.post_from_sns('/anymail/amazon_ses/inbound/', raw_sns_message)
response = self.post_from_sns("/anymail/amazon_ses/inbound/", raw_sns_message)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.inbound_handler, sender=AmazonSESInboundWebhookView,
event=ANY, esp_name='Amazon SES')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.inbound_handler,
sender=AmazonSESInboundWebhookView,
event=ANY,
esp_name="Amazon SES",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailInboundEvent)
self.assertEqual(event.event_type, 'inbound')
self.assertEqual(event.timestamp, datetime(2018, 3, 30, 17, 21, 51, microsecond=636000, tzinfo=timezone.utc))
self.assertEqual(event.event_type, "inbound")
self.assertEqual(
event.timestamp,
datetime(2018, 3, 30, 17, 21, 51, microsecond=636000, tzinfo=timezone.utc),
)
self.assertEqual(event.event_id, "jili9m351il3gkburn7o2f0u6788stij94c8ld01")
self.assertIsInstance(event.message, AnymailInboundMessage)
self.assertEqual(event.esp_event, raw_ses_event)
message = event.message
self.assertIsInstance(message, AnymailInboundMessage)
self.assertEqual(message.envelope_sender, 'envelope-from@example.org')
self.assertEqual(message.envelope_recipient, 'inbound@example.com')
self.assertEqual(message.envelope_sender, "envelope-from@example.org")
self.assertEqual(message.envelope_recipient, "inbound@example.com")
self.assertEqual(str(message.from_email), '"Sender, Inc." <from@example.org>')
self.assertEqual([str(to) for to in message.to],
['Recipient <inbound@example.com>', 'someone-else@example.org'])
self.assertEqual(message.subject, 'Test inbound message')
self.assertEqual(
[str(to) for to in message.to],
["Recipient <inbound@example.com>", "someone-else@example.org"],
)
self.assertEqual(message.subject, "Test inbound message")
self.assertEqual(message.text, "It's a body\N{HORIZONTAL ELLIPSIS}\r\n")
self.assertEqual(message.html, """<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\r\n""")
self.assertEqual(
message.html,
"""<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\r\n""",
)
self.assertIs(message.spam_detected, False)
def test_inbound_sns_base64(self):
@@ -155,7 +197,8 @@ class AmazonSESInboundTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
"mail": {
"source": "envelope-from@example.org",
"timestamp": "2018-03-30T17:21:51.636Z",
"messageId": "jili9m351il3gkburn7o2f0u6788stij94c8ld01", # assigned by Amazon SES
# messageId is assigned by Amazon SES
"messageId": "jili9m351il3gkburn7o2f0u6788stij94c8ld01",
"destination": ["inbound@example.com", "someone-else@example.org"],
},
"receipt": {
@@ -167,7 +210,9 @@ class AmazonSESInboundTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
},
"spamVerdict": {"status": "FAIL"},
},
"content": b64encode(self.TEST_MIME_MESSAGE.encode('ascii')).decode('ascii'),
"content": b64encode(self.TEST_MIME_MESSAGE.encode("ascii")).decode(
"ascii"
),
}
raw_sns_message = {
@@ -177,35 +222,49 @@ class AmazonSESInboundTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
"Message": json.dumps(raw_ses_event),
}
response = self.post_from_sns('/anymail/amazon_ses/inbound/', raw_sns_message)
response = self.post_from_sns("/anymail/amazon_ses/inbound/", raw_sns_message)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.inbound_handler, sender=AmazonSESInboundWebhookView,
event=ANY, esp_name='Amazon SES')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.inbound_handler,
sender=AmazonSESInboundWebhookView,
event=ANY,
esp_name="Amazon SES",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailInboundEvent)
self.assertEqual(event.event_type, 'inbound')
self.assertEqual(event.timestamp, datetime(2018, 3, 30, 17, 21, 51, microsecond=636000, tzinfo=timezone.utc))
self.assertEqual(event.event_type, "inbound")
self.assertEqual(
event.timestamp,
datetime(2018, 3, 30, 17, 21, 51, microsecond=636000, tzinfo=timezone.utc),
)
self.assertEqual(event.event_id, "jili9m351il3gkburn7o2f0u6788stij94c8ld01")
self.assertIsInstance(event.message, AnymailInboundMessage)
self.assertEqual(event.esp_event, raw_ses_event)
message = event.message
self.assertIsInstance(message, AnymailInboundMessage)
self.assertEqual(message.envelope_sender, 'envelope-from@example.org')
self.assertEqual(message.envelope_recipient, 'inbound@example.com')
self.assertEqual(message.envelope_sender, "envelope-from@example.org")
self.assertEqual(message.envelope_recipient, "inbound@example.com")
self.assertEqual(str(message.from_email), '"Sender, Inc." <from@example.org>')
self.assertEqual([str(to) for to in message.to],
['Recipient <inbound@example.com>', 'someone-else@example.org'])
self.assertEqual(message.subject, 'Test inbound message')
self.assertEqual(
[str(to) for to in message.to],
["Recipient <inbound@example.com>", "someone-else@example.org"],
)
self.assertEqual(message.subject, "Test inbound message")
self.assertEqual(message.text, "It's a body\N{HORIZONTAL ELLIPSIS}\r\n")
self.assertEqual(message.html, """<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\r\n""")
self.assertEqual(
message.html,
"""<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\r\n""",
)
self.assertIs(message.spam_detected, True)
def test_inbound_s3(self):
"""Should handle 'S3' receipt action"""
self.mock_s3_downloadables["InboundEmailBucket-KeepPrivate"] = {
"inbound/fqef5sop459utgdf4o9lqbsv7jeo73pejig34301": self.TEST_MIME_MESSAGE.encode('ascii')
"inbound/fqef5sop459utgdf4o9lqbsv7jeo73pejig34301": (
self.TEST_MIME_MESSAGE.encode("ascii")
)
}
raw_ses_event = {
@@ -214,7 +273,8 @@ class AmazonSESInboundTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
"mail": {
"source": "envelope-from@example.org",
"timestamp": "2018-03-30T17:21:51.636Z",
"messageId": "fqef5sop459utgdf4o9lqbsv7jeo73pejig34301", # assigned by Amazon SES
# messageId is assigned by Amazon SES
"messageId": "fqef5sop459utgdf4o9lqbsv7jeo73pejig34301",
"destination": ["inbound@example.com", "someone-else@example.org"],
},
"receipt": {
@@ -224,7 +284,7 @@ class AmazonSESInboundTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
"topicArn": "arn:aws:sns:us-east-1:111111111111:SES_Inbound",
"bucketName": "InboundEmailBucket-KeepPrivate",
"objectKeyPrefix": "inbound",
"objectKey": "inbound/fqef5sop459utgdf4o9lqbsv7jeo73pejig34301"
"objectKey": "inbound/fqef5sop459utgdf4o9lqbsv7jeo73pejig34301",
},
"spamVerdict": {"status": "GRAY"},
},
@@ -235,46 +295,69 @@ class AmazonSESInboundTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
"TopicArn": "arn:aws:sns:us-east-1:111111111111:SES_Inbound",
"Message": json.dumps(raw_ses_event),
}
response = self.post_from_sns('/anymail/amazon_ses/inbound/', raw_sns_message)
response = self.post_from_sns("/anymail/amazon_ses/inbound/", raw_sns_message)
self.assertEqual(response.status_code, 200)
self.mock_client.assert_called_once_with('s3', config=ANY)
self.mock_client.assert_called_once_with("s3", config=ANY)
self.mock_s3.download_fileobj.assert_called_once_with(
"InboundEmailBucket-KeepPrivate", "inbound/fqef5sop459utgdf4o9lqbsv7jeo73pejig34301", ANY)
"InboundEmailBucket-KeepPrivate",
"inbound/fqef5sop459utgdf4o9lqbsv7jeo73pejig34301",
ANY,
)
kwargs = self.assert_handler_called_once_with(self.inbound_handler, sender=AmazonSESInboundWebhookView,
event=ANY, esp_name='Amazon SES')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.inbound_handler,
sender=AmazonSESInboundWebhookView,
event=ANY,
esp_name="Amazon SES",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailInboundEvent)
self.assertEqual(event.event_type, 'inbound')
self.assertEqual(event.timestamp, datetime(2018, 3, 30, 17, 21, 51, microsecond=636000, tzinfo=timezone.utc))
self.assertEqual(event.event_type, "inbound")
self.assertEqual(
event.timestamp,
datetime(2018, 3, 30, 17, 21, 51, microsecond=636000, tzinfo=timezone.utc),
)
self.assertEqual(event.event_id, "fqef5sop459utgdf4o9lqbsv7jeo73pejig34301")
self.assertIsInstance(event.message, AnymailInboundMessage)
self.assertEqual(event.esp_event, raw_ses_event)
message = event.message
self.assertIsInstance(message, AnymailInboundMessage)
self.assertEqual(message.envelope_sender, 'envelope-from@example.org')
self.assertEqual(message.envelope_recipient, 'inbound@example.com')
self.assertEqual(message.envelope_sender, "envelope-from@example.org")
self.assertEqual(message.envelope_recipient, "inbound@example.com")
self.assertEqual(str(message.from_email), '"Sender, Inc." <from@example.org>')
self.assertEqual([str(to) for to in message.to],
['Recipient <inbound@example.com>', 'someone-else@example.org'])
self.assertEqual(message.subject, 'Test inbound message')
self.assertEqual(
[str(to) for to in message.to],
["Recipient <inbound@example.com>", "someone-else@example.org"],
)
self.assertEqual(message.subject, "Test inbound message")
self.assertEqual(message.text, "It's a body\N{HORIZONTAL ELLIPSIS}\n")
self.assertEqual(message.html, """<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\n""")
self.assertEqual(
message.html,
"""<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\n""",
)
self.assertIsNone(message.spam_detected)
def test_inbound_s3_failure_message(self):
"""Issue a helpful error when S3 download fails"""
# Boto's error: "An error occurred (403) when calling the HeadObject operation: Forbidden")
# Boto's error:
# "An error occurred (403) when calling the HeadObject operation: Forbidden"
from botocore.exceptions import ClientError
self.mock_s3.download_fileobj.side_effect = ClientError(
{'Error': {'Code': 403, 'Message': 'Forbidden'}}, operation_name='HeadObject')
{"Error": {"Code": 403, "Message": "Forbidden"}},
operation_name="HeadObject",
)
raw_ses_event = {
"notificationType": "Received",
"receipt": {
"action": {"type": "S3", "bucketName": "YourBucket", "objectKey": "inbound/the_object_key"}
"action": {
"type": "S3",
"bucketName": "YourBucket",
"objectKey": "inbound/the_object_key",
}
},
}
raw_sns_message = {
@@ -285,12 +368,18 @@ class AmazonSESInboundTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
}
with self.assertRaisesMessage(
AnymailAPIError,
"Anymail AmazonSESInboundWebhookView couldn't download S3 object 'YourBucket:inbound/the_object_key'"
"Anymail AmazonSESInboundWebhookView couldn't download"
" S3 object 'YourBucket:inbound/the_object_key'",
) as cm:
self.post_from_sns('/anymail/amazon_ses/inbound/', raw_sns_message)
self.assertIsInstance(cm.exception, ClientError) # both Boto and Anymail exception class
self.assertIn("ClientError: An error occurred (403) when calling the HeadObject operation: Forbidden",
str(cm.exception)) # original Boto message included
self.post_from_sns("/anymail/amazon_ses/inbound/", raw_sns_message)
# both Boto and Anymail exception class:
self.assertIsInstance(cm.exception, ClientError)
# original Boto message included:
self.assertIn(
"ClientError: An error occurred (403) when calling"
" the HeadObject operation: Forbidden",
str(cm.exception),
)
def test_incorrect_tracking_event(self):
"""The inbound webhook should warn if it receives tracking events"""
@@ -303,7 +392,8 @@ class AmazonSESInboundTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
with self.assertRaisesMessage(
AnymailConfigurationError,
"You seem to have set an Amazon SES *sending* event or notification to publish to an SNS Topic "
"that posts to Anymail's *inbound* webhook URL. (SNS TopicArn arn:...:111111111111:SES_Tracking)"
"You seem to have set an Amazon SES *sending* event or notification"
" to publish to an SNS Topic that posts to Anymail's *inbound* webhook URL."
" (SNS TopicArn arn:...:111111111111:SES_Tracking)",
):
self.post_from_sns('/anymail/amazon_ses/inbound/', raw_sns_message)
self.post_from_sns("/anymail/amazon_ses/inbound/", raw_sns_message)

View File

@@ -10,10 +10,15 @@ from anymail.message import AnymailMessage
from .utils import AnymailTestMixin, sample_image_path
ANYMAIL_TEST_AMAZON_SES_ACCESS_KEY_ID = os.getenv("ANYMAIL_TEST_AMAZON_SES_ACCESS_KEY_ID")
ANYMAIL_TEST_AMAZON_SES_SECRET_ACCESS_KEY = os.getenv("ANYMAIL_TEST_AMAZON_SES_SECRET_ACCESS_KEY")
ANYMAIL_TEST_AMAZON_SES_REGION_NAME = os.getenv("ANYMAIL_TEST_AMAZON_SES_REGION_NAME", "us-east-1")
ANYMAIL_TEST_AMAZON_SES_ACCESS_KEY_ID = os.getenv(
"ANYMAIL_TEST_AMAZON_SES_ACCESS_KEY_ID"
)
ANYMAIL_TEST_AMAZON_SES_SECRET_ACCESS_KEY = os.getenv(
"ANYMAIL_TEST_AMAZON_SES_SECRET_ACCESS_KEY"
)
ANYMAIL_TEST_AMAZON_SES_REGION_NAME = os.getenv(
"ANYMAIL_TEST_AMAZON_SES_REGION_NAME", "us-east-1"
)
ANYMAIL_TEST_AMAZON_SES_DOMAIN = os.getenv("ANYMAIL_TEST_AMAZON_SES_DOMAIN")
@@ -21,30 +26,37 @@ ANYMAIL_TEST_AMAZON_SES_DOMAIN = os.getenv("ANYMAIL_TEST_AMAZON_SES_DOMAIN")
ANYMAIL_TEST_AMAZON_SES_ACCESS_KEY_ID
and ANYMAIL_TEST_AMAZON_SES_SECRET_ACCESS_KEY
and ANYMAIL_TEST_AMAZON_SES_DOMAIN,
"Set ANYMAIL_TEST_AMAZON_SES_ACCESS_KEY_ID and ANYMAIL_TEST_AMAZON_SES_SECRET_ACCESS_KEY "
"and ANYMAIL_TEST_AMAZON_SES_DOMAIN environment variables to run Amazon SES integration tests")
"Set ANYMAIL_TEST_AMAZON_SES_ACCESS_KEY_ID and"
" ANYMAIL_TEST_AMAZON_SES_SECRET_ACCESS_KEY and ANYMAIL_TEST_AMAZON_SES_DOMAIN"
" environment variables to run Amazon SES integration tests",
)
@override_settings(
EMAIL_BACKEND="anymail.backends.amazon_ses.EmailBackend",
ANYMAIL={
"AMAZON_SES_CLIENT_PARAMS": {
# This setting provides Anymail-specific AWS credentials to boto3.client(),
# overriding any credentials in the environment or boto config. It's often
# *not* the best approach -- see the Anymail and boto3 docs for other options.
# *not* the best approach. See the Anymail and boto3 docs for other options.
"aws_access_key_id": ANYMAIL_TEST_AMAZON_SES_ACCESS_KEY_ID,
"aws_secret_access_key": ANYMAIL_TEST_AMAZON_SES_SECRET_ACCESS_KEY,
"region_name": ANYMAIL_TEST_AMAZON_SES_REGION_NAME,
# Can supply any other boto3.client params, including botocore.config.Config as dict
# Can supply any other boto3.client params,
# including botocore.config.Config as dict
"config": {"retries": {"max_attempts": 2}},
},
"AMAZON_SES_CONFIGURATION_SET_NAME": "TestConfigurationSet", # actual config set in Anymail test account
})
@tag('amazon_ses', 'live')
# actual config set in Anymail test account:
"AMAZON_SES_CONFIGURATION_SET_NAME": "TestConfigurationSet",
},
)
@tag("amazon_ses", "live")
class AmazonSESBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
"""Amazon SES API integration tests
These tests run against the **live** Amazon SES API, using the environment
variables `ANYMAIL_TEST_AMAZON_SES_ACCESS_KEY_ID` and `ANYMAIL_TEST_AMAZON_SES_SECRET_ACCESS_KEY`
as AWS credentials. If those variables are not set, these tests won't run.
variables `ANYMAIL_TEST_AMAZON_SES_ACCESS_KEY_ID` and
`ANYMAIL_TEST_AMAZON_SES_SECRET_ACCESS_KEY` as AWS credentials.
If those variables are not set, these tests won't run.
(You can also set the environment variable `ANYMAIL_TEST_AMAZON_SES_REGION_NAME`
to test SES using a region other than the default "us-east-1".)
@@ -56,17 +68,24 @@ class AmazonSESBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
def setUp(self):
super().setUp()
self.from_email = 'test@%s' % ANYMAIL_TEST_AMAZON_SES_DOMAIN
self.message = AnymailMessage('Anymail Amazon SES integration test', 'Text content',
self.from_email, ['success@simulator.amazonses.com'])
self.message.attach_alternative('<p>HTML content</p>', "text/html")
self.from_email = "test@%s" % ANYMAIL_TEST_AMAZON_SES_DOMAIN
self.message = AnymailMessage(
"Anymail Amazon SES integration test",
"Text content",
self.from_email,
["success@simulator.amazonses.com"],
)
self.message.attach_alternative("<p>HTML content</p>", "text/html")
# boto3 relies on GC to close connections. Python 3 warns about unclosed ssl.SSLSocket during cleanup.
# We don't care. (It may be a false positive, or it may be a botocore problem, but it's not *our* problem.)
# boto3 relies on GC to close connections. Python 3 warns about unclosed
# ssl.SSLSocket during cleanup. We don't care. (It may be a false positive,
# or it may be a botocore problem, but it's not *our* problem.)
# https://github.com/boto/boto3/issues/454#issuecomment-586033745
# Filter in TestCase.setUp because unittest resets the warning filters for each test.
# https://stackoverflow.com/a/26620811/647002
warnings.filterwarnings("ignore", message=r"unclosed <ssl\.SSLSocket", category=ResourceWarning)
# Filter in TestCase.setUp because unittest resets the warning filters
# for each test. https://stackoverflow.com/a/26620811/647002
warnings.filterwarnings(
"ignore", message=r"unclosed <ssl\.SSLSocket", category=ResourceWarning
)
def test_simple_send(self):
# Example of getting the Amazon SES send status and message id from the message
@@ -74,12 +93,19 @@ class AmazonSESBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
self.assertEqual(sent_count, 1)
anymail_status = self.message.anymail_status
sent_status = anymail_status.recipients['success@simulator.amazonses.com'].status
message_id = anymail_status.recipients['success@simulator.amazonses.com'].message_id
sent_status = anymail_status.recipients[
"success@simulator.amazonses.com"
].status
message_id = anymail_status.recipients[
"success@simulator.amazonses.com"
].message_id
self.assertEqual(sent_status, 'queued') # Amazon SES always queues (or raises an error)
self.assertRegex(message_id, r'[0-9a-f-]+') # Amazon SES message ids are groups of hex chars
self.assertEqual(anymail_status.status, {sent_status}) # set of all recipient statuses
# Amazon SES always queues (or raises an error):
self.assertEqual(sent_status, "queued")
# Amazon SES message ids are groups of hex chars:
self.assertRegex(message_id, r"[0-9a-f-]+")
# set of all recipient statuses:
self.assertEqual(anymail_status.status, {sent_status})
self.assertEqual(anymail_status.message_id, message_id)
def test_all_options(self):
@@ -87,9 +113,18 @@ class AmazonSESBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
subject="Anymail Amazon SES all-options integration test",
body="This is the text body",
from_email=formataddr(("Test From, with comma", self.from_email)),
to=["success+to1@simulator.amazonses.com", "Recipient 2 <success+to2@simulator.amazonses.com>"],
cc=["success+cc1@simulator.amazonses.com", "Copy 2 <success+cc2@simulator.amazonses.com>"],
bcc=["success+bcc1@simulator.amazonses.com", "Blind Copy 2 <success+bcc2@simulator.amazonses.com>"],
to=[
"success+to1@simulator.amazonses.com",
"Recipient 2 <success+to2@simulator.amazonses.com>",
],
cc=[
"success+cc1@simulator.amazonses.com",
"Copy 2 <success+cc2@simulator.amazonses.com>",
],
bcc=[
"success+bcc1@simulator.amazonses.com",
"Blind Copy 2 <success+bcc2@simulator.amazonses.com>",
],
reply_to=["reply1@example.com", "Reply 2 <reply2@example.com>"],
headers={"X-Anymail-Test": "value"},
metadata={"meta1": "simple_string", "meta2": 2},
@@ -101,55 +136,72 @@ class AmazonSESBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
message.attach_alternative(
"<p><b>HTML:</b> with <a href='http://example.com'>link</a>"
"and image: <img src='cid:%s'></div>" % cid,
"text/html")
"text/html",
)
message.attach_alternative(
"Amazon SES SendRawEmail actually supports multiple alternative parts",
"text/x-note-for-email-geeks")
"text/x-note-for-email-geeks",
)
message.send()
self.assertEqual(message.anymail_status.status, {'queued'})
self.assertEqual(message.anymail_status.status, {"queued"})
def test_stored_template(self):
# Using a template created like this:
# boto3.client('ses').create_template(Template={
# "TemplateName": "TestTemplate",
# "SubjectPart": "Your order {{order}} shipped",
# "HtmlPart": "<h1>Dear {{name}}:</h1><p>Your order {{order}} shipped {{ship_date}}.</p>",
# "TextPart": "Dear {{name}}:\r\nYour order {{order}} shipped {{ship_date}}."
# "HtmlPart": "<h1>Dear {{name}}:</h1>"
# "<p>Your order {{order}} shipped {{ship_date}}.</p>",
# "TextPart": "Dear {{name}}:\r\n"
# "Your order {{order}} shipped {{ship_date}}."
# })
message = AnymailMessage(
template_id='TestTemplate',
template_id="TestTemplate",
from_email=formataddr(("Test From", self.from_email)),
to=["First Recipient <success+to1@simulator.amazonses.com>",
"success+to2@simulator.amazonses.com"],
to=[
"First Recipient <success+to1@simulator.amazonses.com>",
"success+to2@simulator.amazonses.com",
],
merge_data={
'success+to1@simulator.amazonses.com': {'order': 12345, 'name': "Test Recipient"},
'success+to2@simulator.amazonses.com': {'order': 6789},
},
merge_global_data={
'name': "Customer", # default
'ship_date': "today"
"success+to1@simulator.amazonses.com": {
"order": 12345,
"name": "Test Recipient",
},
"success+to2@simulator.amazonses.com": {"order": 6789},
},
merge_global_data={"name": "Customer", "ship_date": "today"}, # default
)
message.send()
recipient_status = message.anymail_status.recipients
self.assertEqual(recipient_status['success+to1@simulator.amazonses.com'].status, 'queued')
self.assertRegex(recipient_status['success+to1@simulator.amazonses.com'].message_id, r'[0-9a-f-]+')
self.assertEqual(recipient_status['success+to2@simulator.amazonses.com'].status, 'queued')
self.assertRegex(recipient_status['success+to2@simulator.amazonses.com'].message_id, r'[0-9a-f-]+')
self.assertEqual(
recipient_status["success+to1@simulator.amazonses.com"].status, "queued"
)
self.assertRegex(
recipient_status["success+to1@simulator.amazonses.com"].message_id,
r"[0-9a-f-]+",
)
self.assertEqual(
recipient_status["success+to2@simulator.amazonses.com"].status, "queued"
)
self.assertRegex(
recipient_status["success+to2@simulator.amazonses.com"].message_id,
r"[0-9a-f-]+",
)
@override_settings(ANYMAIL={
"AMAZON_SES_CLIENT_PARAMS": {
"aws_access_key_id": "test-invalid-access-key-id",
"aws_secret_access_key": "test-invalid-secret-access-key",
"region_name": ANYMAIL_TEST_AMAZON_SES_REGION_NAME,
@override_settings(
ANYMAIL={
"AMAZON_SES_CLIENT_PARAMS": {
"aws_access_key_id": "test-invalid-access-key-id",
"aws_secret_access_key": "test-invalid-secret-access-key",
"region_name": ANYMAIL_TEST_AMAZON_SES_REGION_NAME,
}
}
})
)
def test_invalid_aws_credentials(self):
# Make sure the exception message includes AWS's response:
with self.assertRaisesMessage(
AnymailAPIError,
"The security token included in the request is invalid"
AnymailAPIError, "The security token included in the request is invalid"
):
self.message.send()

View File

@@ -16,50 +16,65 @@ class AmazonSESWebhookTestsMixin(SimpleTestCase):
def post_from_sns(self, path, raw_sns_message, **kwargs):
return self.client.post(
path,
content_type='text/plain; charset=UTF-8', # SNS posts JSON as text/plain
content_type="text/plain; charset=UTF-8", # SNS posts JSON as text/plain
data=json.dumps(raw_sns_message),
HTTP_X_AMZ_SNS_MESSAGE_ID=raw_sns_message["MessageId"],
HTTP_X_AMZ_SNS_MESSAGE_TYPE=raw_sns_message["Type"],
# Anymail doesn't use other x-amz-sns-* headers
**kwargs)
**kwargs,
)
@tag('amazon_ses')
class AmazonSESWebhookSecurityTests(AmazonSESWebhookTestsMixin, WebhookBasicAuthTestCase):
@tag("amazon_ses")
class AmazonSESWebhookSecurityTests(
AmazonSESWebhookTestsMixin, WebhookBasicAuthTestCase
):
def call_webhook(self):
return self.post_from_sns('/anymail/amazon_ses/tracking/',
{"Type": "Notification", "MessageId": "123", "Message": "{}"})
return self.post_from_sns(
"/anymail/amazon_ses/tracking/",
{"Type": "Notification", "MessageId": "123", "Message": "{}"},
)
# Most actual tests are in WebhookBasicAuthTestCase
def test_verifies_missing_auth(self):
# Must handle missing auth header slightly differently from Anymail default 400 SuspiciousOperation:
# SNS will only send basic auth after missing auth responds 401 WWW-Authenticate: Basic realm="..."
# Must handle missing auth header slightly differently from Anymail default 400
# SuspiciousOperation: SNS will only send basic auth after missing auth responds
# 401 WWW-Authenticate: Basic realm="..."
self.clear_basic_auth()
response = self.call_webhook()
self.assertEqual(response.status_code, 401)
self.assertEqual(response["WWW-Authenticate"], 'Basic realm="Anymail WEBHOOK_SECRET"')
self.assertEqual(
response["WWW-Authenticate"], 'Basic realm="Anymail WEBHOOK_SECRET"'
)
@tag('amazon_ses')
@tag("amazon_ses")
class AmazonSESNotificationsTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
def test_bounce_event(self):
# This test includes a complete Amazon SES example event. (Later tests omit some payload for brevity.)
# This test includes a complete Amazon SES example event.
# (Later tests omit some payload for brevity.)
# https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-examples.html#notification-examples-bounce
raw_ses_event = {
"notificationType": "Bounce",
"bounce": {
"bounceType": "Permanent",
"reportingMTA": "dns; email.example.com",
"bouncedRecipients": [{
"emailAddress": "jane@example.com",
"status": "5.1.1",
"action": "failed",
"diagnosticCode": "smtp; 550 5.1.1 <jane@example.com>... User unknown",
}],
"bouncedRecipients": [
{
"emailAddress": "jane@example.com",
"status": "5.1.1",
"action": "failed",
"diagnosticCode": "smtp; 550 5.1.1 <jane@example.com>..."
" User unknown",
}
],
"bounceSubType": "General",
"timestamp": "2016-01-27T14:59:44.101Z", # when bounce sent (by receiving ISP)
"feedbackId": "00000138111222aa-44455566-cccc-cccc-cccc-ddddaaaa068a-000000", # unique id for bounce
# when bounce sent (by receiving ISP):
"timestamp": "2016-01-27T14:59:44.101Z",
# unique id for bounce:
"feedbackId": "00000138111222aa-44455566-cccc"
"-cccc-cccc-ddddaaaa068a-000000",
"remoteMtaIp": "127.0.2.0",
},
"mail": {
@@ -68,13 +83,22 @@ class AmazonSESNotificationsTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
"sourceArn": "arn:aws:ses:us-west-2:888888888888:identity/example.com",
"sourceIp": "127.0.3.0",
"sendingAccountId": "123456789012",
"messageId": "00000138111222aa-33322211-cccc-cccc-cccc-ddddaaaa0680-000000",
"destination": ["jane@example.com", "mary@example.com", "richard@example.com"],
"messageId": "00000138111222aa-33322211-cccc"
"-cccc-cccc-ddddaaaa0680-000000",
"destination": [
"jane@example.com",
"mary@example.com",
"richard@example.com",
],
"headersTruncated": False,
"headers": [
{"name": "From", "value": '"John Doe" <john@example.com>'},
{"name": "To", "value": '"Jane Doe" <jane@example.com>, "Mary Doe" <mary@example.com>,'
' "Richard Doe" <richard@example.com>'},
{
"name": "To",
"value": '"Jane Doe" <jane@example.com>,'
' "Mary Doe" <mary@example.com>,'
' "Richard Doe" <richard@example.com>',
},
{"name": "Message-ID", "value": "custom-message-ID"},
{"name": "Subject", "value": "Hello"},
{"name": "Content-Type", "value": 'text/plain; charset="UTF-8"'},
@@ -87,8 +111,10 @@ class AmazonSESNotificationsTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
"commonHeaders": {
"from": ["John Doe <john@example.com>"],
"date": "Wed, 27 Jan 2016 14:05:45 +0000",
"to": ["Jane Doe <jane@example.com>, Mary Doe <mary@example.com>,"
" Richard Doe <richard@example.com>"],
"to": [
"Jane Doe <jane@example.com>, Mary Doe <mary@example.com>,"
" Richard Doe <richard@example.com>"
],
"messageId": "custom-message-ID",
"subject": "Hello",
},
@@ -96,35 +122,48 @@ class AmazonSESNotificationsTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
}
raw_sns_message = {
"Type": "Notification",
"MessageId": "19ba9823-d7f2-53c1-860e-cb10e0d13dfc", # unique id for SNS event
# unique id for SNS event:
"MessageId": "19ba9823-d7f2-53c1-860e-cb10e0d13dfc",
"TopicArn": "arn:aws:sns:us-east-1:1234567890:SES_Events",
"Subject": "Amazon SES Email Event Notification",
"Message": json.dumps(raw_ses_event) + "\n",
"Timestamp": "2018-03-26T17:58:59.675Z",
"SignatureVersion": "1",
"Signature": "EXAMPLE-SIGNATURE==",
"SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-12345abcde.pem",
"UnsubscribeURL": "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn...",
"SigningCertURL": "https://sns.us-east-1.amazonaws.com"
"/SimpleNotificationService-12345abcde.pem",
"UnsubscribeURL": "https://sns.us-east-1.amazonaws.com"
"/?Action=Unsubscribe&SubscriptionArn=arn...",
}
response = self.post_from_sns('/anymail/amazon_ses/tracking/', raw_sns_message)
response = self.post_from_sns("/anymail/amazon_ses/tracking/", raw_sns_message)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=AmazonSESTrackingWebhookView,
event=ANY, esp_name='Amazon SES')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=AmazonSESTrackingWebhookView,
event=ANY,
esp_name="Amazon SES",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "bounced")
self.assertEqual(event.esp_event, raw_ses_event)
# timestamp from SNS:
self.assertEqual(
event.timestamp,
datetime(2018, 3, 26, 17, 58, 59, microsecond=675000, tzinfo=timezone.utc)
) # SNS
self.assertEqual(event.message_id, "00000138111222aa-33322211-cccc-cccc-cccc-ddddaaaa0680-000000")
datetime(2018, 3, 26, 17, 58, 59, microsecond=675000, tzinfo=timezone.utc),
)
self.assertEqual(
event.message_id,
"00000138111222aa-33322211-cccc-cccc-cccc-ddddaaaa0680-000000",
)
self.assertEqual(event.event_id, "19ba9823-d7f2-53c1-860e-cb10e0d13dfc")
self.assertEqual(event.recipient, "jane@example.com")
self.assertEqual(event.reject_reason, "bounced")
self.assertEqual(event.description, "Permanent: General")
self.assertEqual(event.mta_response, "smtp; 550 5.1.1 <jane@example.com>... User unknown")
self.assertEqual(
event.mta_response, "smtp; 550 5.1.1 <jane@example.com>... User unknown"
)
self.assertEqual(event.tags, ["tag 1", "tag 2"])
self.assertEqual(event.metadata, {"meta1": "string", "meta2": 2})
@@ -139,13 +178,18 @@ class AmazonSESNotificationsTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
"bounceSubType": "General",
"bouncedRecipients": [
{"emailAddress": "jane@example.com"},
{"emailAddress": "richard@example.com"}
{"emailAddress": "richard@example.com"},
],
},
"mail": {
"messageId": "00000137860315fd-34208509-5b74-41f3-95c5-22c1edc3c924-000000",
"destination": ["jane@example.com", "mary@example.com", "richard@example.com"],
}
"messageId": "00000137860315fd-34208509-5b74"
"-41f3-95c5-22c1edc3c924-000000",
"destination": [
"jane@example.com",
"mary@example.com",
"richard@example.com",
],
},
}
raw_sns_message = {
"Type": "Notification",
@@ -153,7 +197,7 @@ class AmazonSESNotificationsTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
"Message": json.dumps(raw_ses_event) + "\n",
"Timestamp": "2018-03-26T17:58:59.675Z",
}
response = self.post_from_sns('/anymail/amazon_ses/tracking/', raw_sns_message)
response = self.post_from_sns("/anymail/amazon_ses/tracking/", raw_sns_message)
self.assertEqual(response.status_code, 200)
# tracking handler should be called twice -- once for each bounced recipient
@@ -161,14 +205,14 @@ class AmazonSESNotificationsTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
self.assertEqual(self.tracking_handler.call_count, 2)
_, kwargs = self.tracking_handler.call_args_list[0]
event = kwargs['event']
event = kwargs["event"]
self.assertEqual(event.event_type, "bounced")
self.assertEqual(event.recipient, "jane@example.com")
self.assertEqual(event.description, "Permanent: General")
self.assertIsNone(event.mta_response)
_, kwargs = self.tracking_handler.call_args_list[1]
event = kwargs['event']
event = kwargs["event"]
self.assertEqual(event.esp_event, raw_ses_event)
self.assertEqual(event.recipient, "richard@example.com")
@@ -181,9 +225,14 @@ class AmazonSESNotificationsTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
"complaintFeedbackType": "abuse",
},
"mail": {
"messageId": "000001378603177f-7a5433e7-8edb-42ae-af10-f0181f34d6ee-000000",
"destination": ["jane@example.com", "mary@example.com", "richard@example.com"],
}
"messageId": "000001378603177f-7a5433e7-8edb"
"-42ae-af10-f0181f34d6ee-000000",
"destination": [
"jane@example.com",
"mary@example.com",
"richard@example.com",
],
},
}
raw_sns_message = {
"Type": "Notification",
@@ -191,11 +240,15 @@ class AmazonSESNotificationsTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
"Message": json.dumps(raw_ses_event) + "\n",
"Timestamp": "2018-03-26T17:58:59.675Z",
}
response = self.post_from_sns('/anymail/amazon_ses/tracking/', raw_sns_message)
response = self.post_from_sns("/anymail/amazon_ses/tracking/", raw_sns_message)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=AmazonSESTrackingWebhookView,
event=ANY, esp_name='Amazon SES')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=AmazonSESTrackingWebhookView,
event=ANY,
esp_name="Amazon SES",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "complained")
self.assertEqual(event.recipient, "richard@example.com")
self.assertEqual(event.reject_reason, "spam")
@@ -207,8 +260,13 @@ class AmazonSESNotificationsTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
"notificationType": "Delivery",
"mail": {
"timestamp": "2016-01-27T14:59:38.237Z",
"messageId": "0000014644fe5ef6-9a483358-9170-4cb4-a269-f5dcdf415321-000000",
"destination": ["jane@example.com", "mary@example.com", "richard@example.com"],
"messageId": "0000014644fe5ef6-9a483358-9170"
"-4cb4-a269-f5dcdf415321-000000",
"destination": [
"jane@example.com",
"mary@example.com",
"richard@example.com",
],
},
"delivery": {
"timestamp": "2016-01-27T14:59:38.237Z",
@@ -216,8 +274,8 @@ class AmazonSESNotificationsTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
"processingTimeMillis": 546,
"reportingMTA": "a8-70.smtp-out.amazonses.com",
"smtpResponse": "250 ok: Message 64111812 accepted",
"remoteMtaIp": "127.0.2.0"
}
"remoteMtaIp": "127.0.2.0",
},
}
raw_sns_message = {
"Type": "Notification",
@@ -225,11 +283,15 @@ class AmazonSESNotificationsTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
"Message": json.dumps(raw_ses_event) + "\n",
"Timestamp": "2018-03-26T17:58:59.675Z",
}
response = self.post_from_sns('/anymail/amazon_ses/tracking/', raw_sns_message)
response = self.post_from_sns("/anymail/amazon_ses/tracking/", raw_sns_message)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=AmazonSESTrackingWebhookView,
event=ANY, esp_name='Amazon SES')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=AmazonSESTrackingWebhookView,
event=ANY,
esp_name="Amazon SES",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "delivered")
self.assertEqual(event.recipient, "jane@example.com")
self.assertEqual(event.mta_response, "250 ok: Message 64111812 accepted")
@@ -247,10 +309,10 @@ class AmazonSESNotificationsTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
"ses:from-domain": ["example.com"],
"ses:caller-identity": ["ses_user"],
"myCustomTag1": ["myCustomTagValue1"],
"myCustomTag2": ["myCustomTagValue2"]
}
"myCustomTag2": ["myCustomTagValue2"],
},
},
"send": {}
"send": {},
}
raw_sns_message = {
"Type": "Notification",
@@ -258,22 +320,30 @@ class AmazonSESNotificationsTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
"Message": json.dumps(raw_ses_event) + "\n",
"Timestamp": "2018-03-26T17:58:59.675Z",
}
response = self.post_from_sns('/anymail/amazon_ses/tracking/', raw_sns_message)
response = self.post_from_sns("/anymail/amazon_ses/tracking/", raw_sns_message)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=AmazonSESTrackingWebhookView,
event=ANY, esp_name='Amazon SES')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=AmazonSESTrackingWebhookView,
event=ANY,
esp_name="Amazon SES",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "sent")
self.assertEqual(event.esp_event, raw_ses_event)
# timestamp from SNS:
self.assertEqual(
event.timestamp,
datetime(2018, 3, 26, 17, 58, 59, microsecond=675000, tzinfo=timezone.utc)
) # SNS
self.assertEqual(event.message_id, "7c191be45-e9aedb9a-02f9-4d12-a87d-dd0099a07f8a-000000")
datetime(2018, 3, 26, 17, 58, 59, microsecond=675000, tzinfo=timezone.utc),
)
self.assertEqual(
event.message_id, "7c191be45-e9aedb9a-02f9-4d12-a87d-dd0099a07f8a-000000"
)
self.assertEqual(event.event_id, "19ba9823-d7f2-53c1-860e-cb10e0d13dfc")
self.assertEqual(event.recipient, "recipient@example.com")
self.assertEqual(event.tags, []) # Anymail doesn't load Amazon SES "Message Tags"
# Anymail doesn't load Amazon SES "Message Tags":
self.assertEqual(event.tags, [])
self.assertEqual(event.metadata, {})
def test_reject_event(self):
@@ -284,9 +354,7 @@ class AmazonSESNotificationsTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
"messageId": "7c191be45-e9aedb9a-02f9-4d12-a87d-dd0099a07f8a-000000",
"destination": ["recipient@example.com"],
},
"reject": {
"reason": "Bad content"
}
"reject": {"reason": "Bad content"},
}
raw_sns_message = {
"Type": "Notification",
@@ -294,11 +362,15 @@ class AmazonSESNotificationsTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
"Message": json.dumps(raw_ses_event) + "\n",
"Timestamp": "2018-03-26T17:58:59.675Z",
}
response = self.post_from_sns('/anymail/amazon_ses/tracking/', raw_sns_message)
response = self.post_from_sns("/anymail/amazon_ses/tracking/", raw_sns_message)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=AmazonSESTrackingWebhookView,
event=ANY, esp_name='Amazon SES')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=AmazonSESTrackingWebhookView,
event=ANY,
esp_name="Amazon SES",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "rejected")
self.assertEqual(event.reject_reason, "blocked")
self.assertEqual(event.description, "Bad content")
@@ -314,8 +386,9 @@ class AmazonSESNotificationsTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
"open": {
"ipAddress": "192.0.2.1",
"timestamp": "2017-08-09T22:00:19.652Z",
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_3 like Mac OS X)..."
}
"userAgent": "Mozilla/5.0"
" (iPhone; CPU iPhone OS 10_3_3 like Mac OS X)...",
},
}
raw_sns_message = {
"Type": "Notification",
@@ -323,14 +396,21 @@ class AmazonSESNotificationsTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
"Message": json.dumps(raw_ses_event) + "\n",
"Timestamp": "2018-03-26T17:58:59.675Z",
}
response = self.post_from_sns('/anymail/amazon_ses/tracking/', raw_sns_message)
response = self.post_from_sns("/anymail/amazon_ses/tracking/", raw_sns_message)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=AmazonSESTrackingWebhookView,
event=ANY, esp_name='Amazon SES')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=AmazonSESTrackingWebhookView,
event=ANY,
esp_name="Amazon SES",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "opened")
self.assertEqual(event.recipient, "recipient@example.com")
self.assertEqual(event.user_agent, "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_3 like Mac OS X)...")
self.assertEqual(
event.user_agent,
"Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_3 like Mac OS X)...",
)
def test_click_event(self):
raw_ses_event = {
@@ -343,12 +423,13 @@ class AmazonSESNotificationsTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
"samplekey1": ["samplevalue1"],
},
"timestamp": "2017-08-09T23:51:25.570Z",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36..."
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
" AppleWebKit/537.36...",
},
"mail": {
"destination": ["recipient@example.com"],
"messageId": "7c191be45-e9aedb9a-02f9-4d12-a87d-dd0099a07f8a-000000",
}
},
}
raw_sns_message = {
"Type": "Notification",
@@ -356,15 +437,24 @@ class AmazonSESNotificationsTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
"Message": json.dumps(raw_ses_event) + "\n",
"Timestamp": "2018-03-26T17:58:59.675Z",
}
response = self.post_from_sns('/anymail/amazon_ses/tracking/', raw_sns_message)
response = self.post_from_sns("/anymail/amazon_ses/tracking/", raw_sns_message)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=AmazonSESTrackingWebhookView,
event=ANY, esp_name='Amazon SES')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=AmazonSESTrackingWebhookView,
event=ANY,
esp_name="Amazon SES",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "clicked")
self.assertEqual(event.recipient, "recipient@example.com")
self.assertEqual(event.user_agent, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...")
self.assertEqual(event.click_url, "https://docs.aws.amazon.com/ses/latest/DeveloperGuide/")
self.assertEqual(
event.user_agent,
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...",
)
self.assertEqual(
event.click_url, "https://docs.aws.amazon.com/ses/latest/DeveloperGuide/"
)
def test_rendering_failure_event(self):
raw_ses_event = {
@@ -374,9 +464,10 @@ class AmazonSESNotificationsTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
"destination": ["recipient@example.com"],
},
"failure": {
"errorMessage": "Attribute 'attributeName' is not present in the rendering data.",
"templateName": "MyTemplate"
}
"errorMessage": "Attribute 'attributeName' is not present"
" in the rendering data.",
"templateName": "MyTemplate",
},
}
raw_sns_message = {
"Type": "Notification",
@@ -384,14 +475,21 @@ class AmazonSESNotificationsTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
"Message": json.dumps(raw_ses_event) + "\n",
"Timestamp": "2018-03-26T17:58:59.675Z",
}
response = self.post_from_sns('/anymail/amazon_ses/tracking/', raw_sns_message)
response = self.post_from_sns("/anymail/amazon_ses/tracking/", raw_sns_message)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=AmazonSESTrackingWebhookView,
event=ANY, esp_name='Amazon SES')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=AmazonSESTrackingWebhookView,
event=ANY,
esp_name="Amazon SES",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "failed")
self.assertEqual(event.recipient, "recipient@example.com")
self.assertEqual(event.description, "Attribute 'attributeName' is not present in the rendering data.")
self.assertEqual(
event.description,
"Attribute 'attributeName' is not present in the rendering data.",
)
def test_incorrect_received_event(self):
"""The tracking webhook should warn if it receives inbound events"""
@@ -403,13 +501,14 @@ class AmazonSESNotificationsTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
}
with self.assertRaisesMessage(
AnymailConfigurationError,
"You seem to have set an Amazon SES *inbound* receipt rule to publish to an SNS Topic that posts "
"to Anymail's *tracking* webhook URL. (SNS TopicArn arn:aws:sns:us-east-1:111111111111:SES_Inbound)"
"You seem to have set an Amazon SES *inbound* receipt rule to publish to an"
" SNS Topic that posts to Anymail's *tracking* webhook URL. (SNS TopicArn"
" arn:aws:sns:us-east-1:111111111111:SES_Inbound)",
):
self.post_from_sns('/anymail/amazon_ses/tracking/', raw_sns_message)
self.post_from_sns("/anymail/amazon_ses/tracking/", raw_sns_message)
@tag('amazon_ses')
@tag("amazon_ses")
class AmazonSESSubscriptionManagementTests(WebhookTestCase, AmazonSESWebhookTestsMixin):
# Anymail will automatically respond to SNS subscription notifications
# if Anymail is configured to require basic auth via WEBHOOK_SECRET.
@@ -417,15 +516,22 @@ class AmazonSESSubscriptionManagementTests(WebhookTestCase, AmazonSESWebhookTest
def setUp(self):
super().setUp()
# Mock boto3.session.Session().client('sns').confirm_subscription (and any other client operations)
# (We could also use botocore.stub.Stubber, but mock works well with our test structure)
self.patch_boto3_session = patch('anymail.webhooks.amazon_ses.boto3.session.Session', autospec=True)
self.mock_session = self.patch_boto3_session.start() # boto3.session.Session
# Mock boto3.session.Session().client('sns').confirm_subscription (and any
# other client operations). (We could also use botocore.stub.Stubber, but mock
# works well with our test structure.)
self.patch_boto3_session = patch(
"anymail.webhooks.amazon_ses.boto3.session.Session", autospec=True
)
#: boto3.session.Session
self.mock_session = self.patch_boto3_session.start()
self.addCleanup(self.patch_boto3_session.stop)
self.mock_client = self.mock_session.return_value.client # boto3.session.Session().client
self.mock_client_instance = self.mock_client.return_value # boto3.session.Session().client('sns', ...)
#: boto3.session.Session().client
self.mock_client = self.mock_session.return_value.client
#: boto3.session.Session().client('sns', ...)
self.mock_client_instance = self.mock_client.return_value
self.mock_client_instance.confirm_subscription.return_value = {
'SubscriptionArn': 'arn:aws:sns:us-west-2:123456789012:SES_Notifications:aaaaaaa-...'
"SubscriptionArn": "arn:aws:sns:us-west-2:123456789012:SES_Notifications"
":aaaaaaa-..."
}
SNS_SUBSCRIPTION_CONFIRMATION = {
@@ -433,23 +539,32 @@ class AmazonSESSubscriptionManagementTests(WebhookTestCase, AmazonSESWebhookTest
"MessageId": "165545c9-2a5c-472c-8df2-7ff2be2b3b1b",
"Token": "EXAMPLE_TOKEN",
"TopicArn": "arn:aws:sns:us-west-2:123456789012:SES_Notifications",
"Message": "You have chosen to subscribe ...\nTo confirm..., visit the SubscribeURL included in this message.",
"SubscribeURL": "https://sns.us-west-2.amazonaws.com/?Action=ConfirmSubscription&TopicArn=...",
"Message": "You have chosen to subscribe ...\n"
"To confirm..., visit the SubscribeURL included in this message.",
"SubscribeURL": "https://sns.us-west-2.amazonaws.com/"
"?Action=ConfirmSubscription&TopicArn=...",
"Timestamp": "2012-04-26T20:45:04.751Z",
"SignatureVersion": "1",
"Signature": "EXAMPLE-SIGNATURE==",
"SigningCertURL": "https://sns.us-west-2.amazonaws.com/SimpleNotificationService-12345abcde.pem"
"SigningCertURL": "https://sns.us-west-2.amazonaws.com/"
"SimpleNotificationService-12345abcde.pem",
}
def test_sns_subscription_auto_confirmation(self):
"""Anymail webhook will auto-confirm SNS topic subscriptions"""
response = self.post_from_sns('/anymail/amazon_ses/tracking/', self.SNS_SUBSCRIPTION_CONFIRMATION)
response = self.post_from_sns(
"/anymail/amazon_ses/tracking/", self.SNS_SUBSCRIPTION_CONFIRMATION
)
self.assertEqual(response.status_code, 200)
# auto-confirmed:
self.mock_client.assert_called_once_with('sns', config=ANY, region_name="us-west-2")
self.mock_client.assert_called_once_with(
"sns", config=ANY, region_name="us-west-2"
)
self.mock_client_instance.confirm_subscription.assert_called_once_with(
TopicArn="arn:aws:sns:us-west-2:123456789012:SES_Notifications",
Token="EXAMPLE_TOKEN", AuthenticateOnUnsubscribe="true")
Token="EXAMPLE_TOKEN",
AuthenticateOnUnsubscribe="true",
)
# didn't notify receivers:
self.assertEqual(self.tracking_handler.call_count, 0)
self.assertEqual(self.inbound_handler.call_count, 0)
@@ -457,43 +572,68 @@ class AmazonSESSubscriptionManagementTests(WebhookTestCase, AmazonSESWebhookTest
def test_sns_subscription_confirmation_failure(self):
"""Auto-confirmation allows error through if confirm call fails"""
from botocore.exceptions import ClientError
self.mock_client_instance.confirm_subscription.side_effect = ClientError({
'Error': {
'Type': 'Sender',
'Code': 'InternalError',
'Message': 'Gremlins!',
self.mock_client_instance.confirm_subscription.side_effect = ClientError(
{
"Error": {
"Type": "Sender",
"Code": "InternalError",
"Message": "Gremlins!",
},
"ResponseMetadata": {
"RequestId": "aaaaaaaa-2222-1111-8888-bbbb3333bbbb",
"HTTPStatusCode": 500,
},
},
'ResponseMetadata': {
'RequestId': 'aaaaaaaa-2222-1111-8888-bbbb3333bbbb',
'HTTPStatusCode': 500,
}
}, operation_name="confirm_subscription")
operation_name="confirm_subscription",
)
with self.assertRaisesMessage(ClientError, "Gremlins!"):
self.post_from_sns('/anymail/amazon_ses/tracking/', self.SNS_SUBSCRIPTION_CONFIRMATION)
self.post_from_sns(
"/anymail/amazon_ses/tracking/", self.SNS_SUBSCRIPTION_CONFIRMATION
)
# didn't notify receivers:
self.assertEqual(self.tracking_handler.call_count, 0)
self.assertEqual(self.inbound_handler.call_count, 0)
@override_settings(ANYMAIL_AMAZON_SES_CLIENT_PARAMS={"region_name": "us-east-1"})
def test_sns_subscription_confirmation_different_region(self):
"""Anymail confirms the subscription in the SNS Topic's own region, rather than any default region"""
"""
Anymail confirms the subscription in the SNS Topic's own region,
rather than any default region
"""
# (The SNS_SUBSCRIPTION_CONFIRMATION above has a TopicArn in region us-west-2)
self.post_from_sns('/anymail/amazon_ses/tracking/', self.SNS_SUBSCRIPTION_CONFIRMATION)
self.mock_client.assert_called_once_with('sns', config=ANY, region_name="us-west-2")
self.post_from_sns(
"/anymail/amazon_ses/tracking/", self.SNS_SUBSCRIPTION_CONFIRMATION
)
self.mock_client.assert_called_once_with(
"sns", config=ANY, region_name="us-west-2"
)
@override_settings(ANYMAIL={}) # clear WEBHOOK_SECRET setting from base WebhookTestCase
# clear WEBHOOK_SECRET setting from base WebhookTestCase
@override_settings(ANYMAIL={})
def test_sns_subscription_confirmation_auth_disabled(self):
"""Anymail *won't* auto-confirm SNS subscriptions if WEBHOOK_SECRET isn't in use"""
warnings.simplefilter("ignore", AnymailInsecureWebhookWarning) # (this gets tested elsewhere)
with self.assertLogs('django.security.AnymailWebhookValidationFailure') as cm:
response = self.post_from_sns('/anymail/amazon_ses/tracking/', self.SNS_SUBSCRIPTION_CONFIRMATION)
"""
Anymail *won't* auto-confirm SNS subscriptions if WEBHOOK_SECRET isn't in use
"""
# (warning gets tested elsewhere)
warnings.simplefilter("ignore", AnymailInsecureWebhookWarning)
with self.assertLogs("django.security.AnymailWebhookValidationFailure") as cm:
response = self.post_from_sns(
"/anymail/amazon_ses/tracking/", self.SNS_SUBSCRIPTION_CONFIRMATION
)
self.assertEqual(response.status_code, 400) # bad request
self.assertEqual(
["Anymail received an unexpected SubscriptionConfirmation request for Amazon SNS topic "
"'arn:aws:sns:us-west-2:123456789012:SES_Notifications'. (Anymail can automatically confirm "
"SNS subscriptions if you set a WEBHOOK_SECRET and use that in your SNS notification url. Or "
"you can manually confirm this subscription in the SNS dashboard with token 'EXAMPLE_TOKEN'.)"],
[record.getMessage() for record in cm.records])
[
"Anymail received an unexpected SubscriptionConfirmation request for"
" Amazon SNS topic"
" 'arn:aws:sns:us-west-2:123456789012:SES_Notifications'."
" (Anymail can automatically confirm SNS subscriptions if you set"
" a WEBHOOK_SECRET and use that in your SNS notification url."
" Or you can manually confirm this subscription in the SNS dashboard"
" with token 'EXAMPLE_TOKEN'.)"
],
[record.getMessage() for record in cm.records],
)
# *didn't* try to confirm the subscription:
self.assertEqual(self.mock_client_instance.confirm_subscription.call_count, 0)
# didn't notify receivers:
@@ -501,37 +641,55 @@ class AmazonSESSubscriptionManagementTests(WebhookTestCase, AmazonSESWebhookTest
self.assertEqual(self.inbound_handler.call_count, 0)
def test_sns_confirmation_success_notification(self):
"""Anymail ignores the 'Successfully validated' notification after confirming an SNS subscription"""
response = self.post_from_sns('/anymail/amazon_ses/tracking/', {
"Type": "Notification",
"MessageId": "7fbca0d9-eeab-5285-ae27-f3f57f2e84b0",
"TopicArn": "arn:aws:sns:us-west-2:123456789012:SES_Notifications",
"Message": "Successfully validated SNS topic for Amazon SES event publishing.",
"Timestamp": "2018-03-21T16:58:45.077Z",
"SignatureVersion": "1",
"Signature": "EXAMPLE_SIGNATURE==",
"SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-12345abcde.pem",
"UnsubscribeURL": "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe...",
})
"""
Anymail ignores the 'Successfully validated' notification
after confirming an SNS subscription
"""
response = self.post_from_sns(
"/anymail/amazon_ses/tracking/",
{
"Type": "Notification",
"MessageId": "7fbca0d9-eeab-5285-ae27-f3f57f2e84b0",
"TopicArn": "arn:aws:sns:us-west-2:123456789012:SES_Notifications",
"Message": "Successfully validated SNS topic"
" for Amazon SES event publishing.",
"Timestamp": "2018-03-21T16:58:45.077Z",
"SignatureVersion": "1",
"Signature": "EXAMPLE_SIGNATURE==",
"SigningCertURL": "https://sns.us-east-1.amazonaws.com"
"/SimpleNotificationService-12345abcde.pem",
"UnsubscribeURL": "https://sns.us-east-1.amazonaws.com"
"/?Action=Unsubscribe...",
},
)
self.assertEqual(response.status_code, 200)
# didn't notify receivers:
self.assertEqual(self.tracking_handler.call_count, 0)
self.assertEqual(self.inbound_handler.call_count, 0)
def test_sns_unsubscribe_confirmation(self):
"""Anymail ignores the UnsubscribeConfirmation SNS message after deleting a subscription"""
response = self.post_from_sns('/anymail/amazon_ses/tracking/', {
"Type": "UnsubscribeConfirmation",
"MessageId": "47138184-6831-46b8-8f7c-afc488602d7d",
"Token": "EXAMPLE_TOKEN",
"TopicArn": "arn:aws:sns:us-west-2:123456789012:SES_Notifications",
"Message": "You have chosen to deactivate subscription ...\nTo cancel ... visit the SubscribeURL...",
"SubscribeURL": "https://sns.us-west-2.amazonaws.com/?Action=ConfirmSubscription&TopicArn=...",
"Timestamp": "2012-04-26T20:06:41.581Z",
"SignatureVersion": "1",
"Signature": "EXAMPLE_SIGNATURE==",
"SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-12345abcde.pem",
})
"""
Anymail ignores the UnsubscribeConfirmation SNS message
after deleting a subscription
"""
response = self.post_from_sns(
"/anymail/amazon_ses/tracking/",
{
"Type": "UnsubscribeConfirmation",
"MessageId": "47138184-6831-46b8-8f7c-afc488602d7d",
"Token": "EXAMPLE_TOKEN",
"TopicArn": "arn:aws:sns:us-west-2:123456789012:SES_Notifications",
"Message": "You have chosen to deactivate subscription ...\n"
"To cancel ... visit the SubscribeURL...",
"SubscribeURL": "https://sns.us-west-2.amazonaws.com"
"/?Action=ConfirmSubscription&TopicArn=...",
"Timestamp": "2012-04-26T20:06:41.581Z",
"SignatureVersion": "1",
"Signature": "EXAMPLE_SIGNATURE==",
"SigningCertURL": "https://sns.us-east-1.amazonaws.com"
"/SimpleNotificationService-12345abcde.pem",
},
)
self.assertEqual(response.status_code, 200)
# *didn't* try to use the Token to re-enable the subscription:
self.assertEqual(self.mock_client_instance.confirm_subscription.call_count, 0)
@@ -541,8 +699,13 @@ class AmazonSESSubscriptionManagementTests(WebhookTestCase, AmazonSESWebhookTest
@override_settings(ANYMAIL_AMAZON_SES_AUTO_CONFIRM_SNS_SUBSCRIPTIONS=False)
def test_disable_auto_confirmation(self):
"""The ANYMAIL setting AMAZON_SES_AUTO_CONFIRM_SNS_SUBSCRIPTIONS will disable this feature"""
response = self.post_from_sns('/anymail/amazon_ses/tracking/', self.SNS_SUBSCRIPTION_CONFIRMATION)
"""
The ANYMAIL setting AMAZON_SES_AUTO_CONFIRM_SNS_SUBSCRIPTIONS
will disable this feature
"""
response = self.post_from_sns(
"/anymail/amazon_ses/tracking/", self.SNS_SUBSCRIPTION_CONFIRMATION
)
self.assertEqual(response.status_code, 200)
# *didn't* try to subscribe:
self.assertEqual(self.mock_session.call_count, 0)

View File

@@ -1,4 +1,4 @@
from django.test import override_settings, SimpleTestCase, tag
from django.test import SimpleTestCase, override_settings, tag
from anymail.backends.base_requests import AnymailRequestsBackend, RequestsPayload
from anymail.message import AnymailMessage, AnymailRecipientStatus
@@ -21,7 +21,7 @@ class MinimalRequestsBackend(AnymailRequestsBackend):
return MinimalRequestsPayload(message, defaults, self, **_payload_init)
def parse_recipient_status(self, response, payload, message):
return {'to@example.com': AnymailRecipientStatus('message-id', 'sent')}
return {"to@example.com": AnymailRecipientStatus("message-id", "sent")}
class MinimalRequestsPayload(RequestsPayload):
@@ -41,13 +41,15 @@ class MinimalRequestsPayload(RequestsPayload):
add_attachment = _noop
@override_settings(EMAIL_BACKEND='tests.test_base_backends.MinimalRequestsBackend')
@override_settings(EMAIL_BACKEND="tests.test_base_backends.MinimalRequestsBackend")
class RequestsBackendBaseTestCase(RequestsBackendMockAPITestCase):
"""Test common functionality in AnymailRequestsBackend"""
def setUp(self):
super().setUp()
self.message = AnymailMessage('Subject', 'Text Body', 'from@example.com', ['to@example.com'])
self.message = AnymailMessage(
"Subject", "Text Body", "from@example.com", ["to@example.com"]
)
def test_minimal_requests_backend(self):
"""Make sure the testing backend defined above actually works"""
@@ -57,23 +59,25 @@ class RequestsBackendBaseTestCase(RequestsBackendMockAPITestCase):
def test_timeout_default(self):
"""All requests have a 30 second default timeout"""
self.message.send()
timeout = self.get_api_call_arg('timeout')
timeout = self.get_api_call_arg("timeout")
self.assertEqual(timeout, 30)
@override_settings(ANYMAIL_REQUESTS_TIMEOUT=5)
def test_timeout_setting(self):
"""You can use the Anymail setting REQUESTS_TIMEOUT to override the default"""
self.message.send()
timeout = self.get_api_call_arg('timeout')
timeout = self.get_api_call_arg("timeout")
self.assertEqual(timeout, 5)
@tag('live')
@override_settings(EMAIL_BACKEND='tests.test_base_backends.MinimalRequestsBackend')
@tag("live")
@override_settings(EMAIL_BACKEND="tests.test_base_backends.MinimalRequestsBackend")
class RequestsBackendLiveTestCase(AnymailTestMixin, SimpleTestCase):
@override_settings(ANYMAIL_DEBUG_API_REQUESTS=True)
def test_debug_logging(self):
message = AnymailMessage('Subject', 'Text Body', 'from@example.com', ['to@example.com'])
message = AnymailMessage(
"Subject", "Text Body", "from@example.com", ["to@example.com"]
)
message._payload_init = dict(
data="Request body",
headers={
@@ -84,8 +88,9 @@ class RequestsBackendLiveTestCase(AnymailTestMixin, SimpleTestCase):
with self.assertPrints("===== Anymail API request") as outbuf:
message.send()
# Header order and response data vary to much to do a full comparison, but make sure
# that the output contains some expected pieces of the request and the response"
# Header order and response data vary too much to do a full comparison,
# but make sure that the output contains some expected pieces of the request
# and the response
output = outbuf.getvalue()
self.assertIn("\nPOST https://httpbin.org/post\n", output)
self.assertIn("\nUser-Agent: django-anymail/", output)
@@ -98,7 +103,9 @@ class RequestsBackendLiveTestCase(AnymailTestMixin, SimpleTestCase):
def test_no_debug_logging(self):
# Make sure it doesn't output anything when DEBUG_API_REQUESTS is not set
message = AnymailMessage('Subject', 'Text Body', 'from@example.com', ['to@example.com'])
message = AnymailMessage(
"Subject", "Text Body", "from@example.com", ["to@example.com"]
)
message._payload_init = dict(
data="Request body",
headers={

View File

@@ -11,20 +11,32 @@ class DeprecatedSettingsTests(AnymailTestMixin, SimpleTestCase):
@override_settings(ANYMAIL={"WEBHOOK_AUTHORIZATION": "abcde:12345"})
def test_webhook_authorization(self):
errors = check_deprecated_settings(None)
self.assertEqual(errors, [checks.Error(
"The ANYMAIL setting 'WEBHOOK_AUTHORIZATION' has been renamed 'WEBHOOK_SECRET' to improve security.",
hint="You must update your settings.py.",
id="anymail.E001",
)])
self.assertEqual(
errors,
[
checks.Error(
"The ANYMAIL setting 'WEBHOOK_AUTHORIZATION' has been renamed"
" 'WEBHOOK_SECRET' to improve security.",
hint="You must update your settings.py.",
id="anymail.E001",
)
],
)
@override_settings(ANYMAIL_WEBHOOK_AUTHORIZATION="abcde:12345", ANYMAIL={})
def test_anymail_webhook_authorization(self):
errors = check_deprecated_settings(None)
self.assertEqual(errors, [checks.Error(
"The ANYMAIL_WEBHOOK_AUTHORIZATION setting has been renamed ANYMAIL_WEBHOOK_SECRET to improve security.",
hint="You must update your settings.py.",
id="anymail.E001",
)])
self.assertEqual(
errors,
[
checks.Error(
"The ANYMAIL_WEBHOOK_AUTHORIZATION setting has been renamed"
" ANYMAIL_WEBHOOK_SECRET to improve security.",
hint="You must update your settings.py.",
id="anymail.E001",
)
],
)
class InsecureSettingsTests(AnymailTestMixin, SimpleTestCase):

View File

@@ -10,7 +10,12 @@ from django.utils.functional import Promise
from django.utils.translation import gettext_lazy
from anymail.backends.test import EmailBackend as TestBackend, TestPayload
from anymail.exceptions import AnymailConfigurationError, AnymailError, AnymailInvalidAddress, AnymailUnsupportedFeature
from anymail.exceptions import (
AnymailConfigurationError,
AnymailError,
AnymailInvalidAddress,
AnymailUnsupportedFeature,
)
from anymail.message import AnymailMessage
from anymail.utils import get_anymail_setting
@@ -19,25 +24,31 @@ from .utils import AnymailTestMixin
class SettingsTestBackend(TestBackend):
"""(useful only for these tests)"""
def __init__(self, *args, **kwargs):
esp_name = self.esp_name
self.sample_setting = get_anymail_setting('sample_setting', esp_name=esp_name,
kwargs=kwargs, allow_bare=True)
self.username = get_anymail_setting('username', esp_name=esp_name, kwargs=kwargs,
default=None, allow_bare=True)
self.password = get_anymail_setting('password', esp_name=esp_name, kwargs=kwargs,
default=None, allow_bare=True)
self.sample_setting = get_anymail_setting(
"sample_setting", esp_name=esp_name, kwargs=kwargs, allow_bare=True
)
self.username = get_anymail_setting(
"username", esp_name=esp_name, kwargs=kwargs, default=None, allow_bare=True
)
self.password = get_anymail_setting(
"password", esp_name=esp_name, kwargs=kwargs, default=None, allow_bare=True
)
super().__init__(*args, **kwargs)
@override_settings(EMAIL_BACKEND='anymail.backends.test.EmailBackend')
@override_settings(EMAIL_BACKEND="anymail.backends.test.EmailBackend")
class TestBackendTestCase(AnymailTestMixin, SimpleTestCase):
"""Base TestCase using Anymail's Test EmailBackend"""
def setUp(self):
super().setUp()
# Simple message useful for many tests
self.message = AnymailMessage('Subject', 'Text Body', 'from@example.com', ['to@example.com'])
self.message = AnymailMessage(
"Subject", "Text Body", "from@example.com", ["to@example.com"]
)
@staticmethod
def get_send_count():
@@ -45,7 +56,9 @@ class TestBackendTestCase(AnymailTestMixin, SimpleTestCase):
try:
return len(mail.outbox)
except AttributeError:
return 0 # mail.outbox not initialized by either Anymail test or Django locmem backend
# mail.outbox not initialized by either Anymail test
# or Django locmem backend
return 0
@staticmethod
def get_send_params():
@@ -53,35 +66,40 @@ class TestBackendTestCase(AnymailTestMixin, SimpleTestCase):
try:
return mail.outbox[-1].anymail_test_params
except IndexError:
raise IndexError("No messages have been sent through the Anymail test backend")
raise IndexError(
"No messages have been sent through the Anymail test backend"
)
except AttributeError:
raise AttributeError("The last message sent was not processed through the Anymail test backend")
raise AttributeError(
"The last message sent was not processed"
" through the Anymail test backend"
)
@override_settings(EMAIL_BACKEND='tests.test_general_backend.SettingsTestBackend')
@override_settings(EMAIL_BACKEND="tests.test_general_backend.SettingsTestBackend")
class BackendSettingsTests(TestBackendTestCase):
"""Test settings initializations for Anymail EmailBackends"""
@override_settings(ANYMAIL={'TEST_SAMPLE_SETTING': 'setting_from_anymail_settings'})
@override_settings(ANYMAIL={"TEST_SAMPLE_SETTING": "setting_from_anymail_settings"})
def test_anymail_setting(self):
"""ESP settings usually come from ANYMAIL settings dict"""
backend = get_connection()
self.assertEqual(backend.sample_setting, 'setting_from_anymail_settings')
self.assertEqual(backend.sample_setting, "setting_from_anymail_settings")
@override_settings(TEST_SAMPLE_SETTING='setting_from_bare_settings')
@override_settings(TEST_SAMPLE_SETTING="setting_from_bare_settings")
def test_bare_setting(self):
"""ESP settings are also usually allowed at root of settings file"""
backend = get_connection()
self.assertEqual(backend.sample_setting, 'setting_from_bare_settings')
self.assertEqual(backend.sample_setting, "setting_from_bare_settings")
@override_settings(ANYMAIL={'TEST_SAMPLE_SETTING': 'setting_from_settings'})
@override_settings(ANYMAIL={"TEST_SAMPLE_SETTING": "setting_from_settings"})
def test_connection_kwargs_overrides_settings(self):
"""Can override settings file in get_connection"""
backend = get_connection()
self.assertEqual(backend.sample_setting, 'setting_from_settings')
self.assertEqual(backend.sample_setting, "setting_from_settings")
backend = get_connection(sample_setting='setting_from_kwargs')
self.assertEqual(backend.sample_setting, 'setting_from_kwargs')
backend = get_connection(sample_setting="setting_from_kwargs")
self.assertEqual(backend.sample_setting, "setting_from_kwargs")
def test_missing_setting(self):
"""Settings without defaults must be provided"""
@@ -89,22 +107,28 @@ class BackendSettingsTests(TestBackendTestCase):
get_connection()
self.assertIsInstance(cm.exception, ImproperlyConfigured) # Django consistency
errmsg = str(cm.exception)
self.assertRegex(errmsg, r'\bTEST_SAMPLE_SETTING\b')
self.assertRegex(errmsg, r'\bANYMAIL_TEST_SAMPLE_SETTING\b')
self.assertRegex(errmsg, r"\bTEST_SAMPLE_SETTING\b")
self.assertRegex(errmsg, r"\bANYMAIL_TEST_SAMPLE_SETTING\b")
@override_settings(ANYMAIL={'TEST_USERNAME': 'username_from_settings',
'TEST_PASSWORD': 'password_from_settings',
'TEST_SAMPLE_SETTING': 'required'})
@override_settings(
ANYMAIL={
"TEST_USERNAME": "username_from_settings",
"TEST_PASSWORD": "password_from_settings",
"TEST_SAMPLE_SETTING": "required",
}
)
def test_username_password_kwargs_overrides(self):
"""Overrides for 'username' and 'password' should work like other overrides"""
# These are special-cased because of default args in Django core mail functions.
backend = get_connection()
self.assertEqual(backend.username, 'username_from_settings')
self.assertEqual(backend.password, 'password_from_settings')
self.assertEqual(backend.username, "username_from_settings")
self.assertEqual(backend.password, "password_from_settings")
backend = get_connection(username='username_from_kwargs', password='password_from_kwargs')
self.assertEqual(backend.username, 'username_from_kwargs')
self.assertEqual(backend.password, 'password_from_kwargs')
backend = get_connection(
username="username_from_kwargs", password="password_from_kwargs"
)
self.assertEqual(backend.username, "username_from_kwargs")
self.assertEqual(backend.password, "password_from_kwargs")
class UnsupportedFeatureTests(TestBackendTestCase):
@@ -113,124 +137,149 @@ class UnsupportedFeatureTests(TestBackendTestCase):
def test_unsupported_feature(self):
"""Unsupported features raise AnymailUnsupportedFeature"""
# Test EmailBackend doesn't support non-HTML alternative parts
self.message.attach_alternative(b'FAKE_MP3_DATA', 'audio/mpeg')
self.message.attach_alternative(b"FAKE_MP3_DATA", "audio/mpeg")
with self.assertRaises(AnymailUnsupportedFeature):
self.message.send()
@override_settings(ANYMAIL={
'IGNORE_UNSUPPORTED_FEATURES': True
})
@override_settings(ANYMAIL={"IGNORE_UNSUPPORTED_FEATURES": True})
def test_ignore_unsupported_features(self):
"""Setting prevents exception"""
self.message.attach_alternative(b'FAKE_MP3_DATA', 'audio/mpeg')
self.message.attach_alternative(b"FAKE_MP3_DATA", "audio/mpeg")
self.message.send() # should not raise exception
class SendDefaultsTests(TestBackendTestCase):
"""Tests backend support for global SEND_DEFAULTS and <ESP>_SEND_DEFAULTS"""
@override_settings(ANYMAIL={
'SEND_DEFAULTS': {
# This isn't an exhaustive list of Anymail message attrs; just one of each type
'metadata': {'global': 'globalvalue'},
'send_at': datetime(2016, 5, 12, 4, 17, 0, tzinfo=timezone.utc),
'tags': ['globaltag'],
'template_id': 'my-template',
'track_clicks': True,
'esp_extra': {'globalextra': 'globalsetting'},
@override_settings(
ANYMAIL={
"SEND_DEFAULTS": {
# This isn't an exhaustive list of Anymail message attrs;
# just one of each type
"metadata": {"global": "globalvalue"},
"send_at": datetime(2016, 5, 12, 4, 17, 0, tzinfo=timezone.utc),
"tags": ["globaltag"],
"template_id": "my-template",
"track_clicks": True,
"esp_extra": {"globalextra": "globalsetting"},
}
}
})
)
def test_send_defaults(self):
"""Test that (non-esp-specific) send defaults are applied"""
self.message.send()
params = self.get_send_params()
# All these values came from ANYMAIL_SEND_DEFAULTS:
self.assertEqual(params['metadata'], {'global': 'globalvalue'})
self.assertEqual(params['send_at'], datetime(2016, 5, 12, 4, 17, 0, tzinfo=timezone.utc))
self.assertEqual(params['tags'], ['globaltag'])
self.assertEqual(params['template_id'], 'my-template')
self.assertEqual(params['track_clicks'], True)
self.assertEqual(params['globalextra'], 'globalsetting') # Test EmailBackend merges esp_extra into params
self.assertEqual(params["metadata"], {"global": "globalvalue"})
self.assertEqual(
params["send_at"], datetime(2016, 5, 12, 4, 17, 0, tzinfo=timezone.utc)
)
self.assertEqual(params["tags"], ["globaltag"])
self.assertEqual(params["template_id"], "my-template")
self.assertEqual(params["track_clicks"], True)
# Test EmailBackend merges esp_extra into params:
self.assertEqual(params["globalextra"], "globalsetting")
@override_settings(ANYMAIL={
'TEST_SEND_DEFAULTS': { # "TEST" is the name of the Test EmailBackend's ESP
'metadata': {'global': 'espvalue'},
'tags': ['esptag'],
'track_opens': False,
'esp_extra': {'globalextra': 'espsetting'},
@override_settings(
ANYMAIL={
# SEND_DEFAULTS for the Test EmailBackend, because
# "TEST" is the name of the Test EmailBackend's ESP
"TEST_SEND_DEFAULTS": {
"metadata": {"global": "espvalue"},
"tags": ["esptag"],
"track_opens": False,
"esp_extra": {"globalextra": "espsetting"},
}
}
})
)
def test_esp_send_defaults(self):
"""Test that esp-specific send defaults are applied"""
self.message.send()
params = self.get_send_params()
self.assertEqual(params['metadata'], {'global': 'espvalue'})
self.assertEqual(params['tags'], ['esptag'])
self.assertEqual(params['track_opens'], False)
self.assertEqual(params['globalextra'], 'espsetting') # Test EmailBackend merges esp_extra into params
self.assertEqual(params["metadata"], {"global": "espvalue"})
self.assertEqual(params["tags"], ["esptag"])
self.assertEqual(params["track_opens"], False)
# Test EmailBackend merges esp_extra into params:
self.assertEqual(params["globalextra"], "espsetting")
@override_settings(ANYMAIL={
'SEND_DEFAULTS': {
'metadata': {'global': 'globalvalue', 'other': 'othervalue'},
'tags': ['globaltag'],
'track_clicks': True,
'track_opens': False,
'esp_extra': {'globalextra': 'globalsetting'},
@override_settings(
ANYMAIL={
"SEND_DEFAULTS": {
"metadata": {"global": "globalvalue", "other": "othervalue"},
"tags": ["globaltag"],
"track_clicks": True,
"track_opens": False,
"esp_extra": {"globalextra": "globalsetting"},
}
}
})
)
def test_send_defaults_combine_with_message(self):
"""Individual message settings are *merged into* the global send defaults"""
self.message.metadata = {'message': 'messagevalue', 'other': 'override'}
self.message.tags = ['messagetag']
self.message.metadata = {"message": "messagevalue", "other": "override"}
self.message.tags = ["messagetag"]
self.message.track_clicks = False
self.message.esp_extra = {'messageextra': 'messagesetting'}
self.message.esp_extra = {"messageextra": "messagesetting"}
self.message.send()
params = self.get_send_params()
self.assertEqual(params['metadata'], { # metadata merged
'global': 'globalvalue', # global default preserved
'message': 'messagevalue', # message setting added
'other': 'override'}) # message setting overrides global default
self.assertEqual(params['tags'], ['globaltag', 'messagetag']) # tags concatenated
self.assertEqual(params['track_clicks'], False) # message overrides
self.assertEqual(params['track_opens'], False) # (no message setting)
self.assertEqual(params['globalextra'], 'globalsetting')
self.assertEqual(params['messageextra'], 'messagesetting')
self.assertEqual(
params["metadata"],
{ # metadata merged
"global": "globalvalue", # global default preserved
"message": "messagevalue", # message setting added
"other": "override", # message setting overrides global default
},
)
# tags concatenated:
self.assertEqual(params["tags"], ["globaltag", "messagetag"])
self.assertEqual(params["track_clicks"], False) # message overrides
self.assertEqual(params["track_opens"], False) # (no message setting)
self.assertEqual(params["globalextra"], "globalsetting")
self.assertEqual(params["messageextra"], "messagesetting")
# Send another message to make sure original SEND_DEFAULTS unchanged
send_mail('subject', 'body', 'from@example.com', ['to@example.com'])
send_mail("subject", "body", "from@example.com", ["to@example.com"])
params = self.get_send_params()
self.assertEqual(params['metadata'], {'global': 'globalvalue', 'other': 'othervalue'})
self.assertEqual(params['tags'], ['globaltag'])
self.assertEqual(params['track_clicks'], True)
self.assertEqual(params['track_opens'], False)
self.assertEqual(params['globalextra'], 'globalsetting')
self.assertEqual(
params["metadata"], {"global": "globalvalue", "other": "othervalue"}
)
self.assertEqual(params["tags"], ["globaltag"])
self.assertEqual(params["track_clicks"], True)
self.assertEqual(params["track_opens"], False)
self.assertEqual(params["globalextra"], "globalsetting")
@override_settings(ANYMAIL={
'SEND_DEFAULTS': {
# This isn't an exhaustive list of Anymail message attrs; just one of each type
'metadata': {'global': 'globalvalue'},
'tags': ['globaltag'],
'template_id': 'global-template',
'esp_extra': {'globalextra': 'globalsetting'},
},
'TEST_SEND_DEFAULTS': { # "TEST" is the name of the Test EmailBackend's ESP
'merge_global_data': {'esp': 'espmerge'},
'metadata': {'esp': 'espvalue'},
'tags': ['esptag'],
'esp_extra': {'espextra': 'espsetting'},
@override_settings(
ANYMAIL={
"SEND_DEFAULTS": {
# This isn't an exhaustive list of Anymail message attrs;
# just one of each type
"metadata": {"global": "globalvalue"},
"tags": ["globaltag"],
"template_id": "global-template",
"esp_extra": {"globalextra": "globalsetting"},
},
# "TEST" is the name of the Test EmailBackend's ESP
"TEST_SEND_DEFAULTS": {
"merge_global_data": {"esp": "espmerge"},
"metadata": {"esp": "espvalue"},
"tags": ["esptag"],
"esp_extra": {"espextra": "espsetting"},
},
}
})
)
def test_esp_send_defaults_override_globals(self):
"""ESP-specific send defaults override *individual* global defaults"""
self.message.send()
params = self.get_send_params()
self.assertEqual(params['merge_global_data'], {'esp': 'espmerge'}) # esp-defaults only
self.assertEqual(params['metadata'], {'esp': 'espvalue'})
self.assertEqual(params['tags'], ['esptag'])
self.assertEqual(params['template_id'], 'global-template') # global-defaults only
self.assertEqual(params['espextra'], 'espsetting')
self.assertNotIn('globalextra', params) # entire esp_extra is overriden by esp-send-defaults
# esp-defaults only:
self.assertEqual(params["merge_global_data"], {"esp": "espmerge"})
self.assertEqual(params["metadata"], {"esp": "espvalue"})
self.assertEqual(params["tags"], ["esptag"])
# global-defaults only:
self.assertEqual(params["template_id"], "global-template")
self.assertEqual(params["espextra"], "espsetting")
# entire esp_extra is overriden by esp-send-defaults:
self.assertNotIn("globalextra", params)
class LazyStringsTest(TestBackendTestCase):
@@ -248,66 +297,69 @@ class LazyStringsTest(TestBackendTestCase):
"""
def assertNotLazy(self, s, msg=None):
self.assertNotIsInstance(s, Promise,
msg=msg or "String %r is lazy" % str(s))
self.assertNotIsInstance(s, Promise, msg=msg or "String %r is lazy" % str(s))
def test_lazy_from(self):
# This sometimes ends up lazy when settings.DEFAULT_FROM_EMAIL is meant to be localized
# This sometimes ends up lazy when settings.DEFAULT_FROM_EMAIL
# is meant to be localized
self.message.from_email = gettext_lazy('"Global Sales" <sales@example.com>')
self.message.send()
params = self.get_send_params()
self.assertNotLazy(params['from'].address)
self.assertNotLazy(params["from"].address)
def test_lazy_subject(self):
self.message.subject = gettext_lazy("subject")
self.message.send()
params = self.get_send_params()
self.assertNotLazy(params['subject'])
self.assertNotLazy(params["subject"])
def test_lazy_body(self):
self.message.body = gettext_lazy("text body")
self.message.attach_alternative(gettext_lazy("html body"), "text/html")
self.message.send()
params = self.get_send_params()
self.assertNotLazy(params['text_body'])
self.assertNotLazy(params['html_body'])
self.assertNotLazy(params["text_body"])
self.assertNotLazy(params["html_body"])
def test_lazy_headers(self):
self.message.extra_headers['X-Test'] = gettext_lazy("Test Header")
self.message.extra_headers["X-Test"] = gettext_lazy("Test Header")
self.message.send()
params = self.get_send_params()
self.assertNotLazy(params['extra_headers']['X-Test'])
self.assertNotLazy(params["extra_headers"]["X-Test"])
def test_lazy_attachments(self):
self.message.attach(gettext_lazy("test.csv"), gettext_lazy("test,csv,data"), "text/csv")
self.message.attach(
gettext_lazy("test.csv"), gettext_lazy("test,csv,data"), "text/csv"
)
self.message.attach(MIMEText(gettext_lazy("contact info")))
self.message.send()
params = self.get_send_params()
self.assertNotLazy(params['attachments'][0].name)
self.assertNotLazy(params['attachments'][0].content)
self.assertNotLazy(params['attachments'][1].content)
self.assertNotLazy(params["attachments"][0].name)
self.assertNotLazy(params["attachments"][0].content)
self.assertNotLazy(params["attachments"][1].content)
def test_lazy_tags(self):
self.message.tags = [gettext_lazy("Shipping"), gettext_lazy("Sales")]
self.message.send()
params = self.get_send_params()
self.assertNotLazy(params['tags'][0])
self.assertNotLazy(params['tags'][1])
self.assertNotLazy(params["tags"][0])
self.assertNotLazy(params["tags"][1])
def test_lazy_metadata(self):
self.message.metadata = {'order_type': gettext_lazy("Subscription")}
self.message.metadata = {"order_type": gettext_lazy("Subscription")}
self.message.send()
params = self.get_send_params()
self.assertNotLazy(params['metadata']['order_type'])
self.assertNotLazy(params["metadata"]["order_type"])
def test_lazy_merge_data(self):
self.message.merge_data = {
'to@example.com': {'duration': gettext_lazy("One Month")}}
self.message.merge_global_data = {'order_type': gettext_lazy("Subscription")}
"to@example.com": {"duration": gettext_lazy("One Month")}
}
self.message.merge_global_data = {"order_type": gettext_lazy("Subscription")}
self.message.send()
params = self.get_send_params()
self.assertNotLazy(params['merge_data']['to@example.com']['duration'])
self.assertNotLazy(params['merge_global_data']['order_type'])
self.assertNotLazy(params["merge_data"]["to@example.com"]["duration"])
self.assertNotLazy(params["merge_global_data"]["order_type"])
class CatchCommonErrorsTests(TestBackendTestCase):
@@ -321,46 +373,60 @@ class CatchCommonErrorsTests(TestBackendTestCase):
# in EmailMessage.recipients (called from EmailMessage.send) before
# Anymail gets a chance to complain.)
self.message.reply_to = "single-reply-to@example.com"
with self.assertRaisesMessage(TypeError, '"reply_to" attribute must be a list or other iterable'):
with self.assertRaisesMessage(
TypeError, '"reply_to" attribute must be a list or other iterable'
):
self.message.send()
def test_explains_reply_to_must_be_list_lazy(self):
"""Same as previous tests, with lazy strings"""
# Lazy strings can fool string/iterable detection
self.message.reply_to = gettext_lazy("single-reply-to@example.com")
with self.assertRaisesMessage(TypeError, '"reply_to" attribute must be a list or other iterable'):
with self.assertRaisesMessage(
TypeError, '"reply_to" attribute must be a list or other iterable'
):
self.message.send()
def test_identifies_source_of_parsing_errors(self):
"""Errors parsing email addresses should say which field had the problem"""
# Note: General email address parsing tests are in test_utils.ParseAddressListTests.
# This just checks the error includes the field name when parsing for sending a message.
self.message.from_email = ''
with self.assertRaisesMessage(AnymailInvalidAddress,
"Invalid email address '' parsed from '' in `from_email`."):
# Note: General email address parsing tests are in
# test_utils.ParseAddressListTests. This just checks the error includes the
# field name when parsing for sending a message.
self.message.from_email = ""
with self.assertRaisesMessage(
AnymailInvalidAddress,
"Invalid email address '' parsed from '' in `from_email`.",
):
self.message.send()
self.message.from_email = 'from@example.com'
self.message.from_email = "from@example.com"
# parse_address_list
self.message.to = ['ok@example.com', 'oops']
with self.assertRaisesMessage(AnymailInvalidAddress,
"Invalid email address 'oops' parsed from 'ok@example.com, oops' in `to`."):
self.message.to = ["ok@example.com", "oops"]
with self.assertRaisesMessage(
AnymailInvalidAddress,
"Invalid email address 'oops' parsed from 'ok@example.com, oops' in `to`.",
):
self.message.send()
self.message.to = ['test@example.com']
self.message.to = ["test@example.com"]
# parse_single_address
self.message.envelope_sender = 'one@example.com, two@example.com'
with self.assertRaisesMessage(AnymailInvalidAddress,
"Only one email address is allowed; found 2"
" in 'one@example.com, two@example.com' in `envelope_sender`."):
self.message.envelope_sender = "one@example.com, two@example.com"
with self.assertRaisesMessage(
AnymailInvalidAddress,
"Only one email address is allowed; found 2"
" in 'one@example.com, two@example.com' in `envelope_sender`.",
):
self.message.send()
delattr(self.message, 'envelope_sender')
delattr(self.message, "envelope_sender")
# process_extra_headers
self.message.extra_headers['From'] = 'Mail, Inc. <mail@example.com>'
with self.assertRaisesMessage(AnymailInvalidAddress,
"Invalid email address 'Mail' parsed from 'Mail, Inc. <mail@example.com>'"
" in `extra_headers['From']`. (Maybe missing quotes around a display-name?)"):
self.message.extra_headers["From"] = "Mail, Inc. <mail@example.com>"
with self.assertRaisesMessage(
AnymailInvalidAddress,
"Invalid email address 'Mail' parsed from"
" 'Mail, Inc. <mail@example.com>' in `extra_headers['From']`."
" (Maybe missing quotes around a display-name?)",
):
self.message.send()
def test_error_minimizes_pii_leakage(self):
@@ -390,39 +456,55 @@ class SpecialHeaderTests(TestBackendTestCase):
"""Anymail should handle special extra_headers the same way Django does"""
def test_reply_to(self):
"""Django allows message.reply_to and message.extra_headers['Reply-To'], and the latter takes precedence"""
"""
Django allows message.reply_to and message.extra_headers['Reply-To'],
and the latter takes precedence
"""
self.message.reply_to = ["attr@example.com"]
self.message.extra_headers = {"X-Extra": "extra"}
self.message.send()
params = self.get_send_params()
self.assertEqual(flatten_emails(params['reply_to']), ["attr@example.com"])
self.assertEqual(params['extra_headers'], {"X-Extra": "extra"})
self.assertEqual(flatten_emails(params["reply_to"]), ["attr@example.com"])
self.assertEqual(params["extra_headers"], {"X-Extra": "extra"})
self.message.reply_to = None
self.message.extra_headers = {"Reply-To": "header@example.com", "X-Extra": "extra"}
self.message.extra_headers = {
"Reply-To": "header@example.com",
"X-Extra": "extra",
}
self.message.send()
params = self.get_send_params()
self.assertEqual(flatten_emails(params['reply_to']), ["header@example.com"])
self.assertEqual(params['extra_headers'], {"X-Extra": "extra"}) # Reply-To no longer there
self.assertEqual(flatten_emails(params["reply_to"]), ["header@example.com"])
# Reply-To no longer there:
self.assertEqual(params["extra_headers"], {"X-Extra": "extra"})
# If both are supplied, the header wins (to match Django EmailMessage.message() behavior).
# Also, header names are case-insensitive.
# If both are supplied, the header wins (to match Django EmailMessage.message()
# behavior). Also, header names are case-insensitive.
self.message.reply_to = ["attr@example.com"]
self.message.extra_headers = {"REPLY-to": "header@example.com", "X-Extra": "extra"}
self.message.extra_headers = {
"REPLY-to": "header@example.com",
"X-Extra": "extra",
}
self.message.send()
params = self.get_send_params()
self.assertEqual(flatten_emails(params['reply_to']), ["header@example.com"])
self.assertEqual(params['extra_headers'], {"X-Extra": "extra"}) # Reply-To no longer there
self.assertEqual(flatten_emails(params["reply_to"]), ["header@example.com"])
# Reply-To no longer there
self.assertEqual(params["extra_headers"], {"X-Extra": "extra"})
def test_envelope_sender(self):
"""Django treats message.from_email as envelope-sender if messsage.extra_headers['From'] is set"""
"""
Django treats message.from_email as envelope-sender
if message.extra_headers['From'] is set
"""
# Using Anymail's envelope_sender extension
self.message.from_email = "Header From <header@example.com>"
self.message.envelope_sender = "Envelope From <envelope@bounces.example.com>" # Anymail extension
self.message.envelope_sender = (
"Envelope From <envelope@bounces.example.com>" # Anymail extension
)
self.message.send()
params = self.get_send_params()
self.assertEqual(params['from'].address, "Header From <header@example.com>")
self.assertEqual(params['envelope_sender'], "envelope@bounces.example.com")
self.assertEqual(params["from"].address, "Header From <header@example.com>")
self.assertEqual(params["envelope_sender"], "envelope@bounces.example.com")
# Using Django's undocumented message.extra_headers['From'] extension
# (see https://code.djangoproject.com/ticket/9214)
@@ -430,21 +512,31 @@ class SpecialHeaderTests(TestBackendTestCase):
self.message.extra_headers = {"From": "Header From <header@example.com>"}
self.message.send()
params = self.get_send_params()
self.assertEqual(params['from'].address, "Header From <header@example.com>")
self.assertEqual(params['envelope_sender'], "envelope@bounces.example.com")
self.assertNotIn("From", params.get('extra_headers', {})) # From was removed from extra-headers
self.assertEqual(params["from"].address, "Header From <header@example.com>")
self.assertEqual(params["envelope_sender"], "envelope@bounces.example.com")
# From was removed from extra-headers:
self.assertNotIn("From", params.get("extra_headers", {}))
def test_spoofed_to_header(self):
"""Django treats message.to as envelope-recipient if message.extra_headers['To'] is set"""
"""
Django treats message.to as envelope-recipient
if message.extra_headers['To'] is set
"""
# No current ESP supports this (and it's unlikely they would)
self.message.to = ["actual-recipient@example.com"]
self.message.extra_headers = {"To": "Apparent Recipient <but-not-really@example.com>"}
with self.assertRaisesMessage(AnymailUnsupportedFeature, "spoofing `To` header"):
self.message.extra_headers = {
"To": "Apparent Recipient <but-not-really@example.com>"
}
with self.assertRaisesMessage(
AnymailUnsupportedFeature, "spoofing `To` header"
):
self.message.send()
class AlternativePartsTests(TestBackendTestCase):
"""Anymail should handle alternative parts consistently with Django's SMTP backend"""
"""
Anymail should handle alternative parts consistently with Django's SMTP backend
"""
def test_default_usage(self):
"""Body defaults to text/plain, use alternative for html"""
@@ -452,9 +544,9 @@ class AlternativePartsTests(TestBackendTestCase):
self.message.attach_alternative("html body", "text/html")
self.message.send()
params = self.get_send_params()
self.assertEqual(params['text_body'], "plain body")
self.assertEqual(params['html_body'], "html body")
self.assertNotIn('alternatives', params)
self.assertEqual(params["text_body"], "plain body")
self.assertEqual(params["html_body"], "html body")
self.assertNotIn("alternatives", params)
def test_content_subtype_html(self):
"""Change body to text/html, use alternative for plain"""
@@ -463,20 +555,22 @@ class AlternativePartsTests(TestBackendTestCase):
self.message.attach_alternative("plain body", "text/plain")
self.message.send()
params = self.get_send_params()
self.assertEqual(params['text_body'], "plain body")
self.assertEqual(params['html_body'], "html body")
self.assertNotIn('alternatives', params)
self.assertEqual(params["text_body"], "plain body")
self.assertEqual(params["html_body"], "html body")
self.assertNotIn("alternatives", params)
def test_attach_plain_and_html(self):
"""Use alternatives for both bodies"""
message = AnymailMessage(subject="Subject", from_email="from@example.com", to=["to@example.com"])
message = AnymailMessage(
subject="Subject", from_email="from@example.com", to=["to@example.com"]
)
message.attach_alternative("plain body", "text/plain")
message.attach_alternative("html body", "text/html")
message.send()
params = self.get_send_params()
self.assertEqual(params['text_body'], "plain body")
self.assertEqual(params['html_body'], "html body")
self.assertNotIn('alternatives', params)
self.assertEqual(params["text_body"], "plain body")
self.assertEqual(params["html_body"], "html body")
self.assertNotIn("alternatives", params)
def test_additional_plain_part(self):
"""Two plaintext bodies"""
@@ -486,22 +580,24 @@ class AlternativePartsTests(TestBackendTestCase):
self.message.attach_alternative("second plain body", "text/plain")
self.message.send()
params = self.get_send_params()
self.assertEqual(params['text_body'], "plain body")
self.assertEqual(params['alternatives'], [("second plain body", "text/plain")])
self.assertEqual(params["text_body"], "plain body")
self.assertEqual(params["alternatives"], [("second plain body", "text/plain")])
def test_exotic_content_subtype(self):
"""Change body to text/calendar, use alternatives for plain and html"""
# This is unlikely to work with most ESPs, but we can try to communicate the intent...
# (You probably want an attachment rather than an alternative part.)
# This is unlikely to work with most ESPs, but we can try to communicate the
# intent... (You probably want an attachment rather than an alternative part.)
self.message.content_subtype = "calendar"
self.message.body = "BEGIN:VCALENDAR..."
self.message.attach_alternative("plain body", "text/plain")
self.message.attach_alternative("html body", "text/html")
self.message.send()
params = self.get_send_params()
self.assertEqual(params['text_body'], "plain body")
self.assertEqual(params['html_body'], "html body")
self.assertEqual(params['alternatives'], [("BEGIN:VCALENDAR...", "text/calendar")])
self.assertEqual(params["text_body"], "plain body")
self.assertEqual(params["html_body"], "html body")
self.assertEqual(
params["alternatives"], [("BEGIN:VCALENDAR...", "text/calendar")]
)
class BatchSendDetectionTestCase(TestBackendTestCase):
@@ -510,25 +606,25 @@ class BatchSendDetectionTestCase(TestBackendTestCase):
def test_default_is_not_batch(self):
self.message.send()
params = self.get_send_params()
self.assertFalse(params['is_batch_send'])
self.assertFalse(params["is_batch_send"])
def test_merge_data_implies_batch(self):
self.message.merge_data = {} # *anything* (even empty dict) implies batch
self.message.send()
params = self.get_send_params()
self.assertTrue(params['is_batch_send'])
self.assertTrue(params["is_batch_send"])
def test_merge_metadata_implies_batch(self):
self.message.merge_metadata = {} # *anything* (even empty dict) implies batch
self.message.send()
params = self.get_send_params()
self.assertTrue(params['is_batch_send'])
self.assertTrue(params["is_batch_send"])
def test_merge_global_data_does_not_imply_batch(self):
self.message.merge_global_data = {}
self.message.send()
params = self.get_send_params()
self.assertFalse(params['is_batch_send'])
self.assertFalse(params["is_batch_send"])
def test_cannot_call_is_batch_during_init(self):
# It's tempting to try to warn about unsupported batch features in setters,
@@ -539,8 +635,11 @@ class BatchSendDetectionTestCase(TestBackendTestCase):
self.unsupported_feature("cc with batch send")
super().set_cc(emails)
connection = mail.get_connection('anymail.backends.test.EmailBackend',
payload_class=ImproperlyImplementedPayload)
with self.assertRaisesMessage(AssertionError,
"Cannot call is_batch before all attributes processed"):
connection = mail.get_connection(
"anymail.backends.test.EmailBackend",
payload_class=ImproperlyImplementedPayload,
)
with self.assertRaisesMessage(
AssertionError, "Cannot call is_batch before all attributes processed"
):
connection.send_messages([self.message])

View File

@@ -16,42 +16,63 @@ SAMPLE_IMAGE_CONTENT = sample_image_content()
class AnymailInboundMessageConstructionTests(SimpleTestCase):
def test_construct_params(self):
msg = AnymailInboundMessage.construct(
from_email="from@example.com", to="to@example.com", cc="cc@example.com",
subject="test subject")
self.assertEqual(msg['From'], "from@example.com")
self.assertEqual(msg['To'], "to@example.com")
self.assertEqual(msg['Cc'], "cc@example.com")
self.assertEqual(msg['Subject'], "test subject")
from_email="from@example.com",
to="to@example.com",
cc="cc@example.com",
subject="test subject",
)
self.assertEqual(msg["From"], "from@example.com")
self.assertEqual(msg["To"], "to@example.com")
self.assertEqual(msg["Cc"], "cc@example.com")
self.assertEqual(msg["Subject"], "test subject")
self.assertEqual(msg.defects, []) # ensures email.message.Message.__init__ ran
self.assertIsNone(msg.envelope_recipient) # ensures AnymailInboundMessage.__init__ ran
# ensures email.message.Message.__init__ ran:
self.assertEqual(msg.defects, [])
# ensures AnymailInboundMessage.__init__ ran:
self.assertIsNone(msg.envelope_recipient)
def test_construct_headers_from_mapping(self):
msg = AnymailInboundMessage.construct(
headers={'Reply-To': "reply@example.com", 'X-Test': "anything"})
self.assertEqual(msg['reply-to'], "reply@example.com") # headers are case-insensitive
self.assertEqual(msg['X-TEST'], "anything")
headers={"Reply-To": "reply@example.com", "X-Test": "anything"}
)
# headers are case-insensitive:
self.assertEqual(msg["reply-to"], "reply@example.com")
self.assertEqual(msg["X-TEST"], "anything")
def test_construct_headers_from_pairs(self):
# allows multiple instances of a header
msg = AnymailInboundMessage.construct(
headers=[['Reply-To', "reply@example.com"],
['Received', "by 10.1.1.4 with SMTP id q4csp; Sun, 22 Oct 2017 00:23:22 -0700 (PDT)"],
['Received', "from mail.example.com (mail.example.com. [10.10.1.9])"
" by mx.example.com with SMTPS id 93s8iok for <to@example.com>;"
" Sun, 22 Oct 2017 00:23:21 -0700 (PDT)"],
])
self.assertEqual(msg['Reply-To'], "reply@example.com")
self.assertEqual(msg.get_all('Received'), [
"by 10.1.1.4 with SMTP id q4csp; Sun, 22 Oct 2017 00:23:22 -0700 (PDT)",
"from mail.example.com (mail.example.com. [10.10.1.9])"
" by mx.example.com with SMTPS id 93s8iok for <to@example.com>;"
" Sun, 22 Oct 2017 00:23:21 -0700 (PDT)"])
headers=[
["Reply-To", "reply@example.com"],
[
"Received",
"by 10.1.1.4 with SMTP id q4csp;"
" Sun, 22 Oct 2017 00:23:22 -0700 (PDT)",
],
[
"Received",
"from mail.example.com (mail.example.com. [10.10.1.9])"
" by mx.example.com with SMTPS id 93s8iok for <to@example.com>;"
" Sun, 22 Oct 2017 00:23:21 -0700 (PDT)",
],
]
)
self.assertEqual(msg["Reply-To"], "reply@example.com")
self.assertEqual(
msg.get_all("Received"),
[
"by 10.1.1.4 with SMTP id q4csp; Sun, 22 Oct 2017 00:23:22 -0700 (PDT)",
"from mail.example.com (mail.example.com. [10.10.1.9])"
" by mx.example.com with SMTPS id 93s8iok for <to@example.com>;"
" Sun, 22 Oct 2017 00:23:21 -0700 (PDT)",
],
)
def test_construct_headers_from_raw(self):
# (note header "folding" in second Received header)
msg = AnymailInboundMessage.construct(
raw_headers=dedent("""\
raw_headers=dedent(
"""\
Reply-To: reply@example.com
Subject: raw subject
Content-Type: x-custom/custom
@@ -59,86 +80,125 @@ class AnymailInboundMessageConstructionTests(SimpleTestCase):
Received: from mail.example.com (mail.example.com. [10.10.1.9])
by mx.example.com with SMTPS id 93s8iok for <to@example.com>;
Sun, 22 Oct 2017 00:23:21 -0700 (PDT)
"""),
subject="Explicit subject overrides raw")
self.assertEqual(msg['Reply-To'], "reply@example.com")
self.assertEqual(msg.get_all('Received'), [
"by 10.1.1.4 with SMTP id q4csp; Sun, 22 Oct 2017 00:23:22 -0700 (PDT)",
"from mail.example.com (mail.example.com. [10.10.1.9])" # unfolding should have stripped newlines
" by mx.example.com with SMTPS id 93s8iok for <to@example.com>;"
" Sun, 22 Oct 2017 00:23:21 -0700 (PDT)"])
self.assertEqual(msg.get_all('Subject'), ["Explicit subject overrides raw"])
self.assertEqual(msg.get_all('Content-Type'), ["multipart/mixed"]) # Content-Type in raw header ignored
""" # NOQA: E501
),
subject="Explicit subject overrides raw",
)
self.assertEqual(msg["Reply-To"], "reply@example.com")
self.assertEqual(
msg.get_all("Received"),
[
# unfolding should have stripped newlines
"by 10.1.1.4 with SMTP id q4csp; Sun, 22 Oct 2017 00:23:22 -0700 (PDT)",
"from mail.example.com (mail.example.com. [10.10.1.9])"
" by mx.example.com with SMTPS id 93s8iok for <to@example.com>;"
" Sun, 22 Oct 2017 00:23:21 -0700 (PDT)",
],
)
self.assertEqual(msg.get_all("Subject"), ["Explicit subject overrides raw"])
# Content-Type in raw header ignored:
self.assertEqual(msg.get_all("Content-Type"), ["multipart/mixed"])
def test_construct_bodies(self):
# this verifies we construct the expected MIME structure;
# see the `text` and `html` props (in the ConveniencePropTests below)
# for an easier way to get to these fields (that works however constructed)
msg = AnymailInboundMessage.construct(text="Plaintext body", html="HTML body")
self.assertEqual(msg['Content-Type'], "multipart/mixed")
self.assertEqual(msg["Content-Type"], "multipart/mixed")
self.assertEqual(len(msg.get_payload()), 1)
related = msg.get_payload(0)
self.assertEqual(related['Content-Type'], "multipart/related")
self.assertEqual(related["Content-Type"], "multipart/related")
self.assertEqual(len(related.get_payload()), 1)
alternative = related.get_payload(0)
self.assertEqual(alternative['Content-Type'], "multipart/alternative")
self.assertEqual(alternative["Content-Type"], "multipart/alternative")
self.assertEqual(len(alternative.get_payload()), 2)
plaintext = alternative.get_payload(0)
self.assertEqual(plaintext['Content-Type'], 'text/plain; charset="utf-8"')
self.assertEqual(plaintext["Content-Type"], 'text/plain; charset="utf-8"')
self.assertEqual(plaintext.get_content_text(), "Plaintext body")
html = alternative.get_payload(1)
self.assertEqual(html['Content-Type'], 'text/html; charset="utf-8"')
self.assertEqual(html["Content-Type"], 'text/html; charset="utf-8"')
self.assertEqual(html.get_content_text(), "HTML body")
def test_construct_attachments(self):
att1 = AnymailInboundMessage.construct_attachment(
'text/csv', "One,Two\n1,2".encode('iso-8859-1'), charset="iso-8859-1", filename="test.csv")
"text/csv",
"One,Two\n1,2".encode("iso-8859-1"),
charset="iso-8859-1",
filename="test.csv",
)
att2 = AnymailInboundMessage.construct_attachment(
'image/png', SAMPLE_IMAGE_CONTENT, filename=SAMPLE_IMAGE_FILENAME, content_id="abc123")
"image/png",
SAMPLE_IMAGE_CONTENT,
filename=SAMPLE_IMAGE_FILENAME,
content_id="abc123",
)
msg = AnymailInboundMessage.construct(attachments=[att1, att2])
self.assertEqual(msg['Content-Type'], "multipart/mixed")
self.assertEqual(msg["Content-Type"], "multipart/mixed")
self.assertEqual(len(msg.get_payload()), 2) # bodies (related), att1
att1_part = msg.get_payload(1)
self.assertEqual(att1_part['Content-Type'], 'text/csv; name="test.csv"; charset="iso-8859-1"')
self.assertEqual(att1_part['Content-Disposition'], 'attachment; filename="test.csv"')
self.assertNotIn('Content-ID', att1_part)
self.assertEqual(
att1_part["Content-Type"], 'text/csv; name="test.csv"; charset="iso-8859-1"'
)
self.assertEqual(
att1_part["Content-Disposition"], 'attachment; filename="test.csv"'
)
self.assertNotIn("Content-ID", att1_part)
self.assertEqual(att1_part.get_content_text(), "One,Two\n1,2")
related = msg.get_payload(0)
self.assertEqual(len(related.get_payload()), 2) # alternatives (with no bodies in this test); att2
# alternatives (with no bodies in this test); att2:
self.assertEqual(len(related.get_payload()), 2)
att2_part = related.get_payload(1)
self.assertEqual(att2_part['Content-Type'], 'image/png; name="sample_image.png"')
self.assertEqual(att2_part['Content-Disposition'], 'inline; filename="sample_image.png"')
self.assertEqual(att2_part['Content-ID'], '<abc123>')
self.assertEqual(
att2_part["Content-Type"], 'image/png; name="sample_image.png"'
)
self.assertEqual(
att2_part["Content-Disposition"], 'inline; filename="sample_image.png"'
)
self.assertEqual(att2_part["Content-ID"], "<abc123>")
self.assertEqual(att2_part.get_content_bytes(), SAMPLE_IMAGE_CONTENT)
def test_construct_attachments_from_uploaded_files(self):
from django.core.files.uploadedfile import SimpleUploadedFile
file = SimpleUploadedFile(SAMPLE_IMAGE_FILENAME, SAMPLE_IMAGE_CONTENT, 'image/png')
att = AnymailInboundMessage.construct_attachment_from_uploaded_file(file, content_id="abc123")
self.assertEqual(att['Content-Type'], 'image/png; name="sample_image.png"')
self.assertEqual(att['Content-Disposition'], 'inline; filename="sample_image.png"')
self.assertEqual(att['Content-ID'], '<abc123>')
file = SimpleUploadedFile(
SAMPLE_IMAGE_FILENAME, SAMPLE_IMAGE_CONTENT, "image/png"
)
att = AnymailInboundMessage.construct_attachment_from_uploaded_file(
file, content_id="abc123"
)
self.assertEqual(att["Content-Type"], 'image/png; name="sample_image.png"')
self.assertEqual(
att["Content-Disposition"], 'inline; filename="sample_image.png"'
)
self.assertEqual(att["Content-ID"], "<abc123>")
self.assertEqual(att.get_content_bytes(), SAMPLE_IMAGE_CONTENT)
def test_construct_attachments_from_base64_data(self):
# This is a fairly common way for ESPs to provide attachment content to webhooks
content = b64encode(SAMPLE_IMAGE_CONTENT)
att = AnymailInboundMessage.construct_attachment(content_type="image/png", content=content, base64=True)
att = AnymailInboundMessage.construct_attachment(
content_type="image/png", content=content, base64=True
)
self.assertEqual(att.get_content_bytes(), SAMPLE_IMAGE_CONTENT)
def test_construct_attachment_unicode_filename(self):
# Issue #197
att = AnymailInboundMessage.construct_attachment(
content_type="text/plain", content="Unicode ✓", charset='utf-8', base64=False,
filename="Simulácia.txt", content_id="inline-id",)
content_type="text/plain",
content="Unicode ✓",
charset="utf-8",
base64=False,
filename="Simulácia.txt",
content_id="inline-id",
)
self.assertEqual(att.get_filename(), "Simulácia.txt")
self.assertTrue(att.is_inline_attachment())
self.assertEqual(att.get_content_text(), "Unicode ✓")
@@ -146,35 +206,40 @@ class AnymailInboundMessageConstructionTests(SimpleTestCase):
def test_parse_raw_mime(self):
# (we're not trying to exhaustively test email.parser MIME handling here;
# just that AnymailInboundMessage.parse_raw_mime calls it correctly)
raw = dedent("""\
raw = dedent(
"""\
Content-Type: text/plain
Subject: This is a test message
This is a test body.
""")
"""
)
msg = AnymailInboundMessage.parse_raw_mime(raw)
self.assertEqual(msg['Subject'], "This is a test message")
self.assertEqual(msg["Subject"], "This is a test message")
self.assertEqual(msg.get_content_text(), "This is a test body.\n")
self.assertEqual(msg.defects, [])
# (see test_attachment_as_uploaded_file below for parsing basic attachment from raw mime)
# (see test_attachment_as_uploaded_file below
# for parsing basic attachment from raw mime)
def test_parse_raw_mime_bytes(self):
raw = (
b'Content-Type: text/plain; charset=ISO-8859-3\r\n'
b'Content-Transfer-Encoding: 8bit\r\n'
b'Subject: Test bytes\r\n'
b'\r\n'
b'\xD8i estas retpo\xFEto.\r\n')
b"Content-Type: text/plain; charset=ISO-8859-3\r\n"
b"Content-Transfer-Encoding: 8bit\r\n"
b"Subject: Test bytes\r\n"
b"\r\n"
b"\xD8i estas retpo\xFEto.\r\n"
)
msg = AnymailInboundMessage.parse_raw_mime_bytes(raw)
self.assertEqual(msg['Subject'], "Test bytes")
self.assertEqual(msg["Subject"], "Test bytes")
self.assertEqual(msg.get_content_text(), "Ĝi estas retpoŝto.\r\n")
self.assertEqual(msg.get_content_bytes(), b'\xD8i estas retpo\xFEto.\r\n')
self.assertEqual(msg.get_content_bytes(), b"\xD8i estas retpo\xFEto.\r\n")
self.assertEqual(msg.defects, [])
def test_parse_raw_mime_8bit_utf8(self):
# In come cases, the message below ends up with 'Content-Transfer-Encoding: 8bit',
# so needs to be parsed as bytes, not text (see https://bugs.python.org/issue18271).
# In come cases, the message below ends up with
# 'Content-Transfer-Encoding: 8bit', so needs to be parsed as bytes, not text
# (see https://bugs.python.org/issue18271).
# Message.as_string() returns str (text), not bytes.
# (This might be a Django bug; plain old MIMEText avoids the problem by using
# 'Content-Transfer-Encoding: base64', which parses fine as text or bytes.)
@@ -188,16 +253,26 @@ class AnymailInboundMessageConstructionTests(SimpleTestCase):
msg = AnymailInboundMessage.parse_raw_mime_file(fp)
self.assertEqual(msg["Subject"], "Test email")
self.assertEqual(msg.text, "Hi Bob, This is a message. Thanks!\n")
self.assertEqual(msg.get_all("Received"), [ # this is the first line in the sample email file
"by luna.mailgun.net with SMTP mgrt 8734663311733; Fri, 03 May 2013 18:26:27 +0000"])
self.assertEqual(
msg.get_all("Received"),
[ # this is the first line in the sample email file
"by luna.mailgun.net with SMTP mgrt 8734663311733;"
" Fri, 03 May 2013 18:26:27 +0000"
],
)
def test_parse_raw_mime_file_bytes(self):
with open(sample_email_path(), mode="rb") as fp:
msg = AnymailInboundMessage.parse_raw_mime_file(fp)
self.assertEqual(msg["Subject"], "Test email")
self.assertEqual(msg.text, "Hi Bob, This is a message. Thanks!\n")
self.assertEqual(msg.get_all("Received"), [ # this is the first line in the sample email file
"by luna.mailgun.net with SMTP mgrt 8734663311733; Fri, 03 May 2013 18:26:27 +0000"])
self.assertEqual(
msg.get_all("Received"),
[ # this is the first line in the sample email file
"by luna.mailgun.net with SMTP mgrt 8734663311733;"
" Fri, 03 May 2013 18:26:27 +0000"
],
)
class AnymailInboundMessageConveniencePropTests(SimpleTestCase):
@@ -207,24 +282,24 @@ class AnymailInboundMessageConveniencePropTests(SimpleTestCase):
def test_address_props(self):
msg = AnymailInboundMessage.construct(
from_email='"Sender, Inc." <sender@example.com>',
to='First To <to1@example.com>, to2@example.com',
cc='First Cc <cc1@example.com>, cc2@example.com',
to="First To <to1@example.com>, to2@example.com",
cc="First Cc <cc1@example.com>, cc2@example.com",
)
self.assertEqual(str(msg.from_email), '"Sender, Inc." <sender@example.com>')
self.assertEqual(msg.from_email.addr_spec, 'sender@example.com')
self.assertEqual(msg.from_email.display_name, 'Sender, Inc.')
self.assertEqual(msg.from_email.username, 'sender')
self.assertEqual(msg.from_email.domain, 'example.com')
self.assertEqual(msg.from_email.addr_spec, "sender@example.com")
self.assertEqual(msg.from_email.display_name, "Sender, Inc.")
self.assertEqual(msg.from_email.username, "sender")
self.assertEqual(msg.from_email.domain, "example.com")
self.assertEqual(len(msg.to), 2)
self.assertEqual(msg.to[0].addr_spec, 'to1@example.com')
self.assertEqual(msg.to[0].display_name, 'First To')
self.assertEqual(msg.to[1].addr_spec, 'to2@example.com')
self.assertEqual(msg.to[1].display_name, '')
self.assertEqual(msg.to[0].addr_spec, "to1@example.com")
self.assertEqual(msg.to[0].display_name, "First To")
self.assertEqual(msg.to[1].addr_spec, "to2@example.com")
self.assertEqual(msg.to[1].display_name, "")
self.assertEqual(len(msg.cc), 2)
self.assertEqual(msg.cc[0].address, 'First Cc <cc1@example.com>')
self.assertEqual(msg.cc[1].address, 'cc2@example.com')
self.assertEqual(msg.cc[0].address, "First Cc <cc1@example.com>")
self.assertEqual(msg.cc[1].address, "cc2@example.com")
# Default None/empty lists
msg = AnymailInboundMessage()
@@ -238,15 +313,24 @@ class AnymailInboundMessageConveniencePropTests(SimpleTestCase):
self.assertEqual(msg.html, "Test HTML")
# Make sure attachments don't confuse it
att_text = AnymailInboundMessage.construct_attachment('text/plain', "text attachment")
att_html = AnymailInboundMessage.construct_attachment('text/html', "html attachment")
att_text = AnymailInboundMessage.construct_attachment(
"text/plain", "text attachment"
)
att_html = AnymailInboundMessage.construct_attachment(
"text/html", "html attachment"
)
msg = AnymailInboundMessage.construct(text="Test plaintext", attachments=[att_text, att_html])
msg = AnymailInboundMessage.construct(
text="Test plaintext", attachments=[att_text, att_html]
)
self.assertEqual(msg.text, "Test plaintext")
self.assertIsNone(msg.html) # no html body (the html attachment doesn't count)
msg = AnymailInboundMessage.construct(html="Test HTML", attachments=[att_text, att_html])
self.assertIsNone(msg.text) # no plaintext body (the text attachment doesn't count)
msg = AnymailInboundMessage.construct(
html="Test HTML", attachments=[att_text, att_html]
)
# no plaintext body (the text attachment doesn't count):
self.assertIsNone(msg.text)
self.assertEqual(msg.html, "Test HTML")
# Default None
@@ -257,7 +341,8 @@ class AnymailInboundMessageConveniencePropTests(SimpleTestCase):
def test_body_props_charsets(self):
text_8859_10 = "Detta är det vanliga innehållet".encode("ISO-8859-10")
html_8859_8 = "<p>HTML זהו תוכן</p>".encode("ISO-8859-8")
raw = dedent("""\
raw = dedent(
"""\
MIME-Version: 1.0
Subject: Charset test
Content-Type: multipart/alternative; boundary="this_is_a_boundary"
@@ -273,10 +358,11 @@ class AnymailInboundMessageConveniencePropTests(SimpleTestCase):
{html}
--this_is_a_boundary--
""").format(
text=quopri.encodestring(text_8859_10).decode("ASCII"),
html=quopri.encodestring(html_8859_8).decode("ASCII"),
)
"""
).format(
text=quopri.encodestring(text_8859_10).decode("ASCII"),
html=quopri.encodestring(html_8859_8).decode("ASCII"),
)
msg = AnymailInboundMessage.parse_raw_mime(raw)
self.assertEqual(msg.defects, [])
@@ -284,41 +370,56 @@ class AnymailInboundMessageConveniencePropTests(SimpleTestCase):
self.assertEqual(msg.html, "<p>HTML זהו תוכן</p>")
self.assertEqual(msg.get_payload(0).get_content_bytes(), text_8859_10)
self.assertEqual(msg.get_payload(0).get_content_text(), "Detta är det vanliga innehållet")
self.assertEqual(
msg.get_payload(0).get_content_text(), "Detta är det vanliga innehållet"
)
self.assertEqual(msg.get_payload(1).get_content_bytes(), html_8859_8)
self.assertEqual(msg.get_payload(1).get_content_text(), "<p>HTML זהו תוכן</p>")
def test_missing_or_invalid_charsets(self):
"""get_content_text has options for handling missing/invalid charset declarations"""
raw = dedent("""\
"""
get_content_text has options for handling missing/invalid charset declarations
"""
raw = dedent(
"""\
Subject: Oops, missing charset declaration
Content-Type: text/plain
Content-Transfer-Encoding: quoted-printable
Algunos programas de correo electr=f3nico est=e1n rotos
""")
"""
)
msg = AnymailInboundMessage.parse_raw_mime(raw)
self.assertEqual(msg.defects, [])
# default is charset from Content-Type (or 'utf-8' if missing), errors='replace'; .text uses defaults
self.assertEqual(msg.get_content_text(),
"Algunos programas de correo electr<74>nico est<73>n rotos\n")
self.assertEqual(msg.text, "Algunos programas de correo electr<74>nico est<73>n rotos\n")
# default is charset from Content-Type (or 'utf-8' if missing),
# errors='replace'; .text uses defaults
self.assertEqual(
msg.get_content_text(),
"Algunos programas de correo electr<74>nico est<73>n rotos\n",
)
self.assertEqual(
msg.text, "Algunos programas de correo electr<74>nico est<73>n rotos\n"
)
# can give specific charset if you know headers are wrong/missing
self.assertEqual(msg.get_content_text(charset='ISO-8859-1'),
"Algunos programas de correo electrónico están rotos\n")
self.assertEqual(
msg.get_content_text(charset="ISO-8859-1"),
"Algunos programas de correo electrónico están rotos\n",
)
# can change error handling
with self.assertRaises(UnicodeDecodeError):
msg.get_content_text(errors='strict')
self.assertEqual(msg.get_content_text(errors='ignore'),
"Algunos programas de correo electrnico estn rotos\n")
msg.get_content_text(errors="strict")
self.assertEqual(
msg.get_content_text(errors="ignore"),
"Algunos programas de correo electrnico estn rotos\n",
)
def test_date_props(self):
msg = AnymailInboundMessage.construct(headers={
'Date': "Mon, 23 Oct 2017 17:50:55 -0700"
})
msg = AnymailInboundMessage.construct(
headers={"Date": "Mon, 23 Oct 2017 17:50:55 -0700"}
)
self.assertEqual(msg.date.isoformat(), "2017-10-23T17:50:55-07:00")
# Default None
@@ -326,7 +427,8 @@ class AnymailInboundMessageConveniencePropTests(SimpleTestCase):
def test_attachments_prop(self):
att = AnymailInboundMessage.construct_attachment(
'image/png', SAMPLE_IMAGE_CONTENT, filename=SAMPLE_IMAGE_FILENAME)
"image/png", SAMPLE_IMAGE_CONTENT, filename=SAMPLE_IMAGE_FILENAME
)
msg = AnymailInboundMessage.construct(attachments=[att])
self.assertEqual(msg.attachments, [att])
@@ -336,16 +438,21 @@ class AnymailInboundMessageConveniencePropTests(SimpleTestCase):
def test_inline_attachments_prop(self):
att = AnymailInboundMessage.construct_attachment(
'image/png', SAMPLE_IMAGE_CONTENT, filename=SAMPLE_IMAGE_FILENAME, content_id="abc123")
"image/png",
SAMPLE_IMAGE_CONTENT,
filename=SAMPLE_IMAGE_FILENAME,
content_id="abc123",
)
msg = AnymailInboundMessage.construct(attachments=[att])
self.assertEqual(msg.inline_attachments, {'abc123': att})
self.assertEqual(msg.inline_attachments, {"abc123": att})
# Default empty dict
self.assertEqual(AnymailInboundMessage().inline_attachments, {})
def test_attachment_as_uploaded_file(self):
raw = dedent("""\
raw = dedent(
"""\
MIME-Version: 1.0
Subject: Attachment test
Content-Type: multipart/mixed; boundary="this_is_a_boundary"
@@ -372,7 +479,8 @@ class AnymailInboundMessageConveniencePropTests(SimpleTestCase):
4j9/yYxupaQbXPJLNqsGFgeZ6qwpLP1b4AV4AV5AoKfjpR5OwR6VKwULCAC+AQV4W9Ps4uZQAAAA
AElFTkSuQmCC
--this_is_a_boundary--
""")
"""
)
msg = AnymailInboundMessage.parse_raw_mime(raw)
attachment = msg.attachments[0]
@@ -385,7 +493,8 @@ class AnymailInboundMessageConveniencePropTests(SimpleTestCase):
def test_attachment_as_uploaded_file_security(self):
# Raw attachment filenames can be malicious; we want to make sure that
# our Django file converter sanitizes them (as much as any uploaded filename)
raw = dedent("""\
raw = dedent(
"""\
MIME-Version: 1.0
Subject: Attachment test
Content-Type: multipart/mixed; boundary="this_is_a_boundary"
@@ -407,21 +516,26 @@ class AnymailInboundMessageConveniencePropTests(SimpleTestCase):
<body>Hey, did I overwrite your site?</body>
--this_is_a_boundary--
""")
"""
)
msg = AnymailInboundMessage.parse_raw_mime(raw)
attachments = msg.attachments
self.assertEqual(attachments[0].get_filename(), "/etc/passwd") # you wouldn't want to actually write here
self.assertEqual(attachments[0].as_uploaded_file().name, "passwd") # path removed - good!
# you wouldn't want to actually write here:
self.assertEqual(attachments[0].get_filename(), "/etc/passwd")
# path removed - good!:
self.assertEqual(attachments[0].as_uploaded_file().name, "passwd")
# ditto for relative paths:
self.assertEqual(attachments[1].get_filename(), "../static/index.html")
self.assertEqual(attachments[1].as_uploaded_file().name, "index.html") # ditto for relative paths
self.assertEqual(attachments[1].as_uploaded_file().name, "index.html")
class AnymailInboundMessageAttachedMessageTests(SimpleTestCase):
# message/rfc822 attachments should get parsed recursively
original_raw_message = dedent("""\
original_raw_message = dedent(
"""\
MIME-Version: 1.0
From: sender@example.com
Subject: Original message
@@ -441,11 +555,13 @@ class AnymailInboundMessageAttachedMessageTests(SimpleTestCase):
{image_content_base64}
--boundary-orig--
""").format(image_content_base64=b64encode(SAMPLE_IMAGE_CONTENT).decode('ascii'))
"""
).format(image_content_base64=b64encode(SAMPLE_IMAGE_CONTENT).decode("ascii"))
def test_parse_rfc822_attachment_from_raw_mime(self):
# message/rfc822 attachments should be parsed recursively
raw = dedent("""\
raw = dedent(
"""\
MIME-Version: 1.0
From: mailer-demon@example.org
Subject: Undeliverable
@@ -464,7 +580,8 @@ class AnymailInboundMessageAttachedMessageTests(SimpleTestCase):
{original_raw_message}
--boundary-bounce--
""").format(original_raw_message=self.original_raw_message)
"""
).format(original_raw_message=self.original_raw_message)
msg = AnymailInboundMessage.parse_raw_mime(raw)
self.assertIsInstance(msg, AnymailInboundMessage)
@@ -476,7 +593,7 @@ class AnymailInboundMessageAttachedMessageTests(SimpleTestCase):
orig_msg = att.get_payload(0)
self.assertIsInstance(orig_msg, AnymailInboundMessage)
self.assertEqual(orig_msg['Subject'], "Original message")
self.assertEqual(orig_msg["Subject"], "Original message")
self.assertEqual(orig_msg.get_content_type(), "multipart/related")
self.assertEqual(att.get_content_text(), self.original_raw_message)
@@ -488,7 +605,9 @@ class AnymailInboundMessageAttachedMessageTests(SimpleTestCase):
def test_construct_rfc822_attachment_from_data(self):
# constructed message/rfc822 attachment should end up as parsed message
# (same as if attachment was parsed from raw mime, as in previous test)
att = AnymailInboundMessage.construct_attachment('message/rfc822', self.original_raw_message)
att = AnymailInboundMessage.construct_attachment(
"message/rfc822", self.original_raw_message
)
self.assertIsInstance(att, AnymailInboundMessage)
self.assertEqual(att.get_content_type(), "message/rfc822")
self.assertTrue(att.is_attachment())
@@ -496,7 +615,7 @@ class AnymailInboundMessageAttachedMessageTests(SimpleTestCase):
orig_msg = att.get_payload(0)
self.assertIsInstance(orig_msg, AnymailInboundMessage)
self.assertEqual(orig_msg['Subject'], "Original message")
self.assertEqual(orig_msg["Subject"], "Original message")
self.assertEqual(orig_msg.get_content_type(), "multipart/related")
@@ -507,7 +626,8 @@ class EmailParserBehaviorTests(SimpleTestCase):
# in older, broken versions of the EmailParser.)
def test_parse_folded_headers(self):
raw = dedent("""\
raw = dedent(
"""\
Content-Type: text/plain
Subject: This subject uses
header folding
@@ -517,19 +637,27 @@ class EmailParserBehaviorTests(SimpleTestCase):
Not-A-Header: This is the body.
It is not folded.
""")
for end in ('\n', '\r', '\r\n'): # check NL, CR, and CRNL line-endings
msg = AnymailInboundMessage.parse_raw_mime(raw.replace('\n', end))
self.assertEqual(msg['Subject'], "This subject uses header folding")
self.assertEqual(msg["X-Json"],
'{"problematic": ["encoded newline\\n", "comma,semi;no space"]}')
self.assertEqual(msg.get_content_text(),
"Not-A-Header: This is the body.{end} It is not folded.{end}".format(end=end))
"""
)
for end in ("\n", "\r", "\r\n"): # check NL, CR, and CRNL line-endings
msg = AnymailInboundMessage.parse_raw_mime(raw.replace("\n", end))
self.assertEqual(msg["Subject"], "This subject uses header folding")
self.assertEqual(
msg["X-Json"],
'{"problematic": ["encoded newline\\n", "comma,semi;no space"]}',
)
self.assertEqual(
msg.get_content_text(),
"Not-A-Header: This is the body.{end} It is not folded.{end}".format(
end=end
),
)
self.assertEqual(msg.defects, [])
def test_parse_encoded_headers(self):
# RFC2047 header encoding
raw = dedent("""\
raw = dedent(
"""\
Content-Type: text/plain
From: =?US-ASCII?Q?Keith_Moore?= <moore@example.com>
To: =?ISO-8859-1?Q?Keld_J=F8rn_Simonsen?= <keld@example.com>,
@@ -540,35 +668,43 @@ class EmailParserBehaviorTests(SimpleTestCase):
X-Broken: =?utf-8?q?Not_a_char:_=88.?=
Some examples adapted from http://dogmamix.com/MimeHeadersDecoder/
""")
"""
)
msg = AnymailInboundMessage.parse_raw_mime(raw)
self.assertEqual(msg["From"], "Keith Moore <moore@example.com>")
self.assertEqual(msg.from_email.display_name, "Keith Moore")
self.assertEqual(msg.from_email.addr_spec, "moore@example.com")
self.assertEqual(msg["To"],
'Keld Jørn Simonsen <keld@example.com>, '
'"André Pirard, Jr." <PIRARD@example.com>')
self.assertEqual(
msg["To"],
"Keld Jørn Simonsen <keld@example.com>, "
'"André Pirard, Jr." <PIRARD@example.com>',
)
self.assertEqual(msg.to[0].display_name, "Keld Jørn Simonsen")
self.assertEqual(msg.to[1].display_name, "André Pirard, Jr.")
# Note: Like email.headerregistry.Address, Anymail decodes an RFC2047-encoded display_name,
# but does not decode a punycode domain. (Use `idna.decode(domain)` if you need that.)
# Note: Like email.headerregistry.Address, Anymail decodes an RFC2047-encoded
# display_name, but does not decode a punycode domain.
# (Use `idna.decode(domain)` if you need that.)
self.assertEqual(msg["Cc"], "Người nhận <cc@xn--th-e0a.example.com>")
self.assertEqual(msg.cc[0].display_name, "Người nhận")
self.assertEqual(msg.cc[0].addr_spec, "cc@xn--th-e0a.example.com")
self.assertEqual(msg.cc[0].domain, "xn--th-e0a.example.com")
# Subject breaks between 'o' and 'u' in the word "you", must be re-joined without space.
# Also tests joining encoded words with different charsets:
self.assertEqual(msg["Subject"], "If you can read this you understand the example\N{CHECK MARK}")
# Subject breaks between 'o' and 'u' in the word "you", must be re-joined
# without space. Also tests joining encoded words with different charsets:
self.assertEqual(
msg["Subject"],
"If you can read this you understand the example\N{CHECK MARK}",
)
# Replace illegal encodings (rather than causing error):
self.assertEqual(msg["X-Broken"], "Not a char: \N{REPLACEMENT CHARACTER}.")
def test_parse_encoded_params(self):
raw = dedent("""\
raw = dedent(
"""\
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="this_is_a_boundary"
@@ -584,10 +720,14 @@ class EmailParserBehaviorTests(SimpleTestCase):
This is an attachment
--this_is_a_boundary--
""")
"""
)
msg = AnymailInboundMessage.parse_raw_mime(raw)
att = msg.attachments[0]
self.assertTrue(att.is_attachment())
self.assertEqual(att.get_content_disposition(), "attachment")
self.assertEqual(collapse_rfc2231_value(att.get_param("Name", header="Content-Type")), "TPS Report")
self.assertEqual(
collapse_rfc2231_value(att.get_param("Name", header="Content-Type")),
"TPS Report",
)
self.assertEqual(att.get_filename(), "Une pièce jointe.txt")

File diff suppressed because it is too large Load Diff

View File

@@ -12,128 +12,181 @@ from anymail.signals import AnymailInboundEvent
from anymail.webhooks.mailgun import MailgunInboundWebhookView
from .test_mailgun_webhooks import (
TEST_WEBHOOK_SIGNING_KEY, mailgun_sign_payload,
mailgun_sign_legacy_payload, querydict_to_postdict)
from .utils import sample_image_content, sample_email_content, encode_multipart, make_fileobj
TEST_WEBHOOK_SIGNING_KEY,
mailgun_sign_legacy_payload,
mailgun_sign_payload,
querydict_to_postdict,
)
from .utils import (
encode_multipart,
make_fileobj,
sample_email_content,
sample_image_content,
)
from .webhook_cases import WebhookTestCase
@tag('mailgun')
@tag("mailgun")
@override_settings(ANYMAIL_MAILGUN_WEBHOOK_SIGNING_KEY=TEST_WEBHOOK_SIGNING_KEY)
class MailgunInboundTestCase(WebhookTestCase):
def test_inbound_basics(self):
raw_event = mailgun_sign_legacy_payload({
'token': '06c96bafc3f42a66b9edd546347a2fe18dc23461fe80dc52f0',
'timestamp': '1461261330',
'recipient': 'test@inbound.example.com',
'sender': 'envelope-from@example.org',
'message-headers': json.dumps([
["X-Mailgun-Spam-Rules", "DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, ..."],
["X-Mailgun-Dkim-Check-Result", "Pass"],
["X-Mailgun-Spf", "Pass"],
["X-Mailgun-Sscore", "1.7"],
["X-Mailgun-Sflag", "No"],
["X-Mailgun-Incoming", "Yes"],
["X-Envelope-From", "<envelope-from@example.org>"],
["Received", "from mail.example.org by mxa.mailgun.org ..."],
["Received", "by mail.example.org for <test@inbound.example.com> ..."],
["Dkim-Signature", "v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.org; ..."],
["Mime-Version", "1.0"],
["Received", "by 10.10.1.71 with HTTP; Wed, 11 Oct 2017 18:31:04 -0700 (PDT)"],
["From", "\"Displayed From\" <from+test@example.org>"],
["Date", "Wed, 11 Oct 2017 18:31:04 -0700"],
["Message-Id", "<CAEPk3R+4Zr@mail.example.org>"],
["Subject", "Test subject"],
["To", "\"Test Inbound\" <test@inbound.example.com>, other@example.com"],
["Cc", "cc@example.com"],
["Content-Type", "multipart/mixed; boundary=\"089e0825ccf874a0bb055b4f7e23\""],
]),
'body-plain': 'Test body plain',
'body-html': '<div>Test body html</div>',
'stripped-html': 'stripped html body',
'stripped-text': 'stripped plaintext body',
})
response = self.client.post('/anymail/mailgun/inbound/', data=raw_event)
raw_event = mailgun_sign_legacy_payload(
{
"token": "06c96bafc3f42a66b9edd546347a2fe18dc23461fe80dc52f0",
"timestamp": "1461261330",
"recipient": "test@inbound.example.com",
"sender": "envelope-from@example.org",
"message-headers": json.dumps(
[
[
"X-Mailgun-Spam-Rules",
"DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, ...",
],
["X-Mailgun-Dkim-Check-Result", "Pass"],
["X-Mailgun-Spf", "Pass"],
["X-Mailgun-Sscore", "1.7"],
["X-Mailgun-Sflag", "No"],
["X-Mailgun-Incoming", "Yes"],
["X-Envelope-From", "<envelope-from@example.org>"],
["Received", "from mail.example.org by mxa.mailgun.org ..."],
[
"Received",
"by mail.example.org for <test@inbound.example.com> ...",
],
[
"Dkim-Signature",
"v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.org; ...",
],
["Mime-Version", "1.0"],
[
"Received",
"by 10.10.1.71 with HTTP;"
" Wed, 11 Oct 2017 18:31:04 -0700 (PDT)",
],
["From", '"Displayed From" <from+test@example.org>'],
["Date", "Wed, 11 Oct 2017 18:31:04 -0700"],
["Message-Id", "<CAEPk3R+4Zr@mail.example.org>"],
["Subject", "Test subject"],
[
"To",
'"Test Inbound" <test@inbound.example.com>,'
" other@example.com",
],
["Cc", "cc@example.com"],
[
"Content-Type",
'multipart/mixed; boundary="089e0825ccf874a0bb055b4f7e23"',
],
]
),
"body-plain": "Test body plain",
"body-html": "<div>Test body html</div>",
"stripped-html": "stripped html body",
"stripped-text": "stripped plaintext body",
}
)
response = self.client.post("/anymail/mailgun/inbound/", data=raw_event)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.inbound_handler, sender=MailgunInboundWebhookView,
event=ANY, esp_name='Mailgun')
kwargs = self.assert_handler_called_once_with(
self.inbound_handler,
sender=MailgunInboundWebhookView,
event=ANY,
esp_name="Mailgun",
)
# AnymailInboundEvent
event = kwargs['event']
event = kwargs["event"]
self.assertIsInstance(event, AnymailInboundEvent)
self.assertEqual(event.event_type, 'inbound')
self.assertEqual(event.timestamp, datetime(2016, 4, 21, 17, 55, 30, tzinfo=timezone.utc))
self.assertEqual(event.event_id, "06c96bafc3f42a66b9edd546347a2fe18dc23461fe80dc52f0")
self.assertEqual(event.event_type, "inbound")
self.assertEqual(
event.timestamp, datetime(2016, 4, 21, 17, 55, 30, tzinfo=timezone.utc)
)
self.assertEqual(
event.event_id, "06c96bafc3f42a66b9edd546347a2fe18dc23461fe80dc52f0"
)
self.assertIsInstance(event.message, AnymailInboundMessage)
self.assertEqual(querydict_to_postdict(event.esp_event.POST), raw_event)
# AnymailInboundMessage - convenience properties
message = event.message
self.assertEqual(message.from_email.display_name, 'Displayed From')
self.assertEqual(message.from_email.addr_spec, 'from+test@example.org')
self.assertEqual([str(e) for e in message.to],
['Test Inbound <test@inbound.example.com>', 'other@example.com'])
self.assertEqual([str(e) for e in message.cc],
['cc@example.com'])
self.assertEqual(message.subject, 'Test subject')
self.assertEqual(message.from_email.display_name, "Displayed From")
self.assertEqual(message.from_email.addr_spec, "from+test@example.org")
self.assertEqual(
[str(e) for e in message.to],
["Test Inbound <test@inbound.example.com>", "other@example.com"],
)
self.assertEqual([str(e) for e in message.cc], ["cc@example.com"])
self.assertEqual(message.subject, "Test subject")
self.assertEqual(message.date.isoformat(" "), "2017-10-11 18:31:04-07:00")
self.assertEqual(message.text, 'Test body plain')
self.assertEqual(message.html, '<div>Test body html</div>')
self.assertEqual(message.text, "Test body plain")
self.assertEqual(message.html, "<div>Test body html</div>")
self.assertEqual(message.envelope_sender, 'envelope-from@example.org')
self.assertEqual(message.envelope_recipient, 'test@inbound.example.com')
self.assertEqual(message.stripped_text, 'stripped plaintext body')
self.assertEqual(message.stripped_html, 'stripped html body')
self.assertEqual(message.envelope_sender, "envelope-from@example.org")
self.assertEqual(message.envelope_recipient, "test@inbound.example.com")
self.assertEqual(message.stripped_text, "stripped plaintext body")
self.assertEqual(message.stripped_html, "stripped html body")
self.assertIs(message.spam_detected, False)
self.assertEqual(message.spam_score, 1.7)
# AnymailInboundMessage - other headers
self.assertEqual(message['Message-ID'], "<CAEPk3R+4Zr@mail.example.org>")
self.assertEqual(message.get_all('Received'), [
"from mail.example.org by mxa.mailgun.org ...",
"by mail.example.org for <test@inbound.example.com> ...",
"by 10.10.1.71 with HTTP; Wed, 11 Oct 2017 18:31:04 -0700 (PDT)",
])
self.assertEqual(message["Message-ID"], "<CAEPk3R+4Zr@mail.example.org>")
self.assertEqual(
message.get_all("Received"),
[
"from mail.example.org by mxa.mailgun.org ...",
"by mail.example.org for <test@inbound.example.com> ...",
"by 10.10.1.71 with HTTP; Wed, 11 Oct 2017 18:31:04 -0700 (PDT)",
],
)
def test_attachments(self):
att1 = BytesIO('test attachment'.encode('utf-8'))
att1.name = 'test.txt'
att1 = BytesIO("test attachment".encode("utf-8"))
att1.name = "test.txt"
image_content = sample_image_content()
att2 = BytesIO(image_content)
att2.name = 'image.png'
att2.name = "image.png"
email_content = sample_email_content()
att3 = BytesIO(email_content)
att3.name = '\\share\\mail\\forwarded.msg'
att3.name = "\\share\\mail\\forwarded.msg"
att3.content_type = 'message/rfc822; charset="us-ascii"'
raw_event = mailgun_sign_legacy_payload({
'message-headers': '[]',
'attachment-count': '3',
'content-id-map': """{"<abc123>": "attachment-2"}""",
'attachment-1': att1,
'attachment-2': att2, # inline
'attachment-3': att3,
})
raw_event = mailgun_sign_legacy_payload(
{
"message-headers": "[]",
"attachment-count": "3",
"content-id-map": """{"<abc123>": "attachment-2"}""",
"attachment-1": att1,
"attachment-2": att2, # inline
"attachment-3": att3,
}
)
response = self.client.post('/anymail/mailgun/inbound/', data=raw_event)
response = self.client.post("/anymail/mailgun/inbound/", data=raw_event)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.inbound_handler, sender=MailgunInboundWebhookView,
event=ANY, esp_name='Mailgun')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.inbound_handler,
sender=MailgunInboundWebhookView,
event=ANY,
esp_name="Mailgun",
)
event = kwargs["event"]
message = event.message
attachments = message.attachments # AnymailInboundMessage convenience accessor
self.assertEqual(len(attachments), 2)
self.assertEqual(attachments[0].get_filename(), 'test.txt')
self.assertEqual(attachments[0].get_content_type(), 'text/plain')
self.assertEqual(attachments[0].get_content_text(), 'test attachment')
self.assertEqual(attachments[1].get_filename(), 'forwarded.msg') # Django strips paths
self.assertEqual(attachments[1].get_content_type(), 'message/rfc822')
self.assertEqualIgnoringHeaderFolding(attachments[1].get_content_bytes(), email_content)
self.assertEqual(attachments[0].get_filename(), "test.txt")
self.assertEqual(attachments[0].get_content_type(), "text/plain")
self.assertEqual(attachments[0].get_content_text(), "test attachment")
# Django strips paths:
self.assertEqual(attachments[1].get_filename(), "forwarded.msg")
self.assertEqual(attachments[1].get_content_type(), "message/rfc822")
self.assertEqualIgnoringHeaderFolding(
attachments[1].get_content_bytes(), email_content
)
inlines = message.inline_attachments
self.assertEqual(len(inlines), 1)
inline = inlines['abc123']
self.assertEqual(inline.get_filename(), 'image.png')
self.assertEqual(inline.get_content_type(), 'image/png')
inline = inlines["abc123"]
self.assertEqual(inline.get_filename(), "image.png")
self.assertEqual(inline.get_content_type(), "image/png")
self.assertEqual(inline.get_content_bytes(), image_content)
def test_filtered_attachment_filenames(self):
@@ -141,54 +194,76 @@ class MailgunInboundTestCase(WebhookTestCase):
# Django's multipart/form-data filename filtering. (The attachments are lost,
# but shouldn't cause errors in the inbound webhook.)
filenames = [
"", "path\\", "path/"
".", "path\\.", "path/.",
"..", "path\\..", "path/..",
"",
"path\\",
"path/" ".",
"path\\.",
"path/.",
"..",
"path\\..",
"path/..",
]
num_attachments = len(filenames)
payload = {
"attachment-%d" % (i+1): make_fileobj("content", filename=filenames[i], content_type="text/pdf")
"attachment-%d"
% (i + 1): make_fileobj(
"content", filename=filenames[i], content_type="text/pdf"
)
for i in range(num_attachments)
}
payload.update({
'message-headers': '[]',
'attachment-count': str(num_attachments),
})
payload.update(
{
"message-headers": "[]",
"attachment-count": str(num_attachments),
}
)
# Must do our own multipart/form-data encoding for empty filenames:
response = self.client.post('/anymail/mailgun/inbound/',
data=encode_multipart("BoUnDaRy", mailgun_sign_legacy_payload(payload)),
content_type="multipart/form-data; boundary=BoUnDaRy")
response = self.client.post(
"/anymail/mailgun/inbound/",
data=encode_multipart("BoUnDaRy", mailgun_sign_legacy_payload(payload)),
content_type="multipart/form-data; boundary=BoUnDaRy",
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.inbound_handler, sender=MailgunInboundWebhookView,
event=ANY, esp_name='Mailgun')
kwargs = self.assert_handler_called_once_with(
self.inbound_handler,
sender=MailgunInboundWebhookView,
event=ANY,
esp_name="Mailgun",
)
# Different Django releases strip different filename patterns.
# Just verify that at least some attachments got dropped (so the test is valid)
# without causing an error in the inbound webhook:
attachments = kwargs['event'].message.attachments
attachments = kwargs["event"].message.attachments
self.assertLess(len(attachments), num_attachments)
def test_unusual_content_id_map(self):
# Under unknown conditions, Mailgun appears to generate a content-id-map with multiple
# empty keys (and possibly other duplicate keys). We still want to correctly identify
# inline attachments from it.
raw_event = mailgun_sign_legacy_payload({
'message-headers': '[]',
'attachment-count': '4',
'content-id-map': '{"": "attachment-1", "": "attachment-2",'
' "<abc>": "attachment-3", "<abc>": "attachment-4"}',
'attachment-1': make_fileobj("att1"),
'attachment-2': make_fileobj("att2"),
'attachment-3': make_fileobj("att3"),
'attachment-4': make_fileobj("att4"),
})
# Under unknown conditions, Mailgun appears to generate a content-id-map with
# multiple empty keys (and possibly other duplicate keys). We still want to
# correctly identify inline attachments from it.
raw_event = mailgun_sign_legacy_payload(
{
"message-headers": "[]",
"attachment-count": "4",
"content-id-map": '{"": "attachment-1", "": "attachment-2",'
' "<abc>": "attachment-3", "<abc>": "attachment-4"}',
"attachment-1": make_fileobj("att1"),
"attachment-2": make_fileobj("att2"),
"attachment-3": make_fileobj("att3"),
"attachment-4": make_fileobj("att4"),
}
)
response = self.client.post('/anymail/mailgun/inbound/', data=raw_event)
response = self.client.post("/anymail/mailgun/inbound/", data=raw_event)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.inbound_handler, sender=MailgunInboundWebhookView,
event=ANY, esp_name='Mailgun')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.inbound_handler,
sender=MailgunInboundWebhookView,
event=ANY,
esp_name="Mailgun",
)
event = kwargs["event"]
message = event.message
self.assertEqual(len(message.attachments), 0) # all inlines
inlines = [part for part in message.walk() if part.is_inline_attachment()]
@@ -200,12 +275,14 @@ class MailgunInboundTestCase(WebhookTestCase):
def test_inbound_mime(self):
# Mailgun provides the full, raw MIME message if the webhook url ends in 'mime'
raw_event = mailgun_sign_legacy_payload({
'token': '06c96bafc3f42a66b9edd546347a2fe18dc23461fe80dc52f0',
'timestamp': '1461261330',
'recipient': 'test@inbound.example.com',
'sender': 'envelope-from@example.org',
'body-mime': dedent("""\
raw_event = mailgun_sign_legacy_payload(
{
"token": "06c96bafc3f42a66b9edd546347a2fe18dc23461fe80dc52f0",
"timestamp": "1461261330",
"recipient": "test@inbound.example.com",
"sender": "envelope-from@example.org",
"body-mime": dedent(
"""\
From: A tester <test@example.org>
Date: Thu, 12 Oct 2017 18:03:30 -0700
Message-ID: <CAEPk3RKEx@mail.example.org>
@@ -227,71 +304,95 @@ class MailgunInboundTestCase(WebhookTestCase):
<div dir=3D"ltr">It's a body=E2=80=A6</div>
--94eb2c05e174adb140055b6339c5--
"""),
})
""" # NOQA: E501
),
}
)
response = self.client.post('/anymail/mailgun/inbound_mime/', data=raw_event)
response = self.client.post("/anymail/mailgun/inbound_mime/", data=raw_event)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.inbound_handler, sender=MailgunInboundWebhookView,
event=ANY, esp_name='Mailgun')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.inbound_handler,
sender=MailgunInboundWebhookView,
event=ANY,
esp_name="Mailgun",
)
event = kwargs["event"]
message = event.message
self.assertEqual(message.envelope_sender, 'envelope-from@example.org')
self.assertEqual(message.envelope_recipient, 'test@inbound.example.com')
self.assertEqual(message.subject, 'Raw MIME test')
self.assertEqual(message.envelope_sender, "envelope-from@example.org")
self.assertEqual(message.envelope_recipient, "test@inbound.example.com")
self.assertEqual(message.subject, "Raw MIME test")
self.assertEqual(message.text, "It's a body\N{HORIZONTAL ELLIPSIS}\n")
self.assertEqual(message.html, """<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\n""")
self.assertEqual(
message.html,
"""<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\n""",
)
def test_misconfigured_tracking(self):
raw_event = mailgun_sign_payload({
"event-data": {
"event": "clicked",
"timestamp": 1534109600.089676,
"recipient": "recipient@example.com",
"url": "https://example.com/test"
raw_event = mailgun_sign_payload(
{
"event-data": {
"event": "clicked",
"timestamp": 1534109600.089676,
"recipient": "recipient@example.com",
"url": "https://example.com/test",
}
}
})
)
with self.assertRaisesMessage(
AnymailConfigurationError,
"You seem to have set Mailgun's *clicked tracking* webhook"
" to Anymail's Mailgun *inbound* webhook URL."
" to Anymail's Mailgun *inbound* webhook URL.",
):
self.client.post('/anymail/mailgun/inbound/',
data=json.dumps(raw_event), content_type='application/json')
self.client.post(
"/anymail/mailgun/inbound/",
data=json.dumps(raw_event),
content_type="application/json",
)
def test_misconfigured_store_action(self):
# store() notification includes "attachments" json; forward() includes "attachment-count"
raw_event = mailgun_sign_legacy_payload({
'token': '06c96bafc3f42a66b9edd546347a2fe18dc23461fe80dc52f0',
'timestamp': '1461261330',
'recipient': 'test@inbound.example.com',
'sender': 'envelope-from@example.org',
'body-plain': 'Test body plain',
'body-html': '<div>Test body html</div>',
'attachments': json.dumps([{
"url": "https://storage.mailgun.net/v3/domains/example.com/messages/MESSAGE_KEY/attachments/0",
"content-type": "application/pdf",
"name": "attachment.pdf",
"size": 20202
}]),
})
# store() notification includes "attachments" json;
# forward() includes "attachment-count"
raw_event = mailgun_sign_legacy_payload(
{
"token": "06c96bafc3f42a66b9edd546347a2fe18dc23461fe80dc52f0",
"timestamp": "1461261330",
"recipient": "test@inbound.example.com",
"sender": "envelope-from@example.org",
"body-plain": "Test body plain",
"body-html": "<div>Test body html</div>",
"attachments": json.dumps(
[
{
"url": "https://storage.mailgun.net/v3/domains/example.com"
"/messages/MESSAGE_KEY/attachments/0",
"content-type": "application/pdf",
"name": "attachment.pdf",
"size": 20202,
}
]
),
}
)
with self.assertRaisesMessage(
AnymailConfigurationError,
"You seem to have configured Mailgun's receiving route using the store() action."
" Anymail's inbound webhook requires the forward() action."
"You seem to have configured Mailgun's receiving route using the store()"
" action. Anymail's inbound webhook requires the forward() action.",
):
self.client.post('/anymail/mailgun/inbound/', data=raw_event)
self.client.post("/anymail/mailgun/inbound/", data=raw_event)
def test_misconfigured_tracking_legacy(self):
raw_event = mailgun_sign_legacy_payload({
'domain': 'example.com',
'message-headers': '[]',
'recipient': 'recipient@example.com',
'event': 'delivered',
})
raw_event = mailgun_sign_legacy_payload(
{
"domain": "example.com",
"message-headers": "[]",
"recipient": "recipient@example.com",
"event": "delivered",
}
)
with self.assertRaisesMessage(
AnymailConfigurationError,
"You seem to have set Mailgun's *delivered tracking* webhook"
" to Anymail's Mailgun *inbound* webhook URL."
" to Anymail's Mailgun *inbound* webhook URL.",
):
self.client.post('/anymail/mailgun/inbound/', data=raw_event)
self.client.post("/anymail/mailgun/inbound/", data=raw_event)

View File

@@ -10,21 +10,27 @@ from django.test import SimpleTestCase, override_settings, tag
from anymail.exceptions import AnymailAPIError
from anymail.message import AnymailMessage
from .utils import AnymailTestMixin, sample_image_path
ANYMAIL_TEST_MAILGUN_API_KEY = os.getenv('ANYMAIL_TEST_MAILGUN_API_KEY')
ANYMAIL_TEST_MAILGUN_DOMAIN = os.getenv('ANYMAIL_TEST_MAILGUN_DOMAIN')
ANYMAIL_TEST_MAILGUN_API_KEY = os.getenv("ANYMAIL_TEST_MAILGUN_API_KEY")
ANYMAIL_TEST_MAILGUN_DOMAIN = os.getenv("ANYMAIL_TEST_MAILGUN_DOMAIN")
@tag('mailgun', 'live')
@unittest.skipUnless(ANYMAIL_TEST_MAILGUN_API_KEY and ANYMAIL_TEST_MAILGUN_DOMAIN,
"Set ANYMAIL_TEST_MAILGUN_API_KEY and ANYMAIL_TEST_MAILGUN_DOMAIN environment variables "
"to run Mailgun integration tests")
@override_settings(ANYMAIL={'MAILGUN_API_KEY': ANYMAIL_TEST_MAILGUN_API_KEY,
'MAILGUN_SENDER_DOMAIN': ANYMAIL_TEST_MAILGUN_DOMAIN,
'MAILGUN_SEND_DEFAULTS': {'esp_extra': {'o:testmode': 'yes'}}},
EMAIL_BACKEND="anymail.backends.mailgun.EmailBackend")
@tag("mailgun", "live")
@unittest.skipUnless(
ANYMAIL_TEST_MAILGUN_API_KEY and ANYMAIL_TEST_MAILGUN_DOMAIN,
"Set ANYMAIL_TEST_MAILGUN_API_KEY and ANYMAIL_TEST_MAILGUN_DOMAIN environment"
" variables to run Mailgun integration tests",
)
@override_settings(
ANYMAIL={
"MAILGUN_API_KEY": ANYMAIL_TEST_MAILGUN_API_KEY,
"MAILGUN_SENDER_DOMAIN": ANYMAIL_TEST_MAILGUN_DOMAIN,
"MAILGUN_SEND_DEFAULTS": {"esp_extra": {"o:testmode": "yes"}},
},
EMAIL_BACKEND="anymail.backends.mailgun.EmailBackend",
)
class MailgunBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
"""Mailgun API integration tests
@@ -37,13 +43,18 @@ class MailgunBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
def setUp(self):
super().setUp()
self.from_email = 'from@%s' % ANYMAIL_TEST_MAILGUN_DOMAIN
self.message = AnymailMessage('Anymail Mailgun integration test', 'Text content',
self.from_email, ['test+to1@anymail.dev'])
self.message.attach_alternative('<p>HTML content</p>', "text/html")
self.from_email = "from@%s" % ANYMAIL_TEST_MAILGUN_DOMAIN
self.message = AnymailMessage(
"Anymail Mailgun integration test",
"Text content",
self.from_email,
["test+to1@anymail.dev"],
)
self.message.attach_alternative("<p>HTML content</p>", "text/html")
def fetch_mailgun_events(self, message_id, event=None,
initial_delay=2, retry_delay=2, max_retries=5):
def fetch_mailgun_events(
self, message_id, event=None, initial_delay=2, retry_delay=2, max_retries=5
):
"""Return list of Mailgun events related to message_id"""
url = "https://api.mailgun.net/v3/%s/events" % ANYMAIL_TEST_MAILGUN_DOMAIN
auth = ("api", ANYMAIL_TEST_MAILGUN_API_KEY)
@@ -51,9 +62,9 @@ class MailgunBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
# Despite the docs, Mailgun's events API actually expects the message-id
# without the <...> brackets (so, not exactly "as returned by the messages API")
# https://documentation.mailgun.com/api-events.html#filter-field
params = {'message-id': message_id[1:-1]} # strip <...>
params = {"message-id": message_id[1:-1]} # strip <...>
if event is not None:
params['event'] = event
params["event"] = event
# It can take a few seconds for the events to show up
# in Mailgun's logs, so retry a few times if necessary:
@@ -72,14 +83,17 @@ class MailgunBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
# server error (hopefully transient); try again after delay
pass
elif 403 == response.status_code:
# "forbidden": this may be related to API throttling; try again after delay
# "forbidden": this may be related to API throttling;
# try again after delay
pass
else:
response.raise_for_status()
# Max retries exceeded:
if response is not None and 200 != response.status_code:
logging.warning("Ignoring Mailgun events API error %d:\n%s"
% (response.status_code, response.text))
logging.warning(
"Ignoring Mailgun events API error %d:\n%s"
% (response.status_code, response.text)
)
return None
def test_simple_send(self):
@@ -88,13 +102,15 @@ class MailgunBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
self.assertEqual(sent_count, 1)
anymail_status = self.message.anymail_status
sent_status = anymail_status.recipients['test+to1@anymail.dev'].status
message_id = anymail_status.recipients['test+to1@anymail.dev'].message_id
sent_status = anymail_status.recipients["test+to1@anymail.dev"].status
message_id = anymail_status.recipients["test+to1@anymail.dev"].message_id
self.assertEqual(sent_status, 'queued') # Mailgun always queues
self.assertGreater(len(message_id), 0) # don't know what it'll be, but it should exist
self.assertEqual(sent_status, "queued") # Mailgun always queues
# don't know what it'll be, but it should exist:
self.assertGreater(len(message_id), 0)
self.assertEqual(anymail_status.status, {sent_status}) # set of all recipient statuses
# set of all recipient statuses:
self.assertEqual(anymail_status.status, {sent_status})
self.assertEqual(anymail_status.message_id, message_id)
def test_all_options(self):
@@ -110,7 +126,6 @@ class MailgunBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
bcc=["test+bcc1@anymail.dev", "Blind Copy 2 <test+bcc2@anymail.dev>"],
reply_to=["reply1@example.com", "Reply 2 <reply2@example.com>"],
headers={"X-Anymail-Test": "value"},
metadata={"meta1": "simple string", "meta2": 2},
send_at=send_at,
tags=["tag 1", "tag 2"],
@@ -119,35 +134,55 @@ class MailgunBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
)
message.attach("attachment1.txt", "Here is some\ntext for you", "text/plain")
message.attach("vedhæftet fil.csv", "ID,Name\n1,3", "text/csv")
cid = message.attach_inline_image_file(sample_image_path(), domain=ANYMAIL_TEST_MAILGUN_DOMAIN)
cid = message.attach_inline_image_file(
sample_image_path(), domain=ANYMAIL_TEST_MAILGUN_DOMAIN
)
message.attach_alternative(
"<div>This is the <i>html</i> body <img src='cid:%s'></div>" % cid,
"text/html")
"text/html",
)
message.send()
self.assertEqual(message.anymail_status.status, {'queued'}) # Mailgun always queues
# Mailgun always queues:
self.assertEqual(message.anymail_status.status, {"queued"})
message_id = message.anymail_status.message_id
events = self.fetch_mailgun_events(message_id, event="accepted")
if events is None:
self.skipTest("No Mailgun 'accepted' event after 30sec -- can't complete this test")
self.skipTest(
"No Mailgun 'accepted' event after 30sec -- can't complete this test"
)
return
event = events.pop()
self.assertCountEqual(event["tags"], ["tag 1", "tag 2"]) # don't care about order
self.assertEqual(event["user-variables"],
{"meta1": "simple string", "meta2": "2"}) # all metadata values become strings
# don't care about order:
self.assertCountEqual(event["tags"], ["tag 1", "tag 2"])
# all metadata values become strings:
self.assertEqual(
event["user-variables"], {"meta1": "simple string", "meta2": "2"}
)
self.assertEqual(event["message"]["scheduled-for"], send_at_timestamp)
self.assertIn(event["recipient"], ['test+to1@anymail.dev', 'test+to2@anymail.dev',
'test+cc1@anymail.dev', 'test+cc2@anymail.dev',
'test+bcc1@anymail.dev', 'test+bcc2@anymail.dev'])
self.assertIn(
event["recipient"],
[
"test+to1@anymail.dev",
"test+to2@anymail.dev",
"test+cc1@anymail.dev",
"test+cc2@anymail.dev",
"test+bcc1@anymail.dev",
"test+bcc2@anymail.dev",
],
)
headers = event["message"]["headers"]
self.assertEqual(headers["from"], from_email)
self.assertEqual(headers["to"],
"test+to1@anymail.dev, Recipient 2 <test+to2@anymail.dev>")
self.assertEqual(headers["subject"], "Anymail Mailgun all-options integration test")
self.assertEqual(
headers["to"], "test+to1@anymail.dev, Recipient 2 <test+to2@anymail.dev>"
)
self.assertEqual(
headers["subject"], "Anymail Mailgun all-options integration test"
)
attachments = event["message"]["attachments"]
if len(attachments) == 3:
@@ -168,24 +203,28 @@ class MailgunBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
def test_stored_template(self):
message = AnymailMessage(
template_id='test-template', # name of a real template named in Anymail's Mailgun test account
subject='Your order %recipient.order%', # Mailgun templates don't define subject
from_email=formataddr(('Test From>', self.from_email)), # Mailgun templates don't define sender
# name of a real template named in Anymail's Mailgun test account:
template_id="test-template",
# Mailgun templates don't define subject:
subject="Your order %recipient.order%",
# Mailgun templates don't define sender:
from_email=formataddr(("Test From>", self.from_email)),
to=["test+to1@anymail.dev"],
# metadata and merge_data must not have any conflicting keys when using template_id
# metadata and merge_data must not have any conflicting keys
# when using template_id:
metadata={"meta1": "simple string", "meta2": 2},
merge_data={
'test+to1@anymail.dev': {
'name': "Test Recipient",
"test+to1@anymail.dev": {
"name": "Test Recipient",
}
},
merge_global_data={
'order': '12345',
"order": "12345",
},
)
message.send()
recipient_status = message.anymail_status.recipients
self.assertEqual(recipient_status['test+to1@anymail.dev'].status, 'queued')
self.assertEqual(recipient_status["test+to1@anymail.dev"].status, "queued")
# As of Anymail 0.10, this test is no longer possible, because
# Anymail now raises AnymailInvalidAddress without even calling Mailgun
@@ -197,9 +236,13 @@ class MailgunBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
# self.assertEqual(err.status_code, 400)
# self.assertIn("'from' parameter is not a valid address", str(err))
@override_settings(ANYMAIL={'MAILGUN_API_KEY': "Hey, that's not an API key",
'MAILGUN_SENDER_DOMAIN': ANYMAIL_TEST_MAILGUN_DOMAIN,
'MAILGUN_SEND_DEFAULTS': {'esp_extra': {'o:testmode': 'yes'}}})
@override_settings(
ANYMAIL={
"MAILGUN_API_KEY": "Hey, that's not an API key",
"MAILGUN_SENDER_DOMAIN": ANYMAIL_TEST_MAILGUN_DOMAIN,
"MAILGUN_SEND_DEFAULTS": {"esp_extra": {"o:testmode": "yes"}},
}
)
def test_invalid_api_key(self):
with self.assertRaises(AnymailAPIError) as cm:
self.message.send()

File diff suppressed because it is too large Load Diff

View File

@@ -8,19 +8,34 @@ from django.core import mail
from django.core.exceptions import ImproperlyConfigured
from django.test import SimpleTestCase, override_settings, tag
from anymail.exceptions import AnymailAPIError, AnymailSerializationError, AnymailUnsupportedFeature
from anymail.exceptions import (
AnymailAPIError,
AnymailSerializationError,
AnymailUnsupportedFeature,
)
from anymail.message import attach_inline_image_file
from .mock_requests_backend import RequestsBackendMockAPITestCase, SessionSharingTestCases
from .utils import sample_image_content, sample_image_path, SAMPLE_IMAGE_FILENAME, AnymailTestMixin, decode_att
from .mock_requests_backend import (
RequestsBackendMockAPITestCase,
SessionSharingTestCases,
)
from .utils import (
SAMPLE_IMAGE_FILENAME,
AnymailTestMixin,
decode_att,
sample_image_content,
sample_image_path,
)
@tag('mailjet')
@override_settings(EMAIL_BACKEND='anymail.backends.mailjet.EmailBackend',
ANYMAIL={
'MAILJET_API_KEY': 'API KEY HERE',
'MAILJET_SECRET_KEY': 'SECRET KEY HERE'
})
@tag("mailjet")
@override_settings(
EMAIL_BACKEND="anymail.backends.mailjet.EmailBackend",
ANYMAIL={
"MAILJET_API_KEY": "API KEY HERE",
"MAILJET_SECRET_KEY": "SECRET KEY HERE",
},
)
class MailjetBackendMockAPITestCase(RequestsBackendMockAPITestCase):
DEFAULT_RAW_RESPONSE = b"""{
"Messages": [{
@@ -37,29 +52,36 @@ class MailjetBackendMockAPITestCase(RequestsBackendMockAPITestCase):
def setUp(self):
super().setUp()
# Simple message useful for many tests
self.message = mail.EmailMultiAlternatives('Subject', 'Text Body', 'from@example.com', ['to@example.com'])
self.message = mail.EmailMultiAlternatives(
"Subject", "Text Body", "from@example.com", ["to@example.com"]
)
@tag('mailjet')
@tag("mailjet")
class MailjetBackendStandardEmailTests(MailjetBackendMockAPITestCase):
"""Test backend support for Django standard email features"""
def test_send_mail(self):
"""Test basic API for simple send"""
mail.send_mail('Subject here', 'Here is the message.',
'from@sender.example.com', ['to@example.com'], fail_silently=False)
self.assert_esp_called('/v3.1/send')
mail.send_mail(
"Subject here",
"Here is the message.",
"from@sender.example.com",
["to@example.com"],
fail_silently=False,
)
self.assert_esp_called("/v3.1/send")
auth = self.get_api_call_auth()
self.assertEqual(auth, ('API KEY HERE', 'SECRET KEY HERE'))
self.assertEqual(auth, ("API KEY HERE", "SECRET KEY HERE"))
data = self.get_api_call_json()
self.assertEqual(len(data['Messages']), 1)
message = data['Messages'][0]
self.assertEqual(data['Globals']['Subject'], "Subject here")
self.assertEqual(data['Globals']['TextPart'], "Here is the message.")
self.assertEqual(data['Globals']['From'], {"Email": "from@sender.example.com"})
self.assertEqual(message['To'], [{"Email": "to@example.com"}])
self.assertEqual(len(data["Messages"]), 1)
message = data["Messages"][0]
self.assertEqual(data["Globals"]["Subject"], "Subject here")
self.assertEqual(data["Globals"]["TextPart"], "Here is the message.")
self.assertEqual(data["Globals"]["From"], {"Email": "from@sender.example.com"})
self.assertEqual(message["To"], [{"Email": "to@example.com"}])
def test_name_addr(self):
"""Make sure RFC2822 name-addr format (with display-name) is allowed
@@ -67,101 +89,153 @@ class MailjetBackendStandardEmailTests(MailjetBackendMockAPITestCase):
(Test both sender and recipient addresses)
"""
msg = mail.EmailMessage(
'Subject', 'Message', 'From Name <from@example.com>',
['"Recipient, #1" <to1@example.com>', 'to2@example.com'],
cc=['Carbon Copy <cc1@example.com>', 'cc2@example.com'],
bcc=['Blind Copy <bcc1@example.com>', 'bcc2@example.com'])
"Subject",
"Message",
"From Name <from@example.com>",
['"Recipient, #1" <to1@example.com>', "to2@example.com"],
cc=["Carbon Copy <cc1@example.com>", "cc2@example.com"],
bcc=["Blind Copy <bcc1@example.com>", "bcc2@example.com"],
)
msg.send()
data = self.get_api_call_json()
self.assertEqual(len(data['Messages']), 1)
message = data['Messages'][0]
self.assertEqual(data['Globals']['From'], {"Email": "from@example.com", "Name": "From Name"})
self.assertEqual(message['To'], [{"Email": "to1@example.com", "Name": "Recipient, #1"},
{"Email": "to2@example.com"}])
self.assertEqual(data['Globals']['Cc'], [{"Email": "cc1@example.com", "Name": "Carbon Copy"},
{"Email": "cc2@example.com"}])
self.assertEqual(data['Globals']['Bcc'], [{"Email": "bcc1@example.com", "Name": "Blind Copy"},
{"Email": "bcc2@example.com"}])
self.assertEqual(len(data["Messages"]), 1)
message = data["Messages"][0]
self.assertEqual(
data["Globals"]["From"], {"Email": "from@example.com", "Name": "From Name"}
)
self.assertEqual(
message["To"],
[
{"Email": "to1@example.com", "Name": "Recipient, #1"},
{"Email": "to2@example.com"},
],
)
self.assertEqual(
data["Globals"]["Cc"],
[
{"Email": "cc1@example.com", "Name": "Carbon Copy"},
{"Email": "cc2@example.com"},
],
)
self.assertEqual(
data["Globals"]["Bcc"],
[
{"Email": "bcc1@example.com", "Name": "Blind Copy"},
{"Email": "bcc2@example.com"},
],
)
def test_email_message(self):
email = mail.EmailMessage(
'Subject', 'Body goes here', 'from@example.com',
['to1@example.com', 'Also To <to2@example.com>'],
bcc=['bcc1@example.com', 'Also BCC <bcc2@example.com>'],
cc=['cc1@example.com', 'Also CC <cc2@example.com>'],
headers={'Reply-To': 'another@example.com',
'X-MyHeader': 'my value'})
"Subject",
"Body goes here",
"from@example.com",
["to1@example.com", "Also To <to2@example.com>"],
bcc=["bcc1@example.com", "Also BCC <bcc2@example.com>"],
cc=["cc1@example.com", "Also CC <cc2@example.com>"],
headers={"Reply-To": "another@example.com", "X-MyHeader": "my value"},
)
email.send()
data = self.get_api_call_json()
self.assertEqual(len(data['Messages']), 1)
message = data['Messages'][0]
self.assertEqual(data['Globals']['Subject'], "Subject")
self.assertEqual(data['Globals']['TextPart'], "Body goes here")
self.assertEqual(data['Globals']['From'], {"Email": "from@example.com"})
self.assertEqual(message['To'], [{"Email": "to1@example.com"},
{"Email": "to2@example.com", "Name": "Also To"}])
self.assertEqual(data['Globals']['Cc'], [{"Email": "cc1@example.com"},
{"Email": "cc2@example.com", "Name": "Also CC"}])
self.assertEqual(data['Globals']['Bcc'], [{"Email": "bcc1@example.com"},
{"Email": "bcc2@example.com", "Name": "Also BCC"}])
self.assertEqual(data['Globals']['Headers'],
{'X-MyHeader': 'my value'}) # Reply-To should be moved to own param
self.assertEqual(data['Globals']['ReplyTo'], {"Email": "another@example.com"})
self.assertEqual(len(data["Messages"]), 1)
message = data["Messages"][0]
self.assertEqual(data["Globals"]["Subject"], "Subject")
self.assertEqual(data["Globals"]["TextPart"], "Body goes here")
self.assertEqual(data["Globals"]["From"], {"Email": "from@example.com"})
self.assertEqual(
message["To"],
[
{"Email": "to1@example.com"},
{"Email": "to2@example.com", "Name": "Also To"},
],
)
self.assertEqual(
data["Globals"]["Cc"],
[
{"Email": "cc1@example.com"},
{"Email": "cc2@example.com", "Name": "Also CC"},
],
)
self.assertEqual(
data["Globals"]["Bcc"],
[
{"Email": "bcc1@example.com"},
{"Email": "bcc2@example.com", "Name": "Also BCC"},
],
)
# Reply-To should be moved to own param:
self.assertEqual(data["Globals"]["Headers"], {"X-MyHeader": "my value"})
self.assertEqual(data["Globals"]["ReplyTo"], {"Email": "another@example.com"})
def test_html_message(self):
text_content = 'This is an important message.'
html_content = '<p>This is an <strong>important</strong> message.</p>'
email = mail.EmailMultiAlternatives('Subject', text_content,
'from@example.com', ['to@example.com'])
text_content = "This is an important message."
html_content = "<p>This is an <strong>important</strong> message.</p>"
email = mail.EmailMultiAlternatives(
"Subject", text_content, "from@example.com", ["to@example.com"]
)
email.attach_alternative(html_content, "text/html")
email.send()
data = self.get_api_call_json()
self.assertEqual(len(data['Messages']), 1)
self.assertEqual(data['Globals']['TextPart'], text_content)
self.assertEqual(data['Globals']['HTMLPart'], html_content)
self.assertEqual(len(data["Messages"]), 1)
self.assertEqual(data["Globals"]["TextPart"], text_content)
self.assertEqual(data["Globals"]["HTMLPart"], html_content)
# Don't accidentally send the html part as an attachment:
self.assertNotIn('Attachments', data['Globals'])
self.assertNotIn("Attachments", data["Globals"])
def test_html_only_message(self):
html_content = '<p>This is an <strong>important</strong> message.</p>'
email = mail.EmailMessage('Subject', html_content, 'from@example.com', ['to@example.com'])
html_content = "<p>This is an <strong>important</strong> message.</p>"
email = mail.EmailMessage(
"Subject", html_content, "from@example.com", ["to@example.com"]
)
email.content_subtype = "html" # Main content is now text/html
email.send()
data = self.get_api_call_json()
self.assertNotIn('TextPart', data['Globals'])
self.assertEqual(data['Globals']['HTMLPart'], html_content)
self.assertNotIn("TextPart", data["Globals"])
self.assertEqual(data["Globals"]["HTMLPart"], html_content)
def test_extra_headers(self):
self.message.extra_headers = {'X-Custom': 'string', 'X-Num': 123}
self.message.extra_headers = {"X-Custom": "string", "X-Num": 123}
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['Globals']['Headers'], {
'X-Custom': 'string',
'X-Num': 123,
})
self.assertEqual(
data["Globals"]["Headers"],
{
"X-Custom": "string",
"X-Num": 123,
},
)
def test_extra_headers_serialization_error(self):
self.message.extra_headers = {'X-Custom': Decimal(12.5)}
self.message.extra_headers = {"X-Custom": Decimal(12.5)}
with self.assertRaisesMessage(AnymailSerializationError, "Decimal"):
self.message.send()
@override_settings(ANYMAIL_IGNORE_UNSUPPORTED_FEATURES=True) # Mailjet only allows single reply-to
@override_settings(ANYMAIL_IGNORE_UNSUPPORTED_FEATURES=True)
def test_reply_to(self):
email = mail.EmailMessage('Subject', 'Body goes here', 'from@example.com', ['to1@example.com'],
reply_to=['reply@example.com', 'Other <reply2@example.com>'],
headers={'X-Other': 'Keep'})
# Mailjet only allows single reply-to. Verify correct handling for that
# when ignoring unsupported features.
email = mail.EmailMessage(
"Subject",
"Body goes here",
"from@example.com",
["to1@example.com"],
reply_to=["reply@example.com", "Other <reply2@example.com>"],
headers={"X-Other": "Keep"},
)
email.send()
data = self.get_api_call_json()
self.assertEqual(data['Globals']['ReplyTo'], {"Email": "reply@example.com"}) # only the first reply_to
self.assertEqual(data['Globals']['Headers'], {
'X-Other': 'Keep'
}) # don't lose other headers
# only the first reply_to:
self.assertEqual(data["Globals"]["ReplyTo"], {"Email": "reply@example.com"})
# don't lose other headers:
self.assertEqual(data["Globals"]["Headers"], {"X-Other": "Keep"})
def test_attachments(self):
text_content = "* Item one\n* Item two\n* Item three"
self.message.attach(filename="test.txt", content=text_content, mimetype="text/plain")
self.message.attach(
filename="test.txt", content=text_content, mimetype="text/plain"
)
# Should guess mimetype if not provided...
png_content = b"PNG\xb4 pretend this is the contents of a png file"
@@ -169,40 +243,53 @@ class MailjetBackendStandardEmailTests(MailjetBackendMockAPITestCase):
# Should work with a MIMEBase object (also tests no filename)...
pdf_content = b"PDF\xb4 pretend this is valid pdf data"
mimeattachment = MIMEBase('application', 'pdf')
mimeattachment = MIMEBase("application", "pdf")
mimeattachment.set_payload(pdf_content)
self.message.attach(mimeattachment)
self.message.send()
data = self.get_api_call_json()
attachments = data['Globals']['Attachments']
attachments = data["Globals"]["Attachments"]
self.assertEqual(len(attachments), 3)
self.assertEqual(attachments[0]["Filename"], "test.txt")
self.assertEqual(attachments[0]["ContentType"], "text/plain")
self.assertEqual(decode_att(attachments[0]["Base64Content"]).decode('ascii'), text_content)
self.assertNotIn('ContentID', attachments[0])
self.assertEqual(
decode_att(attachments[0]["Base64Content"]).decode("ascii"), text_content
)
self.assertNotIn("ContentID", attachments[0])
self.assertEqual(attachments[1]["ContentType"], "image/png") # inferred from filename
# inferred from filename:
self.assertEqual(attachments[1]["ContentType"], "image/png")
self.assertEqual(attachments[1]["Filename"], "test.png")
self.assertEqual(decode_att(attachments[1]["Base64Content"]), png_content)
self.assertNotIn('ContentID', attachments[1]) # make sure image not treated as inline
# make sure image not treated as inline:
self.assertNotIn("ContentID", attachments[1])
self.assertEqual(attachments[2]["ContentType"], "application/pdf")
self.assertEqual(attachments[2]["Filename"], "") # none
self.assertEqual(decode_att(attachments[2]["Base64Content"]), pdf_content)
self.assertNotIn('ContentID', attachments[2])
self.assertNotIn("ContentID", attachments[2])
self.assertNotIn('InlinedAttachments', data['Globals'])
self.assertNotIn("InlinedAttachments", data["Globals"])
def test_unicode_attachment_correctly_decoded(self):
self.message.attach("Une pièce jointe.html", '<p>\u2019</p>', mimetype='text/html')
self.message.attach(
"Une pièce jointe.html", "<p>\u2019</p>", mimetype="text/html"
)
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['Globals']['Attachments'], [{
'Filename': 'Une pièce jointe.html',
'ContentType': 'text/html',
'Base64Content': b64encode('<p>\u2019</p>'.encode('utf-8')).decode('ascii')
}])
self.assertEqual(
data["Globals"]["Attachments"],
[
{
"Filename": "Une pièce jointe.html",
"ContentType": "text/html",
"Base64Content": b64encode("<p>\u2019</p>".encode("utf-8")).decode(
"ascii"
),
}
],
)
def test_embedded_images(self):
image_filename = SAMPLE_IMAGE_FILENAME
@@ -210,48 +297,55 @@ class MailjetBackendStandardEmailTests(MailjetBackendMockAPITestCase):
image_data = sample_image_content(image_filename)
cid = attach_inline_image_file(self.message, image_path) # Read from a png file
html_content = '<p>This has an <img src="cid:%s" alt="inline" /> image.</p>' % cid
html_content = (
'<p>This has an <img src="cid:%s" alt="inline" /> image.</p>' % cid
)
self.message.attach_alternative(html_content, "text/html")
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['Globals']['HTMLPart'], html_content)
self.assertEqual(data["Globals"]["HTMLPart"], html_content)
attachments = data['Globals']['InlinedAttachments']
attachments = data["Globals"]["InlinedAttachments"]
self.assertEqual(len(attachments), 1)
self.assertEqual(attachments[0]['Filename'], image_filename)
self.assertEqual(attachments[0]['ContentID'], cid)
self.assertEqual(attachments[0]['ContentType'], 'image/png')
self.assertEqual(attachments[0]["Filename"], image_filename)
self.assertEqual(attachments[0]["ContentID"], cid)
self.assertEqual(attachments[0]["ContentType"], "image/png")
self.assertEqual(decode_att(attachments[0]["Base64Content"]), image_data)
self.assertNotIn('Attachments', data['Globals'])
self.assertNotIn("Attachments", data["Globals"])
def test_attached_images(self):
image_filename = SAMPLE_IMAGE_FILENAME
image_path = sample_image_path(image_filename)
image_data = sample_image_content(image_filename)
self.message.attach_file(image_path) # option 1: attach as a file
# option 1: attach as a file:
self.message.attach_file(image_path)
image = MIMEImage(image_data) # option 2: construct the MIMEImage and attach it directly
# option 2: construct the MIMEImage and attach it directly:
image = MIMEImage(image_data)
self.message.attach(image)
image_data_b64 = b64encode(image_data).decode('ascii')
image_data_b64 = b64encode(image_data).decode("ascii")
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['Globals']['Attachments'], [
{
'Filename': image_filename, # the named one
'ContentType': 'image/png',
'Base64Content': image_data_b64,
},
{
'Filename': '', # the unnamed one
'ContentType': 'image/png',
'Base64Content': image_data_b64,
},
])
self.assertEqual(
data["Globals"]["Attachments"],
[
{
"Filename": image_filename, # the named one
"ContentType": "image/png",
"Base64Content": image_data_b64,
},
{
"Filename": "", # the unnamed one
"ContentType": "image/png",
"Base64Content": image_data_b64,
},
],
)
def test_multiple_html_alternatives(self):
# Multiple alternatives not allowed
@@ -277,38 +371,50 @@ class MailjetBackendStandardEmailTests(MailjetBackendMockAPITestCase):
"""Empty to, cc, bcc, and reply_to shouldn't generate empty fields"""
self.message.send()
data = self.get_api_call_json()
self.assertNotIn('Cc', data['Globals'])
self.assertNotIn('Bcc', data['Globals'])
self.assertNotIn('ReplyTo', data['Globals'])
self.assertNotIn("Cc", data["Globals"])
self.assertNotIn("Bcc", data["Globals"])
self.assertNotIn("ReplyTo", data["Globals"])
def test_empty_to_list(self):
# Mailjet v3.1 doesn't support cc-only or bcc-only messages
self.message.to = []
self.message.cc = ['cc@example.com']
with self.assertRaisesMessage(AnymailUnsupportedFeature, "messages without any `to` recipients"):
self.message.cc = ["cc@example.com"]
with self.assertRaisesMessage(
AnymailUnsupportedFeature, "messages without any `to` recipients"
):
self.message.send()
def test_api_failure(self):
self.set_mock_response(status_code=500)
with self.assertRaisesMessage(AnymailAPIError, "Mailjet API response 500"):
mail.send_mail('Subject', 'Body', 'from@example.com', ['to@example.com'])
mail.send_mail("Subject", "Body", "from@example.com", ["to@example.com"])
# Make sure fail_silently is respected
self.set_mock_response(status_code=500)
sent = mail.send_mail('Subject', 'Body', 'from@example.com', ['to@example.com'], fail_silently=True)
sent = mail.send_mail(
"Subject",
"Body",
"from@example.com",
["to@example.com"],
fail_silently=True,
)
self.assertEqual(sent, 0)
def test_api_error_includes_details(self):
"""AnymailAPIError should include ESP's error message"""
# JSON error response - global error:
error_response = json.dumps({
"ErrorIdentifier": "06df1144-c6f3-4ca7-8885-7ec5d4344113",
"ErrorCode": "mj-0002",
"ErrorMessage": "Helpful explanation from Mailjet.",
"StatusCode": 400
}).encode('utf-8')
error_response = json.dumps(
{
"ErrorIdentifier": "06df1144-c6f3-4ca7-8885-7ec5d4344113",
"ErrorCode": "mj-0002",
"ErrorMessage": "Helpful explanation from Mailjet.",
"StatusCode": 400,
}
).encode("utf-8")
self.set_mock_response(status_code=400, raw=error_response)
with self.assertRaisesMessage(AnymailAPIError, "Helpful explanation from Mailjet"):
with self.assertRaisesMessage(
AnymailAPIError, "Helpful explanation from Mailjet"
):
self.message.send()
# Non-JSON error response:
@@ -322,7 +428,7 @@ class MailjetBackendStandardEmailTests(MailjetBackendMockAPITestCase):
self.message.send()
@tag('mailjet')
@tag("mailjet")
class MailjetBackendAnymailFeatureTests(MailjetBackendMockAPITestCase):
"""Test backend support for Anymail added features"""
@@ -330,107 +436,131 @@ class MailjetBackendAnymailFeatureTests(MailjetBackendMockAPITestCase):
self.message.envelope_sender = "bounce-handler@bounces.example.com"
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['Globals']['Sender'], {"Email": "bounce-handler@bounces.example.com"})
self.assertEqual(
data["Globals"]["Sender"], {"Email": "bounce-handler@bounces.example.com"}
)
def test_metadata(self):
# Mailjet expects the payload to be a single string
# https://dev.mailjet.com/guides/#tagging-email-messages
self.message.metadata = {'user_id': "12345", 'items': 6}
self.message.metadata = {"user_id": "12345", "items": 6}
self.message.send()
data = self.get_api_call_json()
self.assertJSONEqual(data['Globals']['EventPayload'], {"user_id": "12345", "items": 6})
self.assertJSONEqual(
data["Globals"]["EventPayload"], {"user_id": "12345", "items": 6}
)
def test_send_at(self):
self.message.send_at = 1651820889 # 2022-05-06 07:08:09 UTC
with self.assertRaisesMessage(AnymailUnsupportedFeature, 'send_at'):
with self.assertRaisesMessage(AnymailUnsupportedFeature, "send_at"):
self.message.send()
def test_tags(self):
self.message.tags = ["receipt"]
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['Globals']['CustomCampaign'], "receipt")
self.assertEqual(data["Globals"]["CustomCampaign"], "receipt")
self.message.tags = ["receipt", "repeat-user"]
with self.assertRaisesMessage(AnymailUnsupportedFeature, 'multiple tags'):
with self.assertRaisesMessage(AnymailUnsupportedFeature, "multiple tags"):
self.message.send()
def test_track_opens(self):
self.message.track_opens = True
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['Globals']['TrackOpens'], 'enabled')
self.assertEqual(data["Globals"]["TrackOpens"], "enabled")
def test_track_clicks(self):
self.message.track_clicks = True
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['Globals']['TrackClicks'], 'enabled')
self.assertEqual(data["Globals"]["TrackClicks"], "enabled")
self.message.track_clicks = False
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['Globals']['TrackClicks'], 'disabled')
self.assertEqual(data["Globals"]["TrackClicks"], "disabled")
def test_template(self):
# template_id can be str or int (but must be numeric ID -- not the template's name)
self.message.template_id = '1234567'
self.message.merge_global_data = {'name': "Alice", 'group': "Developers"}
# template_id can be str or int (but must be numeric ID-not the template's name)
self.message.template_id = "1234567"
self.message.merge_global_data = {"name": "Alice", "group": "Developers"}
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['Globals']['TemplateID'], 1234567) # must be integer
self.assertEqual(data['Globals']['TemplateLanguage'], True) # required to use variables
self.assertEqual(data['Globals']['Variables'], {'name': "Alice", 'group': "Developers"})
self.assertEqual(data["Globals"]["TemplateID"], 1234567) # must be integer
# TemplateLanguage required to use variables:
self.assertEqual(data["Globals"]["TemplateLanguage"], True)
self.assertEqual(
data["Globals"]["Variables"], {"name": "Alice", "group": "Developers"}
)
def test_template_populate_from_sender(self):
# v3.1 API allows omitting From param to use template's sender
self.message.template_id = '1234567'
self.message.from_email = None # must set to None after constructing EmailMessage
self.message.template_id = "1234567"
# must set from_email to None after constructing EmailMessage:
self.message.from_email = None
self.message.send()
data = self.get_api_call_json()
self.assertNotIn('From', data['Globals']) # use template's sender as From
self.assertNotIn("From", data["Globals"]) # use template's sender as From
def test_merge_data(self):
self.message.to = ['alice@example.com', 'Bob <bob@example.com>']
self.message.to = ["alice@example.com", "Bob <bob@example.com>"]
self.message.merge_data = {
'alice@example.com': {'name': "Alice", 'group': "Developers"},
'bob@example.com': {'name': "Bob"},
"alice@example.com": {"name": "Alice", "group": "Developers"},
"bob@example.com": {"name": "Bob"},
}
self.message.merge_global_data = {
"group": "Default Group",
"global": "Global value",
}
self.message.merge_global_data = {'group': "Default Group", 'global': "Global value"}
self.message.send()
data = self.get_api_call_json()
messages = data['Messages']
self.assertEqual(len(messages), 2) # with merge_data, each 'to' gets separate message
messages = data["Messages"]
# with merge_data, each 'to' gets separate message:
self.assertEqual(len(messages), 2)
self.assertEqual(messages[0]['To'], [{"Email": "alice@example.com"}])
self.assertEqual(messages[1]['To'], [{"Email": "bob@example.com", "Name": "Bob"}])
self.assertEqual(messages[0]["To"], [{"Email": "alice@example.com"}])
self.assertEqual(
messages[1]["To"], [{"Email": "bob@example.com", "Name": "Bob"}]
)
# global merge_data is sent in Globals
self.assertEqual(data['Globals']['Variables'], {'group': "Default Group", 'global': "Global value"})
self.assertEqual(
data["Globals"]["Variables"],
{"group": "Default Group", "global": "Global value"},
)
# per-recipient merge_data is sent in Messages (and Mailjet will merge with Globals)
self.assertEqual(messages[0]['Variables'], {'name': "Alice", 'group': "Developers"})
self.assertEqual(messages[1]['Variables'], {'name': "Bob"})
# per-recipient merge_data is sent in Messages
# (and Mailjet will merge with Globals)
self.assertEqual(
messages[0]["Variables"], {"name": "Alice", "group": "Developers"}
)
self.assertEqual(messages[1]["Variables"], {"name": "Bob"})
def test_merge_metadata(self):
self.message.to = ['alice@example.com', 'Bob <bob@example.com>']
self.message.to = ["alice@example.com", "Bob <bob@example.com>"]
self.message.merge_metadata = {
'alice@example.com': {'order_id': 123, 'tier': 'premium'},
'bob@example.com': {'order_id': 678},
"alice@example.com": {"order_id": 123, "tier": "premium"},
"bob@example.com": {"order_id": 678},
}
self.message.metadata = {'notification_batch': 'zx912'}
self.message.metadata = {"notification_batch": "zx912"}
self.message.send()
data = self.get_api_call_json()
messages = data['Messages']
messages = data["Messages"]
self.assertEqual(len(messages), 2)
self.assertEqual(messages[0]['To'][0]['Email'], "alice@example.com")
self.assertEqual(messages[0]["To"][0]["Email"], "alice@example.com")
# metadata and merge_metadata[recipient] are combined:
self.assertJSONEqual(messages[0]['EventPayload'],
{'order_id': 123, 'tier': 'premium', 'notification_batch': 'zx912'})
self.assertEqual(messages[1]['To'][0]['Email'], "bob@example.com")
self.assertJSONEqual(messages[1]['EventPayload'],
{'order_id': 678, 'notification_batch': 'zx912'})
self.assertJSONEqual(
messages[0]["EventPayload"],
{"order_id": 123, "tier": "premium", "notification_batch": "zx912"},
)
self.assertEqual(messages[1]["To"][0]["Email"], "bob@example.com")
self.assertJSONEqual(
messages[1]["EventPayload"],
{"order_id": 678, "notification_batch": "zx912"},
)
def test_default_omits_options(self):
"""Make sure by default we don't send any ESP-specific options.
@@ -441,14 +571,14 @@ class MailjetBackendAnymailFeatureTests(MailjetBackendMockAPITestCase):
"""
self.message.send()
data = self.get_api_call_json()
self.assertNotIn('CustomCampaign', data["Globals"])
self.assertNotIn('EventPayload', data["Globals"])
self.assertNotIn('HTMLPart', data["Globals"])
self.assertNotIn('TemplateID', data["Globals"])
self.assertNotIn('TemplateLanguage', data["Globals"])
self.assertNotIn('Variables', data["Globals"])
self.assertNotIn('TrackOpens', data["Globals"])
self.assertNotIn('TrackClicks', data["Globals"])
self.assertNotIn("CustomCampaign", data["Globals"])
self.assertNotIn("EventPayload", data["Globals"])
self.assertNotIn("HTMLPart", data["Globals"])
self.assertNotIn("TemplateID", data["Globals"])
self.assertNotIn("TemplateLanguage", data["Globals"])
self.assertNotIn("Variables", data["Globals"])
self.assertNotIn("TrackOpens", data["Globals"])
self.assertNotIn("TrackClicks", data["Globals"])
def test_esp_extra(self):
# Anymail deep merges Mailjet esp_extra into the v3.1 Send API payload.
@@ -456,90 +586,131 @@ class MailjetBackendAnymailFeatureTests(MailjetBackendMockAPITestCase):
# at the root. Note that it's *not* possible to merge into Messages
# (though you could completely replace it).
self.message.esp_extra = {
'Globals': {
'TemplateErrorDeliver': True,
'TemplateErrorReporting': 'bugs@example.com',
"Globals": {
"TemplateErrorDeliver": True,
"TemplateErrorReporting": "bugs@example.com",
},
'SandboxMode': True,
"SandboxMode": True,
}
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data["Globals"]['TemplateErrorDeliver'], True)
self.assertEqual(data["Globals"]['TemplateErrorReporting'], 'bugs@example.com')
self.assertIs(data['SandboxMode'], True)
self.assertEqual(data["Globals"]["TemplateErrorDeliver"], True)
self.assertEqual(data["Globals"]["TemplateErrorReporting"], "bugs@example.com")
self.assertIs(data["SandboxMode"], True)
# Make sure the backend params are also still there
self.assertEqual(data["Globals"]['Subject'], "Subject")
self.assertEqual(data["Globals"]["Subject"], "Subject")
# noinspection PyUnresolvedReferences
def test_send_attaches_anymail_status(self):
""" The anymail_status should be attached to the message when it is sent """
response_content = json.dumps({
"Messages": [{
"Status": "success",
"To": [{
"Email": "to1@example.com",
"MessageUUID": "cb927469-36fd-4c02-bce4-0d199929a207",
"MessageID": 12345678901234500,
"MessageHref": "https://api.mailjet.com/v3/message/12345678901234500"
}]
}]
}).encode('utf-8')
"""The anymail_status should be attached to the message when it is sent"""
response_content = json.dumps(
{
"Messages": [
{
"Status": "success",
"To": [
{
"Email": "to1@example.com",
"MessageUUID": "cb927469-36fd-4c02-bce4-0d199929a207",
"MessageID": 12345678901234500,
"MessageHref": "https://api.mailjet.com/v3/message"
"/12345678901234500",
}
],
}
]
}
).encode("utf-8")
self.set_mock_response(raw=response_content)
msg = mail.EmailMessage('Subject', 'Message', 'from@example.com', ['to1@example.com'])
msg = mail.EmailMessage(
"Subject", "Message", "from@example.com", ["to1@example.com"]
)
sent = msg.send()
self.assertEqual(sent, 1)
self.assertEqual(msg.anymail_status.status, {'sent'})
self.assertEqual(msg.anymail_status.status, {"sent"})
self.assertEqual(msg.anymail_status.message_id, "12345678901234500")
self.assertEqual(msg.anymail_status.recipients['to1@example.com'].status, 'sent')
self.assertEqual(msg.anymail_status.recipients['to1@example.com'].message_id, "12345678901234500")
self.assertEqual(
msg.anymail_status.recipients["to1@example.com"].status, "sent"
)
self.assertEqual(
msg.anymail_status.recipients["to1@example.com"].message_id,
"12345678901234500",
)
self.assertEqual(msg.anymail_status.esp_response.content, response_content)
# noinspection PyUnresolvedReferences
def test_mixed_status(self):
"""The status should include an entry for each recipient"""
# Mailjet's v3.1 API will partially fail a batch send, allowing valid emails to go out.
# The API response doesn't identify the failed email addresses; make sure we represent
# them correctly in the anymail_status.
response_content = json.dumps({
"Messages": [{
"Status": "success",
"CustomID": "",
"To": [{
"Email": "to-good@example.com",
"MessageUUID": "556e896a-e041-4836-bb35-8bb75ee308c5",
"MessageID": 12345678901234500,
"MessageHref": "https://api.mailjet.com/v3/REST/message/12345678901234500"
}],
"Cc": [],
"Bcc": []
}, {
"Errors": [{
"ErrorIdentifier": "f480a5a2-0334-4e08-b2b7-f372ce5669e0",
"ErrorCode": "mj-0013",
"StatusCode": 400,
"ErrorMessage": "\"invalid@123.4\" is an invalid email address.",
"ErrorRelatedTo": ["To[0].Email"]
}],
"Status": "error"
}]
}).encode('utf-8')
self.set_mock_response(raw=response_content, status_code=400) # Mailjet uses 400 for partial success
msg = mail.EmailMessage('Subject', 'Message', 'from@example.com', ['to-good@example.com', 'invalid@123.4'])
# Mailjet's v3.1 API will partially fail a batch send, allowing valid emails
# to go out. The API response doesn't identify the failed email addresses;
# make sure we represent them correctly in the anymail_status.
response_content = json.dumps(
{
"Messages": [
{
"Status": "success",
"CustomID": "",
"To": [
{
"Email": "to-good@example.com",
"MessageUUID": "556e896a-e041-4836-bb35-8bb75ee308c5",
"MessageID": 12345678901234500,
"MessageHref": "https://api.mailjet.com/v3/REST"
"/message/12345678901234500",
}
],
"Cc": [],
"Bcc": [],
},
{
"Errors": [
{
"ErrorIdentifier": "f480a5a2-0334-4e08"
"-b2b7-f372ce5669e0",
"ErrorCode": "mj-0013",
"StatusCode": 400,
"ErrorMessage": '"invalid@123.4" is an invalid'
" email address.",
"ErrorRelatedTo": ["To[0].Email"],
}
],
"Status": "error",
},
]
}
).encode("utf-8")
# Mailjet uses 400 for partial success:
self.set_mock_response(raw=response_content, status_code=400)
msg = mail.EmailMessage(
"Subject",
"Message",
"from@example.com",
["to-good@example.com", "invalid@123.4"],
)
sent = msg.send()
self.assertEqual(sent, 1)
self.assertEqual(msg.anymail_status.status, {'sent', 'failed'})
self.assertEqual(msg.anymail_status.recipients['to-good@example.com'].status, 'sent')
self.assertEqual(msg.anymail_status.recipients['to-good@example.com'].message_id, "12345678901234500")
self.assertEqual(msg.anymail_status.recipients['invalid@123.4'].status, 'failed')
self.assertEqual(msg.anymail_status.recipients['invalid@123.4'].message_id, None)
self.assertEqual(msg.anymail_status.status, {"sent", "failed"})
self.assertEqual(
msg.anymail_status.recipients["to-good@example.com"].status, "sent"
)
self.assertEqual(
msg.anymail_status.recipients["to-good@example.com"].message_id,
"12345678901234500",
)
self.assertEqual(
msg.anymail_status.recipients["invalid@123.4"].status, "failed"
)
self.assertEqual(
msg.anymail_status.recipients["invalid@123.4"].message_id, None
)
self.assertEqual(msg.anymail_status.message_id, {"12345678901234500", None})
self.assertEqual(msg.anymail_status.esp_response.content, response_content)
# noinspection PyUnresolvedReferences
def test_send_failed_anymail_status(self):
""" If the send fails, anymail_status should contain initial values"""
"""If the send fails, anymail_status should contain initial values"""
self.set_mock_response(status_code=500)
sent = self.message.send(fail_silently=True)
self.assertEqual(sent, 0)
@@ -550,9 +721,12 @@ class MailjetBackendAnymailFeatureTests(MailjetBackendMockAPITestCase):
# noinspection PyUnresolvedReferences
def test_send_unparsable_response(self):
"""If the send succeeds, but a non-JSON API response, should raise an API exception"""
mock_response = self.set_mock_response(status_code=200,
raw=b"yikes, this isn't a real response")
"""
If the send succeeds, but a non-JSON API response, should raise an API exception
"""
mock_response = self.set_mock_response(
status_code=200, raw=b"yikes, this isn't a real response"
)
with self.assertRaises(AnymailAPIError):
self.message.send()
self.assertIsNone(self.message.anymail_status.status)
@@ -562,44 +736,53 @@ class MailjetBackendAnymailFeatureTests(MailjetBackendMockAPITestCase):
def test_json_serialization_errors(self):
"""Try to provide more information about non-json-serializable data"""
self.message.tags = [Decimal('19.99')] # yeah, don't do this
self.message.tags = [Decimal("19.99")] # yeah, don't do this
with self.assertRaises(AnymailSerializationError) as cm:
self.message.send()
print(self.get_api_call_json())
err = cm.exception
self.assertIsInstance(err, TypeError) # compatibility with json.dumps
self.assertIn("Don't know how to send this data to Mailjet", str(err)) # our added context
self.assertRegex(str(err), r"Decimal.*is not JSON serializable") # original message
# our added context:
self.assertIn("Don't know how to send this data to Mailjet", str(err))
# original message:
self.assertRegex(str(err), r"Decimal.*is not JSON serializable")
def test_merge_data_null_values(self):
# Mailjet doesn't accept None (null) as a merge value;
# returns "HTTP/1.1 500 Cannot convert data from Null value"
self.message.merge_global_data = {'Some': None}
self.set_mock_response(status_code=500, reason="Cannot convert data from Null value", raw=None)
with self.assertRaisesMessage(AnymailAPIError, "Cannot convert data from Null value"):
self.message.merge_global_data = {"Some": None}
self.set_mock_response(
status_code=500, reason="Cannot convert data from Null value", raw=None
)
with self.assertRaisesMessage(
AnymailAPIError, "Cannot convert data from Null value"
):
self.message.send()
@tag('mailjet')
class MailjetBackendSessionSharingTestCase(SessionSharingTestCases, MailjetBackendMockAPITestCase):
@tag("mailjet")
class MailjetBackendSessionSharingTestCase(
SessionSharingTestCases, MailjetBackendMockAPITestCase
):
"""Requests session sharing tests"""
pass # tests are defined in SessionSharingTestCases
@tag('mailjet')
@tag("mailjet")
@override_settings(EMAIL_BACKEND="anymail.backends.mailjet.EmailBackend")
class MailjetBackendImproperlyConfiguredTests(AnymailTestMixin, SimpleTestCase):
"""Test ESP backend without required settings in place"""
def test_missing_api_key(self):
with self.assertRaises(ImproperlyConfigured) as cm:
mail.send_mail('Subject', 'Message', 'from@example.com', ['to@example.com'])
mail.send_mail("Subject", "Message", "from@example.com", ["to@example.com"])
errmsg = str(cm.exception)
self.assertRegex(errmsg, r'\bMAILJET_API_KEY\b')
self.assertRegex(errmsg, r"\bMAILJET_API_KEY\b")
@override_settings(ANYMAIL={'MAILJET_API_KEY': 'dummy'})
@override_settings(ANYMAIL={"MAILJET_API_KEY": "dummy"})
def test_missing_secret_key(self):
with self.assertRaises(ImproperlyConfigured) as cm:
mail.send_mail('Subject', 'Message', 'from@example.com', ['to@example.com'])
mail.send_mail("Subject", "Message", "from@example.com", ["to@example.com"])
errmsg = str(cm.exception)
self.assertRegex(errmsg, r'\bMAILJET_SECRET_KEY\b')
self.assertRegex(errmsg, r"\bMAILJET_SECRET_KEY\b")

View File

@@ -8,21 +8,24 @@ from anymail.inbound import AnymailInboundMessage
from anymail.signals import AnymailInboundEvent
from anymail.webhooks.mailjet import MailjetInboundWebhookView
from .utils import sample_image_content, sample_email_content
from .utils import sample_email_content, sample_image_content
from .webhook_cases import WebhookTestCase
@tag('mailjet')
@tag("mailjet")
class MailjetInboundTestCase(WebhookTestCase):
def test_inbound_basics(self):
raw_event = {
"Sender": "envelope-from@example.org",
"Recipient": "test@inbound.example.com",
"Date": "20171012T013104", # this is just the Date header from the sender, parsed to UTC
# this is just the Date header from the sender, parsed to UTC:
"Date": "20171012T013104",
"From": '"Displayed From" <from+test@example.org>',
"Subject": "Test subject",
"Headers": {
"Return-Path": ["<bounce-handler=from+test%example.org@mail.example.org>"],
"Return-Path": [
"<bounce-handler=from+test%example.org@mail.example.org>"
],
"Received": [
"from mail.example.org by parse.mailjet.com ..."
"by mail.example.org for <test@inbound.example.com> ...",
@@ -36,68 +39,85 @@ class MailjetInboundTestCase(WebhookTestCase):
"To": "Test Inbound <test@inbound.example.com>, other@example.com",
"Cc": "cc@example.com",
"Reply-To": "from+test@milter.example.org",
"Content-Type": ["multipart/alternative; boundary=\"boundary0\""],
"Content-Type": ['multipart/alternative; boundary="boundary0"'],
},
"Parts": [{
"Headers": {
"Content-Type": ['text/plain; charset="UTF-8"']
"Parts": [
{
"Headers": {"Content-Type": ['text/plain; charset="UTF-8"']},
"ContentRef": "Text-part",
},
"ContentRef": "Text-part"
}, {
"Headers": {
"Content-Type": ['text/html; charset="UTF-8"'],
"Content-Transfer-Encoding": ["quoted-printable"]
{
"Headers": {
"Content-Type": ['text/html; charset="UTF-8"'],
"Content-Transfer-Encoding": ["quoted-printable"],
},
"ContentRef": "Html-part",
},
"ContentRef": "Html-part"
}],
],
"Text-part": "Test body plain",
"Html-part": "<div>Test body html</div>",
"SpamAssassinScore": "1.7"
"SpamAssassinScore": "1.7",
}
response = self.client.post('/anymail/mailjet/inbound/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/mailjet/inbound/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.inbound_handler, sender=MailjetInboundWebhookView,
event=ANY, esp_name='Mailjet')
kwargs = self.assert_handler_called_once_with(
self.inbound_handler,
sender=MailjetInboundWebhookView,
event=ANY,
esp_name="Mailjet",
)
# AnymailInboundEvent
event = kwargs['event']
event = kwargs["event"]
self.assertIsInstance(event, AnymailInboundEvent)
self.assertEqual(event.event_type, 'inbound')
self.assertIsNone(event.timestamp) # Mailjet doesn't provide inbound event timestamp
self.assertIsNone(event.event_id) # Mailjet doesn't provide inbound event id
self.assertEqual(event.event_type, "inbound")
# Mailjet doesn't provide inbound event timestamp:
self.assertIsNone(event.timestamp)
# Mailjet doesn't provide inbound event id:
self.assertIsNone(event.event_id)
self.assertIsInstance(event.message, AnymailInboundMessage)
self.assertEqual(event.esp_event, raw_event)
# AnymailInboundMessage - convenience properties
message = event.message
self.assertEqual(message.from_email.display_name, 'Displayed From')
self.assertEqual(message.from_email.addr_spec, 'from+test@example.org')
self.assertEqual([str(e) for e in message.to],
['Test Inbound <test@inbound.example.com>', 'other@example.com'])
self.assertEqual([str(e) for e in message.cc],
['cc@example.com'])
self.assertEqual(message.subject, 'Test subject')
self.assertEqual(message.from_email.display_name, "Displayed From")
self.assertEqual(message.from_email.addr_spec, "from+test@example.org")
self.assertEqual(
[str(e) for e in message.to],
["Test Inbound <test@inbound.example.com>", "other@example.com"],
)
self.assertEqual([str(e) for e in message.cc], ["cc@example.com"])
self.assertEqual(message.subject, "Test subject")
self.assertEqual(message.date.isoformat(" "), "2017-10-11 18:31:04-07:00")
self.assertEqual(message.text, 'Test body plain')
self.assertEqual(message.html, '<div>Test body html</div>')
self.assertEqual(message.text, "Test body plain")
self.assertEqual(message.html, "<div>Test body html</div>")
self.assertEqual(message.envelope_sender, 'envelope-from@example.org')
self.assertEqual(message.envelope_recipient, 'test@inbound.example.com')
self.assertIsNone(message.stripped_text) # Mailjet doesn't provide stripped plaintext body
self.assertIsNone(message.stripped_html) # Mailjet doesn't provide stripped html
self.assertIsNone(message.spam_detected) # Mailjet doesn't provide spam boolean
self.assertEqual(message.envelope_sender, "envelope-from@example.org")
self.assertEqual(message.envelope_recipient, "test@inbound.example.com")
# Mailjet doesn't provide stripped plaintext body:
self.assertIsNone(message.stripped_text)
# Mailjet doesn't provide stripped html:
self.assertIsNone(message.stripped_html)
# Mailjet doesn't provide spam boolean:
self.assertIsNone(message.spam_detected)
self.assertEqual(message.spam_score, 1.7)
# AnymailInboundMessage - other headers
self.assertEqual(message['Message-ID'], "<CAEPk3R+4Zr@mail.example.org>")
self.assertEqual(message['Reply-To'], "from+test@milter.example.org")
self.assertEqual(message.get_all('Received'), [
"from mail.example.org by parse.mailjet.com ..."
"by mail.example.org for <test@inbound.example.com> ...",
"by 10.10.1.71 with HTTP; Wed, 11 Oct 2017 18:31:04 -0700 (PDT)",
])
self.assertEqual(message["Message-ID"], "<CAEPk3R+4Zr@mail.example.org>")
self.assertEqual(message["Reply-To"], "from+test@milter.example.org")
self.assertEqual(
message.get_all("Received"),
[
"from mail.example.org by parse.mailjet.com ..."
"by mail.example.org for <test@inbound.example.com> ...",
"by 10.10.1.71 with HTTP; Wed, 11 Oct 2017 18:31:04 -0700 (PDT)",
],
)
def test_attachments(self):
image_content = sample_image_content()
@@ -105,68 +125,89 @@ class MailjetInboundTestCase(WebhookTestCase):
raw_event = {
"Headers": {
"MIME-Version": ["1.0"],
"Content-Type": ["multipart/mixed; boundary=\"boundary0\""],
"Content-Type": ['multipart/mixed; boundary="boundary0"'],
},
"Parts": [{
"Headers": {"Content-Type": ['multipart/related; boundary="boundary1"']}
}, {
"Headers": {"Content-Type": ['multipart/alternative; boundary="boundary2"']}
}, {
"Headers": {"Content-Type": ['text/plain; charset="UTF-8"']},
"ContentRef": "Text-part"
}, {
"Headers": {
"Content-Type": ['text/html; charset="UTF-8"'],
"Content-Transfer-Encoding": ["quoted-printable"]
"Parts": [
{
"Headers": {
"Content-Type": ['multipart/related; boundary="boundary1"']
}
},
"ContentRef": "Html-part"
}, {
"Headers": {
"Content-Type": ['text/plain'],
"Content-Disposition": ['attachment; filename="test.txt"'],
"Content-Transfer-Encoding": ["quoted-printable"],
{
"Headers": {
"Content-Type": ['multipart/alternative; boundary="boundary2"']
}
},
"ContentRef": "Attachment1"
}, {
"Headers": {
"Content-Type": ['image/png; name="image.png"'],
"Content-Disposition": ['inline; filename="image.png"'],
"Content-Transfer-Encoding": ["base64"],
"Content-ID": ["<abc123>"],
{
"Headers": {"Content-Type": ['text/plain; charset="UTF-8"']},
"ContentRef": "Text-part",
},
"ContentRef": "InlineAttachment1"
}, {
"Headers": {
"Content-Type": ['message/rfc822; charset="US-ASCII"'],
"Content-Disposition": ['attachment'],
{
"Headers": {
"Content-Type": ['text/html; charset="UTF-8"'],
"Content-Transfer-Encoding": ["quoted-printable"],
},
"ContentRef": "Html-part",
},
"ContentRef": "Attachment2"
}],
{
"Headers": {
"Content-Type": ["text/plain"],
"Content-Disposition": ['attachment; filename="test.txt"'],
"Content-Transfer-Encoding": ["quoted-printable"],
},
"ContentRef": "Attachment1",
},
{
"Headers": {
"Content-Type": ['image/png; name="image.png"'],
"Content-Disposition": ['inline; filename="image.png"'],
"Content-Transfer-Encoding": ["base64"],
"Content-ID": ["<abc123>"],
},
"ContentRef": "InlineAttachment1",
},
{
"Headers": {
"Content-Type": ['message/rfc822; charset="US-ASCII"'],
"Content-Disposition": ["attachment"],
},
"ContentRef": "Attachment2",
},
],
"Text-part": "Test body plain",
"Html-part": "<div>Test body html <img src='cid:abc123'></div>",
"InlineAttachment1": b64encode(image_content).decode('ascii'),
"Attachment1": b64encode('test attachment'.encode('utf-8')).decode('ascii'),
"Attachment2": b64encode(email_content).decode('ascii'),
"InlineAttachment1": b64encode(image_content).decode("ascii"),
"Attachment1": b64encode("test attachment".encode("utf-8")).decode("ascii"),
"Attachment2": b64encode(email_content).decode("ascii"),
}
response = self.client.post('/anymail/mailjet/inbound/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/mailjet/inbound/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.inbound_handler, sender=MailjetInboundWebhookView,
event=ANY, esp_name='Mailjet')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.inbound_handler,
sender=MailjetInboundWebhookView,
event=ANY,
esp_name="Mailjet",
)
event = kwargs["event"]
message = event.message
attachments = message.attachments # AnymailInboundMessage convenience accessor
self.assertEqual(len(attachments), 2)
self.assertEqual(attachments[0].get_filename(), 'test.txt')
self.assertEqual(attachments[0].get_content_type(), 'text/plain')
self.assertEqual(attachments[0].get_content_text(), 'test attachment')
self.assertEqual(attachments[1].get_content_type(), 'message/rfc822')
self.assertEqualIgnoringHeaderFolding(attachments[1].get_content_bytes(), email_content)
self.assertEqual(attachments[0].get_filename(), "test.txt")
self.assertEqual(attachments[0].get_content_type(), "text/plain")
self.assertEqual(attachments[0].get_content_text(), "test attachment")
self.assertEqual(attachments[1].get_content_type(), "message/rfc822")
self.assertEqualIgnoringHeaderFolding(
attachments[1].get_content_bytes(), email_content
)
inlines = message.inline_attachments
self.assertEqual(len(inlines), 1)
inline = inlines['abc123']
self.assertEqual(inline.get_filename(), 'image.png')
self.assertEqual(inline.get_content_type(), 'image/png')
inline = inlines["abc123"]
self.assertEqual(inline.get_filename(), "image.png")
self.assertEqual(inline.get_content_type(), "image/png")
self.assertEqual(inline.get_content_bytes(), image_content)

View File

@@ -9,28 +9,38 @@ from anymail.message import AnymailMessage
from .utils import AnymailTestMixin, sample_image_path
ANYMAIL_TEST_MAILJET_API_KEY = os.getenv('ANYMAIL_TEST_MAILJET_API_KEY')
ANYMAIL_TEST_MAILJET_SECRET_KEY = os.getenv('ANYMAIL_TEST_MAILJET_SECRET_KEY')
ANYMAIL_TEST_MAILJET_DOMAIN = os.getenv('ANYMAIL_TEST_MAILJET_DOMAIN')
ANYMAIL_TEST_MAILJET_API_KEY = os.getenv("ANYMAIL_TEST_MAILJET_API_KEY")
ANYMAIL_TEST_MAILJET_SECRET_KEY = os.getenv("ANYMAIL_TEST_MAILJET_SECRET_KEY")
ANYMAIL_TEST_MAILJET_DOMAIN = os.getenv("ANYMAIL_TEST_MAILJET_DOMAIN")
@tag('mailjet', 'live')
@tag("mailjet", "live")
@unittest.skipUnless(
ANYMAIL_TEST_MAILJET_API_KEY and ANYMAIL_TEST_MAILJET_SECRET_KEY and ANYMAIL_TEST_MAILJET_DOMAIN,
"Set ANYMAIL_TEST_MAILJET_API_KEY and ANYMAIL_TEST_MAILJET_SECRET_KEY and ANYMAIL_TEST_MAILJET_DOMAIN "
"environment variables to run Mailjet integration tests")
@override_settings(ANYMAIL={"MAILJET_API_KEY": ANYMAIL_TEST_MAILJET_API_KEY,
"MAILJET_SECRET_KEY": ANYMAIL_TEST_MAILJET_SECRET_KEY,
"MAILJET_SEND_DEFAULTS": {"esp_extra": {"SandboxMode": True}}, # don't actually send mail
},
EMAIL_BACKEND="anymail.backends.mailjet.EmailBackend")
ANYMAIL_TEST_MAILJET_API_KEY
and ANYMAIL_TEST_MAILJET_SECRET_KEY
and ANYMAIL_TEST_MAILJET_DOMAIN,
"Set ANYMAIL_TEST_MAILJET_API_KEY and ANYMAIL_TEST_MAILJET_SECRET_KEY"
" and ANYMAIL_TEST_MAILJET_DOMAIN environment variables to run Mailjet"
" integration tests",
)
@override_settings(
ANYMAIL={
"MAILJET_API_KEY": ANYMAIL_TEST_MAILJET_API_KEY,
"MAILJET_SECRET_KEY": ANYMAIL_TEST_MAILJET_SECRET_KEY,
"MAILJET_SEND_DEFAULTS": {
"esp_extra": {"SandboxMode": True} # don't actually send mail
},
},
EMAIL_BACKEND="anymail.backends.mailjet.EmailBackend",
)
class MailjetBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
"""Mailjet API integration tests
"""
Mailjet API integration tests
These tests run against the **live** Mailjet API, using the
environment variables `ANYMAIL_TEST_MAILJET_API_KEY` and `ANYMAIL_TEST_MAILJET_SECRET_KEY`
as the API key and API secret key, respectively, and `ANYMAIL_TEST_MAILJET_DOMAIN` as
a validated Mailjet sending domain. If those variables are not set, these tests won't run.
These tests run against the **live** Mailjet API, using the environment variables
`ANYMAIL_TEST_MAILJET_API_KEY` and `ANYMAIL_TEST_MAILJET_SECRET_KEY` as the API key
and API secret key, respectively, and `ANYMAIL_TEST_MAILJET_DOMAIN` as a validated
Mailjet sending domain. If those variables are not set, these tests won't run.
These tests enable Mailjet's SandboxMode to avoid sending any email;
remove the esp_extra setting above if you are trying to actually send test messages.
@@ -38,10 +48,14 @@ class MailjetBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
def setUp(self):
super().setUp()
self.from_email = 'test@%s' % ANYMAIL_TEST_MAILJET_DOMAIN
self.message = AnymailMessage('Anymail Mailjet integration test', 'Text content',
self.from_email, ['test+to1@anymail.dev'])
self.message.attach_alternative('<p>HTML content</p>', "text/html")
self.from_email = "test@%s" % ANYMAIL_TEST_MAILJET_DOMAIN
self.message = AnymailMessage(
"Anymail Mailjet integration test",
"Text content",
self.from_email,
["test+to1@anymail.dev"],
)
self.message.attach_alternative("<p>HTML content</p>", "text/html")
def test_simple_send(self):
# Example of getting the Mailjet send status and message id from the message
@@ -49,12 +63,13 @@ class MailjetBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
self.assertEqual(sent_count, 1)
anymail_status = self.message.anymail_status
sent_status = anymail_status.recipients['test+to1@anymail.dev'].status
message_id = anymail_status.recipients['test+to1@anymail.dev'].message_id
sent_status = anymail_status.recipients["test+to1@anymail.dev"].status
message_id = anymail_status.recipients["test+to1@anymail.dev"].message_id
self.assertEqual(sent_status, 'sent')
self.assertRegex(message_id, r'.+')
self.assertEqual(anymail_status.status, {sent_status}) # set of all recipient statuses
self.assertEqual(sent_status, "sent")
self.assertRegex(message_id, r".+")
# set of all recipient statuses:
self.assertEqual(anymail_status.status, {sent_status})
self.assertEqual(anymail_status.message_id, message_id)
def test_all_options(self):
@@ -62,12 +77,12 @@ class MailjetBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
subject="Anymail Mailjet all-options integration test",
body="This is the text body",
from_email=formataddr(("Test Sender, Inc.", self.from_email)),
to=['test+to1@anymail.dev', '"Recipient, 2nd" <test+to2@anymail.dev>'],
cc=['test+cc1@anymail.dev', 'Copy 2 <test+cc1@anymail.dev>'],
bcc=['test+bcc1@anymail.dev', 'Blind Copy 2 <test+bcc2@anymail.dev>'],
reply_to=['"Reply, To" <reply2@example.com>'], # Mailjet only supports single reply_to
to=["test+to1@anymail.dev", '"Recipient, 2nd" <test+to2@anymail.dev>'],
cc=["test+cc1@anymail.dev", "Copy 2 <test+cc1@anymail.dev>"],
bcc=["test+bcc1@anymail.dev", "Blind Copy 2 <test+bcc2@anymail.dev>"],
# Mailjet only supports single reply_to:
reply_to=['"Reply, To" <reply2@example.com>'],
headers={"X-Anymail-Test": "value"},
metadata={"meta1": "simple string", "meta2": 2},
tags=["tag 1"], # Mailjet only allows a single tag
track_clicks=True,
@@ -79,51 +94,56 @@ class MailjetBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
message.attach_alternative(
"<p><b>HTML:</b> with <a href='http://example.com'>link</a>"
"and image: <img src='cid:%s'></div>" % cid,
"text/html")
"text/html",
)
message.send()
self.assertEqual(message.anymail_status.status, {'sent'})
self.assertEqual(message.anymail_status.status, {"sent"})
def test_merge_data(self):
message = AnymailMessage(
subject="Anymail Mailjet merge_data test", # Mailjet doesn't support merge fields in the subject
# Mailjet doesn't support merge fields in the subject
subject="Anymail Mailjet merge_data test",
body="This body includes merge data: [[var:value]]\n"
"And global merge data: [[var:global]]",
"And global merge data: [[var:global]]",
from_email=formataddr(("Test From", self.from_email)),
to=["test+to1@anymail.dev", "Recipient 2 <test+to2@anymail.dev>"],
merge_data={
'test+to1@anymail.dev': {'value': 'one'},
'test+to2@anymail.dev': {'value': 'two'},
},
merge_global_data={
'global': 'global_value'
"test+to1@anymail.dev": {"value": "one"},
"test+to2@anymail.dev": {"value": "two"},
},
merge_global_data={"global": "global_value"},
)
message.send()
recipient_status = message.anymail_status.recipients
self.assertEqual(recipient_status['test+to1@anymail.dev'].status, 'sent')
self.assertEqual(recipient_status['test+to2@anymail.dev'].status, 'sent')
self.assertEqual(recipient_status["test+to1@anymail.dev"].status, "sent")
self.assertEqual(recipient_status["test+to2@anymail.dev"].status, "sent")
def test_stored_template(self):
message = AnymailMessage(
template_id='176375', # ID of the real template named 'test-template' in our Mailjet test account
# ID of the real template named 'test-template' in our Mailjet test account:
template_id="176375",
to=["test+to1@anymail.dev"],
merge_data={
'test+to1@anymail.dev': {
'name': "Test Recipient",
"test+to1@anymail.dev": {
"name": "Test Recipient",
}
},
merge_global_data={
'order': '12345',
"order": "12345",
},
)
message.from_email = None # use the template's sender email/name
message.send()
recipient_status = message.anymail_status.recipients
self.assertEqual(recipient_status['test+to1@anymail.dev'].status, 'sent')
self.assertEqual(recipient_status["test+to1@anymail.dev"].status, "sent")
@override_settings(ANYMAIL={"MAILJET_API_KEY": "Hey, that's not an API key!",
"MAILJET_SECRET_KEY": "and this isn't the secret for it"})
@override_settings(
ANYMAIL={
"MAILJET_API_KEY": "Hey, that's not an API key!",
"MAILJET_SECRET_KEY": "and this isn't the secret for it",
}
)
def test_invalid_api_key(self):
with self.assertRaises(AnymailAPIError) as cm:
self.message.send()

View File

@@ -6,126 +6,178 @@ from django.test import tag
from anymail.signals import AnymailTrackingEvent
from anymail.webhooks.mailjet import MailjetTrackingWebhookView
from .webhook_cases import WebhookBasicAuthTestCase, WebhookTestCase
@tag('mailjet')
@tag("mailjet")
class MailjetWebhookSecurityTestCase(WebhookBasicAuthTestCase):
def call_webhook(self):
return self.client.post('/anymail/mailjet/tracking/',
content_type='application/json', data=json.dumps([]))
return self.client.post(
"/anymail/mailjet/tracking/",
content_type="application/json",
data=json.dumps([]),
)
# Actual tests are in WebhookBasicAuthTestCase
@tag('mailjet')
@tag("mailjet")
class MailjetDeliveryTestCase(WebhookTestCase):
def test_sent_event(self):
# Mailjet's "sent" event indicates receiving MTA has accepted message; Anymail calls this "delivered"
raw_events = [{
"event": "sent",
"time": 1498093527,
"MessageID": 12345678901234567,
"email": "recipient@example.com",
"mj_campaign_id": 1234567890,
"mj_contact_id": 9876543210,
"customcampaign": "tag1",
"mj_message_id": "12345678901234567",
"smtp_reply": "sent (250 2.0.0 OK 1498093527 a67bc12345def.22 - gsmtp)",
"Payload": "{\"meta1\": \"simple string\", \"meta2\": 2}",
}]
response = self.client.post('/anymail/mailjet/tracking/',
content_type='application/json', data=json.dumps(raw_events))
# Mailjet's "sent" event indicates receiving MTA has accepted message;
# Anymail calls this "delivered"
raw_events = [
{
"event": "sent",
"time": 1498093527,
"MessageID": 12345678901234567,
"email": "recipient@example.com",
"mj_campaign_id": 1234567890,
"mj_contact_id": 9876543210,
"customcampaign": "tag1",
"mj_message_id": "12345678901234567",
"smtp_reply": "sent (250 2.0.0 OK 1498093527 a67bc12345def.22 - gsmtp)",
"Payload": '{"meta1": "simple string", "meta2": 2}',
}
]
response = self.client.post(
"/anymail/mailjet/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=MailjetTrackingWebhookView,
event=ANY, esp_name='Mailjet')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=MailjetTrackingWebhookView,
event=ANY,
esp_name="Mailjet",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "delivered")
self.assertEqual(event.timestamp, datetime(2017, 6, 22, 1, 5, 27, tzinfo=timezone.utc))
self.assertEqual(
event.timestamp, datetime(2017, 6, 22, 1, 5, 27, tzinfo=timezone.utc)
)
self.assertEqual(event.esp_event, raw_events[0])
self.assertEqual(event.mta_response, "sent (250 2.0.0 OK 1498093527 a67bc12345def.22 - gsmtp)")
self.assertEqual(event.message_id, "12345678901234567") # converted to str (matching backend status)
self.assertEqual(
event.mta_response,
"sent (250 2.0.0 OK 1498093527 a67bc12345def.22 - gsmtp)",
)
# message_id converted to str (matching backend status):
self.assertEqual(event.message_id, "12345678901234567")
self.assertEqual(event.recipient, "recipient@example.com")
self.assertEqual(event.tags, ["tag1"])
self.assertEqual(event.metadata, {"meta1": "simple string", "meta2": 2})
def test_open_event(self):
raw_events = [{
"event": "open",
"time": 1498093527,
"MessageID": 12345678901234567,
"email": "recipient@example.com",
"mj_campaign_id": 1234567890,
"mj_contact_id": 9876543210,
"customcampaign": "",
"ip": "192.168.100.100",
"geo": "US",
"agent": "Mozilla/5.0 (via ggpht.com GoogleImageProxy)",
"Payload": "",
}]
response = self.client.post('/anymail/mailjet/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"event": "open",
"time": 1498093527,
"MessageID": 12345678901234567,
"email": "recipient@example.com",
"mj_campaign_id": 1234567890,
"mj_contact_id": 9876543210,
"customcampaign": "",
"ip": "192.168.100.100",
"geo": "US",
"agent": "Mozilla/5.0 (via ggpht.com GoogleImageProxy)",
"Payload": "",
}
]
response = self.client.post(
"/anymail/mailjet/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=MailjetTrackingWebhookView,
event=ANY, esp_name='Mailjet')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=MailjetTrackingWebhookView,
event=ANY,
esp_name="Mailjet",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "opened")
self.assertEqual(event.message_id, "12345678901234567")
self.assertEqual(event.recipient, "recipient@example.com")
self.assertEqual(event.user_agent, "Mozilla/5.0 (via ggpht.com GoogleImageProxy)")
self.assertEqual(
event.user_agent, "Mozilla/5.0 (via ggpht.com GoogleImageProxy)"
)
self.assertEqual(event.tags, [])
self.assertEqual(event.metadata, {})
def test_click_event(self):
raw_events = [{
"event": "open",
"time": 1498093527,
"MessageID": 12345678901234567,
"email": "recipient@example.com",
"mj_campaign_id": 1234567890,
"mj_contact_id": 9876543210,
"customcampaign": "",
"url": "http://example.com",
"ip": "192.168.100.100",
"geo": "US",
"agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) Chrome/58.0.3029.110",
}]
response = self.client.post('/anymail/mailjet/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"event": "open",
"time": 1498093527,
"MessageID": 12345678901234567,
"email": "recipient@example.com",
"mj_campaign_id": 1234567890,
"mj_contact_id": 9876543210,
"customcampaign": "",
"url": "http://example.com",
"ip": "192.168.100.100",
"geo": "US",
"agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5)"
" Chrome/58.0.3029.110",
}
]
response = self.client.post(
"/anymail/mailjet/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=MailjetTrackingWebhookView,
event=ANY, esp_name='Mailjet')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=MailjetTrackingWebhookView,
event=ANY,
esp_name="Mailjet",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "opened")
self.assertEqual(event.message_id, "12345678901234567")
self.assertEqual(event.recipient, "recipient@example.com")
self.assertEqual(event.user_agent, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) Chrome/58.0.3029.110")
self.assertEqual(
event.user_agent,
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) Chrome/58.0.3029.110",
)
self.assertEqual(event.click_url, "http://example.com")
self.assertEqual(event.tags, [])
self.assertEqual(event.metadata, {})
def test_bounce_event(self):
raw_events = [{
"event": "bounce",
"time": 1498093527,
"MessageID": 12345678901234567,
"email": "invalid@invalid",
"mj_campaign_id": 1234567890,
"mj_contact_id": 9876543210,
"customcampaign": "",
"blocked": True,
"hard_bounce": True,
"error_related_to": "domain",
"error": "invalid domain"
}]
response = self.client.post('/anymail/mailjet/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"event": "bounce",
"time": 1498093527,
"MessageID": 12345678901234567,
"email": "invalid@invalid",
"mj_campaign_id": 1234567890,
"mj_contact_id": 9876543210,
"customcampaign": "",
"blocked": True,
"hard_bounce": True,
"error_related_to": "domain",
"error": "invalid domain",
}
]
response = self.client.post(
"/anymail/mailjet/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=MailjetTrackingWebhookView,
event=ANY, esp_name='Mailjet')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=MailjetTrackingWebhookView,
event=ANY,
esp_name="Mailjet",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "bounced")
self.assertEqual(event.message_id, "12345678901234567")
self.assertEqual(event.recipient, "invalid@invalid")
@@ -133,23 +185,32 @@ class MailjetDeliveryTestCase(WebhookTestCase):
self.assertEqual(event.mta_response, None)
def test_blocked_event(self):
raw_events = [{
"event": "blocked",
"time": 1498093527,
"MessageID": 12345678901234567,
"email": "bad@example.com",
"mj_campaign_id": 0,
"mj_contact_id": 9876543210,
"customcampaign": "",
"error_related_to": "domain",
"error": "typofix",
}]
response = self.client.post('/anymail/mailjet/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"event": "blocked",
"time": 1498093527,
"MessageID": 12345678901234567,
"email": "bad@example.com",
"mj_campaign_id": 0,
"mj_contact_id": 9876543210,
"customcampaign": "",
"error_related_to": "domain",
"error": "typofix",
}
]
response = self.client.post(
"/anymail/mailjet/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=MailjetTrackingWebhookView,
event=ANY, esp_name='Mailjet')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=MailjetTrackingWebhookView,
event=ANY,
esp_name="Mailjet",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "rejected")
self.assertEqual(event.message_id, "12345678901234567")
self.assertEqual(event.recipient, "bad@example.com")
@@ -157,79 +218,106 @@ class MailjetDeliveryTestCase(WebhookTestCase):
self.assertEqual(event.mta_response, None)
def test_spam_event(self):
raw_events = [{
"event": "spam",
"time": 1498093527,
"MessageID": 12345678901234567,
"email": "spam@example.com",
"mj_campaign_id": 1234567890,
"mj_contact_id": 9876543210,
"customcampaign": "",
"source": "greylisted"
}]
response = self.client.post('/anymail/mailjet/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"event": "spam",
"time": 1498093527,
"MessageID": 12345678901234567,
"email": "spam@example.com",
"mj_campaign_id": 1234567890,
"mj_contact_id": 9876543210,
"customcampaign": "",
"source": "greylisted",
}
]
response = self.client.post(
"/anymail/mailjet/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=MailjetTrackingWebhookView,
event=ANY, esp_name='Mailjet')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=MailjetTrackingWebhookView,
event=ANY,
esp_name="Mailjet",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "complained")
self.assertEqual(event.message_id, "12345678901234567")
self.assertEqual(event.recipient, "spam@example.com")
def test_unsub_event(self):
raw_events = [{
"event": "unsub",
"time": 1498093527,
"MessageID": 12345678901234567,
"email": "recipient@example.com",
"mj_campaign_id": 1234567890,
"mj_contact_id": 9876543210,
"customcampaign": "",
"mj_list_id": 0,
"ip": "127.0.0.4",
"geo": "",
"agent": "List-Unsubscribe"
}]
response = self.client.post('/anymail/mailjet/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"event": "unsub",
"time": 1498093527,
"MessageID": 12345678901234567,
"email": "recipient@example.com",
"mj_campaign_id": 1234567890,
"mj_contact_id": 9876543210,
"customcampaign": "",
"mj_list_id": 0,
"ip": "127.0.0.4",
"geo": "",
"agent": "List-Unsubscribe",
}
]
response = self.client.post(
"/anymail/mailjet/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=MailjetTrackingWebhookView,
event=ANY, esp_name='Mailjet')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=MailjetTrackingWebhookView,
event=ANY,
esp_name="Mailjet",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "unsubscribed")
self.assertEqual(event.message_id, "12345678901234567")
self.assertEqual(event.recipient, "recipient@example.com")
def test_bounced_greylist_event(self):
# greylist "bounce" should be reported as "deferred" (will be retried later)
raw_events = [{
"event": "bounce",
"time": 1498093527,
"MessageID": 12345678901234567,
"email": "protected@example.com",
"mj_campaign_id": 1234567890,
"mj_contact_id": 9876543210,
"customcampaign": "",
"blocked": True,
"hard_bounce": False,
"error_related_to": "domain",
"error": "greylisted"
}]
response = self.client.post('/anymail/mailjet/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"event": "bounce",
"time": 1498093527,
"MessageID": 12345678901234567,
"email": "protected@example.com",
"mj_campaign_id": 1234567890,
"mj_contact_id": 9876543210,
"customcampaign": "",
"blocked": True,
"hard_bounce": False,
"error_related_to": "domain",
"error": "greylisted",
}
]
response = self.client.post(
"/anymail/mailjet/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=MailjetTrackingWebhookView,
event=ANY, esp_name='Mailjet')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=MailjetTrackingWebhookView,
event=ANY,
esp_name="Mailjet",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "deferred")
self.assertEqual(event.message_id, "12345678901234567")
self.assertEqual(event.recipient, "protected@example.com")
self.assertEqual(event.reject_reason, "other")
def test_non_grouped_event(self):
# If you don't enable "group events" on a webhook, Mailjet sends a single bare event
# (not a list of one event, despite what the docs say).
# If you don't enable "group events" on a webhook, Mailjet sends a single bare
# event (not a list of one event, despite what the docs say).
raw_event = {
"event": "sent",
"time": 1498093527,
@@ -242,16 +330,29 @@ class MailjetDeliveryTestCase(WebhookTestCase):
"smtp_reply": "sent (250 2.0.0 OK 1498093527 a67bc12345def.22 - gsmtp)",
"Payload": "",
}
response = self.client.post('/anymail/mailjet/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/mailjet/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=MailjetTrackingWebhookView,
event=ANY, esp_name='Mailjet')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=MailjetTrackingWebhookView,
event=ANY,
esp_name="Mailjet",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "delivered")
self.assertEqual(event.timestamp, datetime(2017, 6, 22, 1, 5, 27, tzinfo=timezone.utc))
self.assertEqual(
event.timestamp, datetime(2017, 6, 22, 1, 5, 27, tzinfo=timezone.utc)
)
self.assertEqual(event.esp_event, raw_event)
self.assertEqual(event.mta_response, "sent (250 2.0.0 OK 1498093527 a67bc12345def.22 - gsmtp)")
self.assertEqual(event.message_id, "12345678901234567") # converted to str (matching backend status)
self.assertEqual(
event.mta_response,
"sent (250 2.0.0 OK 1498093527 a67bc12345def.22 - gsmtp)",
)
# message_id converted to str (matching backend status)
self.assertEqual(event.message_id, "12345678901234567")
self.assertEqual(event.recipient, "recipient@example.com")

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
from datetime import date
from django.core import mail
from django.test import override_settings, tag
@@ -7,7 +8,7 @@ from anymail.exceptions import AnymailSerializationError
from .test_mandrill_backend import MandrillBackendMockAPITestCase
@tag('mandrill')
@tag("mandrill")
class MandrillBackendDjrillFeatureTests(MandrillBackendMockAPITestCase):
"""Test backend support for deprecated features leftover from Djrill"""
@@ -18,164 +19,180 @@ class MandrillBackendDjrillFeatureTests(MandrillBackendMockAPITestCase):
# self.message.async = True
# it should be changed to:
# self.message.esp_extra = {"async": True}
# (The setattr below keeps these tests compatible, but isn't recommended for your code.)
setattr(self.message, 'async', True) # don't do this; use esp_extra instead
with self.assertWarnsRegex(DeprecationWarning, 'async'):
# (The setattr below keeps these tests compatible,
# but isn't recommended for your code.)
setattr(self.message, "async", True) # don't do this; use esp_extra instead
with self.assertWarnsRegex(DeprecationWarning, "async"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['async'], True)
self.assertEqual(data["async"], True)
def test_auto_html(self):
self.message.auto_html = True
with self.assertWarnsRegex(DeprecationWarning, 'auto_html'):
with self.assertWarnsRegex(DeprecationWarning, "auto_html"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['message']['auto_html'], True)
self.assertEqual(data["message"]["auto_html"], True)
def test_auto_text(self):
self.message.auto_text = True
with self.assertWarnsRegex(DeprecationWarning, 'auto_text'):
with self.assertWarnsRegex(DeprecationWarning, "auto_text"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['message']['auto_text'], True)
self.assertEqual(data["message"]["auto_text"], True)
def test_google_analytics_campaign(self):
self.message.google_analytics_campaign = "Email Receipts"
with self.assertWarnsRegex(DeprecationWarning, 'google_analytics_campaign'):
with self.assertWarnsRegex(DeprecationWarning, "google_analytics_campaign"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['message']['google_analytics_campaign'], "Email Receipts")
self.assertEqual(data["message"]["google_analytics_campaign"], "Email Receipts")
def test_google_analytics_domains(self):
self.message.google_analytics_domains = ["example.com"]
with self.assertWarnsRegex(DeprecationWarning, 'google_analytics_domains'):
with self.assertWarnsRegex(DeprecationWarning, "google_analytics_domains"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['message']['google_analytics_domains'], ["example.com"])
self.assertEqual(data["message"]["google_analytics_domains"], ["example.com"])
def test_important(self):
self.message.important = True
with self.assertWarnsRegex(DeprecationWarning, 'important'):
with self.assertWarnsRegex(DeprecationWarning, "important"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['message']['important'], True)
self.assertEqual(data["message"]["important"], True)
def test_inline_css(self):
self.message.inline_css = True
with self.assertWarnsRegex(DeprecationWarning, 'inline_css'):
with self.assertWarnsRegex(DeprecationWarning, "inline_css"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['message']['inline_css'], True)
self.assertEqual(data["message"]["inline_css"], True)
def test_ip_pool(self):
self.message.ip_pool = "Bulk Pool"
with self.assertWarnsRegex(DeprecationWarning, 'ip_pool'):
with self.assertWarnsRegex(DeprecationWarning, "ip_pool"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['ip_pool'], "Bulk Pool")
self.assertEqual(data["ip_pool"], "Bulk Pool")
def test_merge_language(self):
self.message.merge_language = "mailchimp"
with self.assertWarnsRegex(DeprecationWarning, 'merge_language'):
with self.assertWarnsRegex(DeprecationWarning, "merge_language"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['message']['merge_language'], "mailchimp")
self.assertEqual(data["message"]["merge_language"], "mailchimp")
def test_preserve_recipients(self):
self.message.preserve_recipients = True
with self.assertWarnsRegex(DeprecationWarning, 'preserve_recipients'):
with self.assertWarnsRegex(DeprecationWarning, "preserve_recipients"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['message']['preserve_recipients'], True)
self.assertEqual(data["message"]["preserve_recipients"], True)
def test_recipient_metadata(self):
self.message.recipient_metadata = {
# Anymail expands simple python dicts into the more-verbose
# rcpt/values structures the Mandrill API uses
"customer@example.com": {'cust_id': "67890", 'order_id': "54321"},
"guest@example.com": {'cust_id': "94107", 'order_id': "43215"}
"customer@example.com": {"cust_id": "67890", "order_id": "54321"},
"guest@example.com": {"cust_id": "94107", "order_id": "43215"},
}
with self.assertWarnsRegex(DeprecationWarning, 'recipient_metadata'):
with self.assertWarnsRegex(DeprecationWarning, "recipient_metadata"):
self.message.send()
data = self.get_api_call_json()
self.assertCountEqual(data['message']['recipient_metadata'], [
{'rcpt': "customer@example.com",
'values': {'cust_id': "67890", 'order_id': "54321"}},
{'rcpt': "guest@example.com",
'values': {'cust_id': "94107", 'order_id': "43215"}}])
self.assertCountEqual(
data["message"]["recipient_metadata"],
[
{
"rcpt": "customer@example.com",
"values": {"cust_id": "67890", "order_id": "54321"},
},
{
"rcpt": "guest@example.com",
"values": {"cust_id": "94107", "order_id": "43215"},
},
],
)
def test_return_path_domain(self):
self.message.return_path_domain = "support.example.com"
with self.assertWarnsRegex(DeprecationWarning, 'return_path_domain'):
with self.assertWarnsRegex(DeprecationWarning, "return_path_domain"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['message']['return_path_domain'], "support.example.com")
self.assertEqual(data["message"]["return_path_domain"], "support.example.com")
def test_signing_domain(self):
self.message.signing_domain = "example.com"
with self.assertWarnsRegex(DeprecationWarning, 'signing_domain'):
with self.assertWarnsRegex(DeprecationWarning, "signing_domain"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['message']['signing_domain'], "example.com")
self.assertEqual(data["message"]["signing_domain"], "example.com")
def test_subaccount(self):
self.message.subaccount = "marketing-dept"
with self.assertWarnsRegex(DeprecationWarning, 'subaccount'):
with self.assertWarnsRegex(DeprecationWarning, "subaccount"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['message']['subaccount'], "marketing-dept")
self.assertEqual(data["message"]["subaccount"], "marketing-dept")
def test_template_content(self):
self.message.template_content = {
'HEADLINE': "<h1>Specials Just For *|FNAME|*</h1>",
'OFFER_BLOCK': "<p><em>Half off</em> all fruit</p>"
"HEADLINE": "<h1>Specials Just For *|FNAME|*</h1>",
"OFFER_BLOCK": "<p><em>Half off</em> all fruit</p>",
}
with self.assertWarnsRegex(DeprecationWarning, 'template_content'):
with self.assertWarnsRegex(DeprecationWarning, "template_content"):
self.message.send()
data = self.get_api_call_json()
# Anymail expands simple python dicts into the more-verbose name/content
# structures the Mandrill API uses
self.assertCountEqual(data['template_content'], [
{'name': "HEADLINE", 'content': "<h1>Specials Just For *|FNAME|*</h1>"},
{'name': "OFFER_BLOCK", 'content': "<p><em>Half off</em> all fruit</p>"}])
self.assertCountEqual(
data["template_content"],
[
{"name": "HEADLINE", "content": "<h1>Specials Just For *|FNAME|*</h1>"},
{
"name": "OFFER_BLOCK",
"content": "<p><em>Half off</em> all fruit</p>",
},
],
)
def test_tracking_domain(self):
self.message.tracking_domain = "click.example.com"
with self.assertWarnsRegex(DeprecationWarning, 'tracking_domain'):
with self.assertWarnsRegex(DeprecationWarning, "tracking_domain"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['message']['tracking_domain'], "click.example.com")
self.assertEqual(data["message"]["tracking_domain"], "click.example.com")
def test_url_strip_qs(self):
self.message.url_strip_qs = True
with self.assertWarnsRegex(DeprecationWarning, 'url_strip_qs'):
with self.assertWarnsRegex(DeprecationWarning, "url_strip_qs"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['message']['url_strip_qs'], True)
self.assertEqual(data["message"]["url_strip_qs"], True)
def test_use_template_from(self):
self.message.template_id = "PERSONALIZED_SPECIALS" # forces send-template api
self.message.use_template_from = True
with self.assertWarnsRegex(DeprecationWarning, 'use_template_from'):
with self.assertWarnsRegex(DeprecationWarning, "use_template_from"):
self.message.send()
data = self.get_api_call_json()
self.assertNotIn('from_email', data['message'])
self.assertNotIn('from_name', data['message'])
self.assertNotIn("from_email", data["message"])
self.assertNotIn("from_name", data["message"])
def test_use_template_subject(self):
self.message.template_id = "PERSONALIZED_SPECIALS" # force send-template API
self.message.use_template_subject = True
with self.assertWarnsRegex(DeprecationWarning, 'use_template_subject'):
with self.assertWarnsRegex(DeprecationWarning, "use_template_subject"):
self.message.send()
data = self.get_api_call_json()
self.assertNotIn('subject', data['message'])
self.assertNotIn("subject", data["message"])
def test_view_content_link(self):
self.message.view_content_link = True
with self.assertWarnsRegex(DeprecationWarning, 'view_content_link'):
with self.assertWarnsRegex(DeprecationWarning, "view_content_link"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['message']['view_content_link'], True)
self.assertEqual(data["message"]["view_content_link"], True)
def test_default_omits_options(self):
"""Make sure by default we don't send any Mandrill-specific options.
@@ -187,49 +204,56 @@ class MandrillBackendDjrillFeatureTests(MandrillBackendMockAPITestCase):
self.message.send()
self.assert_esp_called("/messages/send.json")
data = self.get_api_call_json()
self.assertFalse('auto_html' in data['message'])
self.assertFalse('auto_text' in data['message'])
self.assertFalse('bcc_address' in data['message'])
self.assertFalse('from_name' in data['message'])
self.assertFalse('global_merge_vars' in data['message'])
self.assertFalse('google_analytics_campaign' in data['message'])
self.assertFalse('google_analytics_domains' in data['message'])
self.assertFalse('important' in data['message'])
self.assertFalse('inline_css' in data['message'])
self.assertFalse('merge_language' in data['message'])
self.assertFalse('merge_vars' in data['message'])
self.assertFalse('preserve_recipients' in data['message'])
self.assertFalse('recipient_metadata' in data['message'])
self.assertFalse('return_path_domain' in data['message'])
self.assertFalse('signing_domain' in data['message'])
self.assertFalse('subaccount' in data['message'])
self.assertFalse('tracking_domain' in data['message'])
self.assertFalse('url_strip_qs' in data['message'])
self.assertFalse('view_content_link' in data['message'])
self.assertFalse("auto_html" in data["message"])
self.assertFalse("auto_text" in data["message"])
self.assertFalse("bcc_address" in data["message"])
self.assertFalse("from_name" in data["message"])
self.assertFalse("global_merge_vars" in data["message"])
self.assertFalse("google_analytics_campaign" in data["message"])
self.assertFalse("google_analytics_domains" in data["message"])
self.assertFalse("important" in data["message"])
self.assertFalse("inline_css" in data["message"])
self.assertFalse("merge_language" in data["message"])
self.assertFalse("merge_vars" in data["message"])
self.assertFalse("preserve_recipients" in data["message"])
self.assertFalse("recipient_metadata" in data["message"])
self.assertFalse("return_path_domain" in data["message"])
self.assertFalse("signing_domain" in data["message"])
self.assertFalse("subaccount" in data["message"])
self.assertFalse("tracking_domain" in data["message"])
self.assertFalse("url_strip_qs" in data["message"])
self.assertFalse("view_content_link" in data["message"])
# Options at top level of api params (not in message dict):
self.assertFalse('async' in data)
self.assertFalse('ip_pool' in data)
self.assertFalse("async" in data)
self.assertFalse("ip_pool" in data)
def test_dates_not_serialized(self):
"""Old versions of predecessor package Djrill accidentally serialized dates to ISO"""
self.message.metadata = {'SHIP_DATE': date(2015, 12, 2)}
"""
Old versions of predecessor package Djrill accidentally serialized dates to ISO
"""
self.message.metadata = {"SHIP_DATE": date(2015, 12, 2)}
with self.assertRaises(AnymailSerializationError):
self.message.send()
@override_settings(ANYMAIL_MANDRILL_SEND_DEFAULTS={'subaccount': 'test_subaccount'})
@override_settings(ANYMAIL_MANDRILL_SEND_DEFAULTS={"subaccount": "test_subaccount"})
def test_subaccount_setting(self):
"""Global, non-esp_extra version of subaccount default"""
with self.assertWarnsRegex(DeprecationWarning, 'subaccount'):
mail.send_mail('Subject', 'Body', 'from@example.com', ['to@example.com'])
with self.assertWarnsRegex(DeprecationWarning, "subaccount"):
mail.send_mail("Subject", "Body", "from@example.com", ["to@example.com"])
data = self.get_api_call_json()
self.assertEqual(data['message']['subaccount'], "test_subaccount")
self.assertEqual(data["message"]["subaccount"], "test_subaccount")
@override_settings(ANYMAIL_MANDRILL_SEND_DEFAULTS={'subaccount': 'global_setting_subaccount'})
@override_settings(
ANYMAIL_MANDRILL_SEND_DEFAULTS={"subaccount": "global_setting_subaccount"}
)
def test_subaccount_message_overrides_setting(self):
"""Global, non-esp_extra version of subaccount default"""
message = mail.EmailMessage('Subject', 'Body', 'from@example.com', ['to@example.com'])
message.subaccount = "individual_message_subaccount" # should override global setting
with self.assertWarnsRegex(DeprecationWarning, 'subaccount'):
message = mail.EmailMessage(
"Subject", "Body", "from@example.com", ["to@example.com"]
)
# subaccount should override global setting:
message.subaccount = "individual_message_subaccount"
with self.assertWarnsRegex(DeprecationWarning, "subaccount"):
message.send()
data = self.get_api_call_json()
self.assertEqual(data['message']['subaccount'], "individual_message_subaccount")
self.assertEqual(data["message"]["subaccount"], "individual_message_subaccount")

View File

@@ -11,7 +11,7 @@ from .test_mandrill_webhooks import TEST_WEBHOOK_KEY, mandrill_args
from .webhook_cases import WebhookTestCase
@tag('mandrill')
@tag("mandrill")
@override_settings(ANYMAIL_MANDRILL_WEBHOOK_KEY=TEST_WEBHOOK_KEY)
class MandrillInboundTestCase(WebhookTestCase):
def test_inbound_basics(self):
@@ -19,7 +19,8 @@ class MandrillInboundTestCase(WebhookTestCase):
"event": "inbound",
"ts": 1507856722,
"msg": {
"raw_msg": dedent("""\
"raw_msg": dedent(
"""\
From: A tester <test@example.org>
Date: Thu, 12 Oct 2017 18:03:30 -0700
Message-ID: <CAEPk3RKEx@mail.example.org>
@@ -41,9 +42,11 @@ class MandrillInboundTestCase(WebhookTestCase):
<div dir=3D"ltr">It's a body=E2=80=A6</div>
--94eb2c05e174adb140055b6339c5--
"""),
""" # NOQA: E501
),
"email": "delivered-to@example.com",
"sender": None, # Mandrill populates "sender" only for outbound message events
# Mandrill populates "sender" only for outbound message events
"sender": None,
"spam_report": {
"score": 1.7,
},
@@ -52,13 +55,20 @@ class MandrillInboundTestCase(WebhookTestCase):
},
}
response = self.client.post(**mandrill_args(events=[raw_event], path='/anymail/mandrill/'))
response = self.client.post(
**mandrill_args(events=[raw_event], path="/anymail/mandrill/")
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.inbound_handler, sender=MandrillCombinedWebhookView,
event=ANY, esp_name='Mandrill')
self.assertEqual(self.tracking_handler.call_count, 0) # Inbound should not dispatch tracking signal
kwargs = self.assert_handler_called_once_with(
self.inbound_handler,
sender=MandrillCombinedWebhookView,
event=ANY,
esp_name="Mandrill",
)
# Inbound should not dispatch tracking signal:
self.assertEqual(self.tracking_handler.call_count, 0)
event = kwargs['event']
event = kwargs["event"]
self.assertIsInstance(event, AnymailInboundEvent)
self.assertEqual(event.event_type, "inbound")
self.assertEqual(event.timestamp.isoformat(), "2017-10-13T01:05:22+00:00")
@@ -67,22 +77,28 @@ class MandrillInboundTestCase(WebhookTestCase):
self.assertEqual(event.esp_event, raw_event)
message = event.message
self.assertEqual(message.from_email.display_name, 'A tester')
self.assertEqual(message.from_email.addr_spec, 'test@example.org')
self.assertEqual(message.from_email.display_name, "A tester")
self.assertEqual(message.from_email.addr_spec, "test@example.org")
self.assertEqual(len(message.to), 2)
self.assertEqual(message.to[0].display_name, 'Test, Inbound')
self.assertEqual(message.to[0].addr_spec, 'test@inbound.example.com')
self.assertEqual(message.to[1].addr_spec, 'other@example.com')
self.assertEqual(message.subject, 'Test subject')
self.assertEqual(message.to[0].display_name, "Test, Inbound")
self.assertEqual(message.to[0].addr_spec, "test@inbound.example.com")
self.assertEqual(message.to[1].addr_spec, "other@example.com")
self.assertEqual(message.subject, "Test subject")
self.assertEqual(message.date.isoformat(" "), "2017-10-12 18:03:30-07:00")
self.assertEqual(message.text, "It's a body\N{HORIZONTAL ELLIPSIS}\n")
self.assertEqual(message.html, """<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\n""")
self.assertEqual(
message.html,
"""<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\n""",
)
self.assertIsNone(message.envelope_sender) # Mandrill doesn't provide sender
self.assertEqual(message.envelope_recipient, 'delivered-to@example.com')
self.assertIsNone(message.stripped_text) # Mandrill doesn't provide stripped plaintext body
self.assertIsNone(message.stripped_html) # Mandrill doesn't provide stripped html
self.assertIsNone(message.spam_detected) # Mandrill doesn't provide spam boolean
self.assertEqual(message.envelope_recipient, "delivered-to@example.com")
# Mandrill doesn't provide stripped plaintext body:
self.assertIsNone(message.stripped_text)
# Mandrill doesn't provide stripped html:
self.assertIsNone(message.stripped_html)
# Mandrill doesn't provide spam boolean:
self.assertIsNone(message.spam_detected)
self.assertEqual(message.spam_score, 1.7)
# Anymail will also parse attachments (if any) from the raw mime.

View File

@@ -10,16 +10,20 @@ from anymail.message import AnymailMessage
from .utils import AnymailTestMixin, sample_image_path
ANYMAIL_TEST_MANDRILL_API_KEY = os.getenv('ANYMAIL_TEST_MANDRILL_API_KEY')
ANYMAIL_TEST_MANDRILL_DOMAIN = os.getenv('ANYMAIL_TEST_MANDRILL_DOMAIN')
ANYMAIL_TEST_MANDRILL_API_KEY = os.getenv("ANYMAIL_TEST_MANDRILL_API_KEY")
ANYMAIL_TEST_MANDRILL_DOMAIN = os.getenv("ANYMAIL_TEST_MANDRILL_DOMAIN")
@tag('mandrill', 'live')
@unittest.skipUnless(ANYMAIL_TEST_MANDRILL_API_KEY and ANYMAIL_TEST_MANDRILL_DOMAIN,
"Set ANYMAIL_TEST_MANDRILL_API_KEY and ANYMAIL_TEST_MANDRILL_DOMAIN "
"environment variables to run integration tests")
@override_settings(MANDRILL_API_KEY=ANYMAIL_TEST_MANDRILL_API_KEY,
EMAIL_BACKEND="anymail.backends.mandrill.EmailBackend")
@tag("mandrill", "live")
@unittest.skipUnless(
ANYMAIL_TEST_MANDRILL_API_KEY and ANYMAIL_TEST_MANDRILL_DOMAIN,
"Set ANYMAIL_TEST_MANDRILL_API_KEY and ANYMAIL_TEST_MANDRILL_DOMAIN "
"environment variables to run integration tests",
)
@override_settings(
MANDRILL_API_KEY=ANYMAIL_TEST_MANDRILL_API_KEY,
EMAIL_BACKEND="anymail.backends.mandrill.EmailBackend",
)
class MandrillBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
"""Mandrill API integration tests
@@ -33,17 +37,23 @@ class MandrillBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
def setUp(self):
super().setUp()
self.from_email = self.addr('from')
self.message = mail.EmailMultiAlternatives('Anymail Mandrill integration test', 'Text content',
self.from_email, [self.addr('test+to1')])
self.message.attach_alternative('<p>HTML content</p>', "text/html")
self.from_email = self.addr("from")
self.message = mail.EmailMultiAlternatives(
"Anymail Mandrill integration test",
"Text content",
self.from_email,
[self.addr("test+to1")],
)
self.message.attach_alternative("<p>HTML content</p>", "text/html")
def addr(self, username, display_name=None):
"""Construct test email address within our test domain"""
# Because integration tests run within a Mandrill trial account,
# both sender and recipient addresses must be within the test domain.
# (Other recipient addresses will be rejected with 'recipient-domain-mismatch'.)
email = '{username}@{domain}'.format(username=username, domain=ANYMAIL_TEST_MANDRILL_DOMAIN)
email = "{username}@{domain}".format(
username=username, domain=ANYMAIL_TEST_MANDRILL_DOMAIN
)
if display_name is not None:
return formataddr((display_name, email))
else:
@@ -60,11 +70,15 @@ class MandrillBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
sent_status = anymail_status.recipients[to_email].status
message_id = anymail_status.recipients[to_email].message_id
self.assertIn(sent_status, ['sent', 'queued']) # successful send (could still bounce later)
self.assertGreater(len(message_id), 0) # don't know what it'll be, but it should exist
# successful send (could still bounce later):
self.assertIn(sent_status, ["sent", "queued"])
# don't know what it'll be, but it should exist:
self.assertGreater(len(message_id), 0)
self.assertEqual(anymail_status.status, {sent_status}) # set of all recipient statuses
self.assertEqual(anymail_status.message_id, message_id) # because only a single recipient (else would be a set)
# set of all recipient statuses:
self.assertEqual(anymail_status.status, {sent_status})
# because only a single recipient (else would be a set):
self.assertEqual(anymail_status.message_id, message_id)
def test_all_options(self):
message = AnymailMessage(
@@ -76,7 +90,6 @@ class MandrillBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
bcc=[self.addr("test+bcc1"), self.addr("test+bcc2", "Blind Copy 2")],
reply_to=["reply1@example.com", "Reply 2 <reply2@example.com>"],
headers={"X-Anymail-Test": "value"},
# no metadata, send_at, track_clicks support
tags=["tag 1"], # max one tag
track_opens=True,
@@ -87,15 +100,18 @@ class MandrillBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
message.attach_alternative(
"<p><b>HTML:</b> with <a href='http://example.com'>link</a>"
"and image: <img src='cid:%s'></div>" % cid,
"text/html")
"text/html",
)
message.send()
self.assertTrue(message.anymail_status.status.issubset({'queued', 'sent'}))
self.assertTrue(message.anymail_status.status.issubset({"queued", "sent"}))
def test_invalid_from(self):
# Example of trying to send from an invalid address
# Mandrill returns a 500 response (which raises a MandrillAPIError)
self.message.from_email = 'webmaster@localhost' # Django default DEFAULT_FROM_EMAIL
self.message.from_email = (
"webmaster@localhost" # Django default DEFAULT_FROM_EMAIL
)
with self.assertRaises(AnymailAPIError) as cm:
self.message.send()
err = cm.exception
@@ -104,41 +120,54 @@ class MandrillBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
def test_invalid_to(self):
# Example of detecting when a recipient is not a valid email address
self.message.to = ['invalid@localhost']
self.message.to = ["invalid@localhost"]
try:
self.message.send()
except AnymailRecipientsRefused:
# Mandrill refused to deliver the mail -- message.anymail_status will tell you why:
# Mandrill refused to deliver the mail -- message.anymail_status
# will tell you why:
# noinspection PyUnresolvedReferences
anymail_status = self.message.anymail_status
self.assertEqual(anymail_status.recipients['invalid@localhost'].status, 'invalid')
self.assertEqual(anymail_status.status, {'invalid'})
self.assertEqual(
anymail_status.recipients["invalid@localhost"].status, "invalid"
)
self.assertEqual(anymail_status.status, {"invalid"})
else:
# Sometimes Mandrill queues these test sends
# noinspection PyUnresolvedReferences
if self.message.anymail_status.status == {'queued'}:
if self.message.anymail_status.status == {"queued"}:
self.skipTest("Mandrill queued the send -- can't complete this test")
else:
self.fail("Anymail did not raise AnymailRecipientsRefused for invalid recipient")
self.fail(
"Anymail did not raise AnymailRecipientsRefused"
" for invalid recipient"
)
def test_rejected_to(self):
# Example of detecting when a recipient is on Mandrill's rejection blacklist
self.message.to = ['reject@test.mandrillapp.com']
self.message.to = ["reject@test.mandrillapp.com"]
try:
self.message.send()
except AnymailRecipientsRefused:
# Mandrill refused to deliver the mail -- message.anymail_status will tell you why:
# Mandrill refused to deliver the mail -- message.anymail_status will
# tell you why:
# noinspection PyUnresolvedReferences
anymail_status = self.message.anymail_status
self.assertEqual(anymail_status.recipients['reject@test.mandrillapp.com'].status, 'rejected')
self.assertEqual(anymail_status.status, {'rejected'})
self.assertEqual(
anymail_status.recipients["reject@test.mandrillapp.com"].status,
"rejected",
)
self.assertEqual(anymail_status.status, {"rejected"})
else:
# Sometimes Mandrill queues these test sends
# noinspection PyUnresolvedReferences
if self.message.anymail_status.status == {'queued'}:
if self.message.anymail_status.status == {"queued"}:
self.skipTest("Mandrill queued the send -- can't complete this test")
else:
self.fail("Anymail did not raise AnymailRecipientsRefused for blacklist recipient")
self.fail(
"Anymail did not raise AnymailRecipientsRefused"
" for blacklist recipient"
)
@override_settings(MANDRILL_API_KEY="Hey, that's not an API key!")
def test_invalid_api_key(self):

View File

@@ -10,17 +10,23 @@ from django.core.exceptions import ImproperlyConfigured
from django.test import override_settings, tag
from anymail.signals import AnymailTrackingEvent
from anymail.webhooks.mandrill import MandrillCombinedWebhookView, MandrillTrackingWebhookView
from anymail.webhooks.mandrill import (
MandrillCombinedWebhookView,
MandrillTrackingWebhookView,
)
from .webhook_cases import WebhookBasicAuthTestCase, WebhookTestCase
TEST_WEBHOOK_KEY = 'TEST_WEBHOOK_KEY'
TEST_WEBHOOK_KEY = "TEST_WEBHOOK_KEY"
def mandrill_args(events=None,
host="http://testserver/", # Django test-client default
path='/anymail/mandrill/', # Anymail urlconf default
auth="username:password", # WebhookTestCase default
key=TEST_WEBHOOK_KEY):
def mandrill_args(
events=None,
host="http://testserver/", # Django test-client default
path="/anymail/mandrill/", # Anymail urlconf default
auth="username:password", # WebhookTestCase default
key=TEST_WEBHOOK_KEY,
):
"""Returns TestClient.post kwargs for Mandrill webhook call with events
Computes correct signature.
@@ -35,55 +41,59 @@ def mandrill_args(events=None,
else:
full_url = test_client_path
mandrill_events = json.dumps(events)
signed_data = full_url + 'mandrill_events' + mandrill_events
signature = b64encode(hmac.new(key=key.encode('ascii'),
msg=signed_data.encode('utf-8'),
digestmod=hashlib.sha1).digest())
signed_data = full_url + "mandrill_events" + mandrill_events
signature = b64encode(
hmac.new(
key=key.encode("ascii"),
msg=signed_data.encode("utf-8"),
digestmod=hashlib.sha1,
).digest()
)
return {
'path': test_client_path,
'data': {'mandrill_events': mandrill_events},
'HTTP_X_MANDRILL_SIGNATURE': signature,
"path": test_client_path,
"data": {"mandrill_events": mandrill_events},
"HTTP_X_MANDRILL_SIGNATURE": signature,
}
@tag('mandrill')
@tag("mandrill")
class MandrillWebhookSettingsTestCase(WebhookTestCase):
def test_requires_webhook_key(self):
with self.assertRaisesRegex(ImproperlyConfigured, r'MANDRILL_WEBHOOK_KEY'):
self.client.post('/anymail/mandrill/',
data={'mandrill_events': '[]'})
with self.assertRaisesRegex(ImproperlyConfigured, r"MANDRILL_WEBHOOK_KEY"):
self.client.post("/anymail/mandrill/", data={"mandrill_events": "[]"})
def test_head_does_not_require_webhook_key(self):
# Mandrill issues an unsigned HEAD request to verify the wehbook url.
# Only *after* that succeeds will Mandrill will tell you the webhook key.
# So make sure that HEAD request will go through without any key set:
response = self.client.head('/anymail/mandrill/')
response = self.client.head("/anymail/mandrill/")
self.assertEqual(response.status_code, 200)
@tag('mandrill')
@tag("mandrill")
@override_settings(ANYMAIL_MANDRILL_WEBHOOK_KEY=TEST_WEBHOOK_KEY)
class MandrillWebhookSecurityTestCase(WebhookBasicAuthTestCase):
should_warn_if_no_auth = False # because we check webhook signature
def call_webhook(self):
kwargs = mandrill_args([{'event': 'send'}])
kwargs = mandrill_args([{"event": "send"}])
return self.client.post(**kwargs)
# Additional tests are in WebhookBasicAuthTestCase
def test_verifies_correct_signature(self):
kwargs = mandrill_args([{'event': 'send'}])
kwargs = mandrill_args([{"event": "send"}])
response = self.client.post(**kwargs)
self.assertEqual(response.status_code, 200)
def test_verifies_missing_signature(self):
response = self.client.post('/anymail/mandrill/',
data={'mandrill_events': '[{"event":"send"}]'})
response = self.client.post(
"/anymail/mandrill/", data={"mandrill_events": '[{"event":"send"}]'}
)
self.assertEqual(response.status_code, 400)
def test_verifies_bad_signature(self):
kwargs = mandrill_args([{'event': 'send'}], key="wrong API key")
kwargs = mandrill_args([{"event": "send"}], key="wrong API key")
response = self.client.post(**kwargs)
self.assertEqual(response.status_code, 400)
@@ -91,81 +101,93 @@ class MandrillWebhookSecurityTestCase(WebhookBasicAuthTestCase):
def test_no_basic_auth(self):
# Signature validation should work properly if you're not using basic auth
self.clear_basic_auth()
kwargs = mandrill_args([{'event': 'send'}], auth="")
kwargs = mandrill_args([{"event": "send"}], auth="")
response = self.client.post(**kwargs)
self.assertEqual(response.status_code, 200)
@override_settings(
ALLOWED_HOSTS=['127.0.0.1', '.example.com'],
ALLOWED_HOSTS=["127.0.0.1", ".example.com"],
ANYMAIL={
"MANDRILL_WEBHOOK_URL": "https://abcde:12345@example.com/anymail/mandrill/",
"WEBHOOK_SECRET": "abcde:12345",
})
},
)
def test_webhook_url_setting(self):
# If Django can't build_absolute_uri correctly (e.g., because your proxy
# frontend isn't setting the proxy headers correctly), you must set
# MANDRILL_WEBHOOK_URL to the actual public url where Mandrill calls the webhook.
# MANDRILL_WEBHOOK_URL to the actual public url where Mandrill calls
# the webhook.
self.set_basic_auth("abcde", "12345")
kwargs = mandrill_args([{'event': 'send'}], host="https://example.com/", auth="abcde:12345")
kwargs = mandrill_args(
[{"event": "send"}], host="https://example.com/", auth="abcde:12345"
)
response = self.client.post(SERVER_NAME="127.0.0.1", **kwargs)
self.assertEqual(response.status_code, 200)
# override WebhookBasicAuthTestCase version of this test
@override_settings(ANYMAIL={'WEBHOOK_SECRET': ['cred1:pass1', 'cred2:pass2']})
@override_settings(ANYMAIL={"WEBHOOK_SECRET": ["cred1:pass1", "cred2:pass2"]})
def test_supports_credential_rotation(self):
"""You can supply a list of basic auth credentials, and any is allowed"""
self.set_basic_auth('cred1', 'pass1')
self.set_basic_auth("cred1", "pass1")
response = self.client.post(**mandrill_args(auth="cred1:pass1"))
self.assertEqual(response.status_code, 200)
self.set_basic_auth('cred2', 'pass2')
self.set_basic_auth("cred2", "pass2")
response = self.client.post(**mandrill_args(auth="cred2:pass2"))
self.assertEqual(response.status_code, 200)
self.set_basic_auth('baduser', 'wrongpassword')
self.set_basic_auth("baduser", "wrongpassword")
response = self.client.post(**mandrill_args(auth="baduser:wrongpassword"))
self.assertEqual(response.status_code, 400)
@tag('mandrill')
@tag("mandrill")
@override_settings(ANYMAIL_MANDRILL_WEBHOOK_KEY=TEST_WEBHOOK_KEY)
class MandrillTrackingTestCase(WebhookTestCase):
def test_head_request(self):
# Mandrill verifies webhooks at config time with a HEAD request
# (See MandrillWebhookSettingsTestCase above for equivalent without the key yet set)
response = self.client.head('/anymail/mandrill/tracking/')
# (See MandrillWebhookSettingsTestCase above for equivalent
# without the key yet set)
response = self.client.head("/anymail/mandrill/tracking/")
self.assertEqual(response.status_code, 200)
def test_post_request_invalid_json(self):
kwargs = mandrill_args()
kwargs['data'] = {'mandrill_events': "GARBAGE DATA"}
kwargs["data"] = {"mandrill_events": "GARBAGE DATA"}
response = self.client.post(**kwargs)
self.assertEqual(response.status_code, 400)
def test_send_event(self):
raw_events = [{
"event": "send",
"msg": {
"ts": 1461095211, # time send called
"subject": "Webhook Test",
"email": "recipient@example.com",
"sender": "sender@example.com",
"tags": ["tag1", "tag2"],
"metadata": {"custom1": "value1", "custom2": "value2"},
"_id": "abcdef012345789abcdef012345789"
},
"_id": "abcdef012345789abcdef012345789",
"ts": 1461095246 # time of event
}]
raw_events = [
{
"event": "send",
"msg": {
"ts": 1461095211, # time send called
"subject": "Webhook Test",
"email": "recipient@example.com",
"sender": "sender@example.com",
"tags": ["tag1", "tag2"],
"metadata": {"custom1": "value1", "custom2": "value2"},
"_id": "abcdef012345789abcdef012345789",
},
"_id": "abcdef012345789abcdef012345789",
"ts": 1461095246, # time of event
}
]
response = self.client.post(**mandrill_args(events=raw_events))
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=MandrillCombinedWebhookView,
event=ANY, esp_name='Mandrill')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=MandrillCombinedWebhookView,
event=ANY,
esp_name="Mandrill",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "sent")
self.assertEqual(event.timestamp, datetime(2016, 4, 19, 19, 47, 26, tzinfo=timezone.utc))
self.assertEqual(
event.timestamp, datetime(2016, 4, 19, 19, 47, 26, tzinfo=timezone.utc)
)
self.assertEqual(event.esp_event, raw_events[0])
self.assertEqual(event.message_id, "abcdef012345789abcdef012345789")
self.assertEqual(event.recipient, "recipient@example.com")
@@ -173,100 +195,129 @@ class MandrillTrackingTestCase(WebhookTestCase):
self.assertEqual(event.metadata, {"custom1": "value1", "custom2": "value2"})
def test_hard_bounce_event(self):
raw_events = [{
"event": "hard_bounce",
"msg": {
"ts": 1461095211, # time send called
"subject": "Webhook Test",
"email": "bounce@example.com",
"sender": "sender@example.com",
"bounce_description": "bad_mailbox",
"bgtools_code": 10,
"diag": "smtp;550 5.1.1 The email account that you tried to reach does not exist.",
"_id": "abcdef012345789abcdef012345789"
},
"_id": "abcdef012345789abcdef012345789",
"ts": 1461095246 # time of event
}]
raw_events = [
{
"event": "hard_bounce",
"msg": {
"ts": 1461095211, # time send called
"subject": "Webhook Test",
"email": "bounce@example.com",
"sender": "sender@example.com",
"bounce_description": "bad_mailbox",
"bgtools_code": 10,
"diag": "smtp;550 5.1.1 The email account that you tried"
" to reach does not exist.",
"_id": "abcdef012345789abcdef012345789",
},
"_id": "abcdef012345789abcdef012345789",
"ts": 1461095246, # time of event
}
]
response = self.client.post(**mandrill_args(events=raw_events))
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=MandrillCombinedWebhookView,
event=ANY, esp_name='Mandrill')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=MandrillCombinedWebhookView,
event=ANY,
esp_name="Mandrill",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "bounced")
self.assertEqual(event.esp_event, raw_events[0])
self.assertEqual(event.message_id, "abcdef012345789abcdef012345789")
self.assertEqual(event.recipient, "bounce@example.com")
self.assertEqual(event.mta_response,
"smtp;550 5.1.1 The email account that you tried to reach does not exist.")
self.assertEqual(
event.mta_response,
"smtp;550 5.1.1 The email account that you tried to reach does not exist.",
)
def test_click_event(self):
raw_events = [{
"event": "click",
"msg": {
"ts": 1461095211, # time send called
"subject": "Webhook Test",
"email": "recipient@example.com",
"sender": "sender@example.com",
"opens": [{"ts": 1461095242}],
"clicks": [{"ts": 1461095246, "url": "http://example.com"}],
"_id": "abcdef012345789abcdef012345789"
},
"user_agent": "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0",
"url": "http://example.com",
"_id": "abcdef012345789abcdef012345789",
"ts": 1461095246 # time of event
}]
raw_events = [
{
"event": "click",
"msg": {
"ts": 1461095211, # time send called
"subject": "Webhook Test",
"email": "recipient@example.com",
"sender": "sender@example.com",
"opens": [{"ts": 1461095242}],
"clicks": [{"ts": 1461095246, "url": "http://example.com"}],
"_id": "abcdef012345789abcdef012345789",
},
"user_agent": "Mozilla/5.0 (Windows NT 5.1; rv:11.0)"
" Gecko Firefox/11.0",
"url": "http://example.com",
"_id": "abcdef012345789abcdef012345789",
"ts": 1461095246, # time of event
}
]
response = self.client.post(**mandrill_args(events=raw_events))
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=MandrillCombinedWebhookView,
event=ANY, esp_name='Mandrill')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=MandrillCombinedWebhookView,
event=ANY,
esp_name="Mandrill",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "clicked")
self.assertEqual(event.esp_event, raw_events[0])
self.assertEqual(event.click_url, "http://example.com")
self.assertEqual(event.user_agent, "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0")
self.assertEqual(
event.user_agent, "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0"
)
def test_sync_event(self):
# Mandrill sync events use a different format from other events
# https://mandrill.zendesk.com/hc/en-us/articles/205583297-Sync-Event-Webhook-format
raw_events = [{
"type": "blacklist",
"action": "add",
"reject": {
"email": "recipient@example.com",
"reason": "manual edit"
raw_events = [
{
"type": "blacklist",
"action": "add",
"reject": {"email": "recipient@example.com", "reason": "manual edit"},
}
}]
]
response = self.client.post(**mandrill_args(events=raw_events))
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=MandrillCombinedWebhookView,
event=ANY, esp_name='Mandrill')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=MandrillCombinedWebhookView,
event=ANY,
esp_name="Mandrill",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "unknown")
self.assertEqual(event.recipient, "recipient@example.com")
self.assertEqual(event.description, "manual edit")
def test_old_tracking_url(self):
# Earlier versions of Anymail used /mandrill/tracking/ (and didn't support inbound);
# make sure that URL continues to work.
raw_events = [{
"event": "send",
"msg": {
"ts": 1461095211, # time send called
"subject": "Webhook Test",
"email": "recipient@example.com",
"sender": "sender@example.com",
"tags": ["tag1", "tag2"],
"metadata": {"custom1": "value1", "custom2": "value2"},
"_id": "abcdef012345789abcdef012345789"
},
"_id": "abcdef012345789abcdef012345789",
"ts": 1461095246 # time of event
}]
response = self.client.post(**mandrill_args(events=raw_events, path='/anymail/mandrill/tracking/'))
# Earlier versions of Anymail used /mandrill/tracking/ (and didn't support
# inbound); make sure that URL continues to work.
raw_events = [
{
"event": "send",
"msg": {
"ts": 1461095211, # time send called
"subject": "Webhook Test",
"email": "recipient@example.com",
"sender": "sender@example.com",
"tags": ["tag1", "tag2"],
"metadata": {"custom1": "value1", "custom2": "value2"},
"_id": "abcdef012345789abcdef012345789",
},
"_id": "abcdef012345789abcdef012345789",
"ts": 1461095246, # time of event
}
]
response = self.client.post(
**mandrill_args(events=raw_events, path="/anymail/mandrill/tracking/")
)
self.assertEqual(response.status_code, 200)
self.assert_handler_called_once_with(self.tracking_handler, sender=MandrillTrackingWebhookView,
event=ANY, esp_name='Mandrill')
self.assert_handler_called_once_with(
self.tracking_handler,
sender=MandrillTrackingWebhookView,
event=ANY,
esp_name="Mandrill",
)

View File

@@ -21,15 +21,21 @@ class InlineImageTests(AnymailTestMixin, SimpleTestCase):
# an inline attachment filename that causes Gmail to reject the message.)
mock_getfqdn.return_value = "server.example.com"
cid = attach_inline_image(self.message, sample_image_content())
self.assertRegex(cid, r"[\w.]+@inline",
"Content-ID should be a valid Message-ID, "
"but _not_ @server.example.com")
self.assertRegex(
cid,
r"[\w.]+@inline",
"Content-ID should be a valid Message-ID, " "but _not_ @server.example.com",
)
def test_domain_override(self):
cid = attach_inline_image(self.message, sample_image_content(),
domain="example.org")
self.assertRegex(cid, r"[\w.]+@example\.org",
"Content-ID should be a valid Message-ID @example.org")
cid = attach_inline_image(
self.message, sample_image_content(), domain="example.org"
)
self.assertRegex(
cid,
r"[\w.]+@example\.org",
"Content-ID should be a valid Message-ID @example.org",
)
class AnymailStatusTests(AnymailTestMixin, SimpleTestCase):
@@ -42,10 +48,14 @@ class AnymailStatusTests(AnymailTestMixin, SimpleTestCase):
self.assertEqual(status.status, {"sent"})
self.assertEqual(status.message_id, "12345")
self.assertEqual(status.recipients, recipients)
self.assertEqual(repr(status),
"AnymailStatus<status={'sent'}, message_id='12345', 1 recipients>")
self.assertEqual(repr(status.recipients["one@example.com"]),
"AnymailRecipientStatus('12345', 'sent')")
self.assertEqual(
repr(status),
"AnymailStatus<status={'sent'}, message_id='12345', 1 recipients>",
)
self.assertEqual(
repr(status.recipients["one@example.com"]),
"AnymailRecipientStatus('12345', 'sent')",
)
def test_multiple_recipients(self):
recipients = {
@@ -57,8 +67,11 @@ class AnymailStatusTests(AnymailTestMixin, SimpleTestCase):
self.assertEqual(status.status, {"queued", "sent"})
self.assertEqual(status.message_id, {"12345", "45678"})
self.assertEqual(status.recipients, recipients)
self.assertEqual(repr(status),
"AnymailStatus<status={'queued', 'sent'}, message_id={'12345', '45678'}, 2 recipients>")
self.assertEqual(
repr(status),
"AnymailStatus<status={'queued', 'sent'},"
" message_id={'12345', '45678'}, 2 recipients>",
)
def test_multiple_recipients_same_message_id(self):
# status.message_id collapses when it's the same for all recipients
@@ -69,8 +82,11 @@ class AnymailStatusTests(AnymailTestMixin, SimpleTestCase):
status = AnymailStatus()
status.set_recipient_status(recipients)
self.assertEqual(status.message_id, "12345")
self.assertEqual(repr(status),
"AnymailStatus<status={'queued', 'sent'}, message_id='12345', 2 recipients>")
self.assertEqual(
repr(status),
"AnymailStatus<status={'queued', 'sent'},"
" message_id='12345', 2 recipients>",
)
def test_none(self):
status = AnymailStatus()

View File

@@ -8,16 +8,33 @@ from django.core.exceptions import ImproperlyConfigured
from django.test import SimpleTestCase, override_settings, tag
from anymail.exceptions import (
AnymailAPIError, AnymailSerializationError,
AnymailUnsupportedFeature)
AnymailAPIError,
AnymailSerializationError,
AnymailUnsupportedFeature,
)
from anymail.message import attach_inline_image_file
from .mock_requests_backend import RequestsBackendMockAPITestCase, SessionSharingTestCases
from .utils import sample_image_content, sample_image_path, SAMPLE_IMAGE_FILENAME, AnymailTestMixin, decode_att
from .mock_requests_backend import (
RequestsBackendMockAPITestCase,
SessionSharingTestCases,
)
from .utils import (
SAMPLE_IMAGE_FILENAME,
AnymailTestMixin,
decode_att,
sample_image_content,
sample_image_path,
)
@tag('postal')
@override_settings(EMAIL_BACKEND='anymail.backends.postal.EmailBackend',
ANYMAIL={'POSTAL_API_KEY': 'test_server_token', 'POSTAL_API_URL': 'https://postal.example.com'})
@tag("postal")
@override_settings(
EMAIL_BACKEND="anymail.backends.postal.EmailBackend",
ANYMAIL={
"POSTAL_API_KEY": "test_server_token",
"POSTAL_API_URL": "https://postal.example.com",
},
)
class PostalBackendMockAPITestCase(RequestsBackendMockAPITestCase):
DEFAULT_RAW_RESPONSE = b"""{
"status": "success",
@@ -34,25 +51,32 @@ class PostalBackendMockAPITestCase(RequestsBackendMockAPITestCase):
def setUp(self):
super().setUp()
# Simple message useful for many tests
self.message = mail.EmailMultiAlternatives('Subject', 'Text Body', 'from@example.com', ['to@example.com'])
self.message = mail.EmailMultiAlternatives(
"Subject", "Text Body", "from@example.com", ["to@example.com"]
)
@tag('postal')
@tag("postal")
class PostalBackendStandardEmailTests(PostalBackendMockAPITestCase):
"""Test backend support for Django standard email features"""
def test_send_mail(self):
"""Test basic API for simple send"""
mail.send_mail('Subject here', 'Here is the message.',
'from@sender.example.com', ['to@example.com'], fail_silently=False)
self.assert_esp_called('/message')
mail.send_mail(
"Subject here",
"Here is the message.",
"from@sender.example.com",
["to@example.com"],
fail_silently=False,
)
self.assert_esp_called("/message")
headers = self.get_api_call_headers()
self.assertEqual(headers["X-Server-API-Key"], "test_server_token")
data = self.get_api_call_json()
self.assertEqual(data['subject'], "Subject here")
self.assertEqual(data['plain_body'], "Here is the message.")
self.assertEqual(data['from'], "from@sender.example.com")
self.assertEqual(data['to'], ["to@example.com"])
self.assertEqual(data["subject"], "Subject here")
self.assertEqual(data["plain_body"], "Here is the message.")
self.assertEqual(data["from"], "from@sender.example.com")
self.assertEqual(data["to"], ["to@example.com"])
def test_name_addr(self):
"""Make sure RFC2822 name-addr format (with display-name) is allowed
@@ -60,93 +84,124 @@ class PostalBackendStandardEmailTests(PostalBackendMockAPITestCase):
(Test both sender and recipient addresses)
"""
msg = mail.EmailMessage(
'Subject', 'Message', 'From Name <from@example.com>',
['Recipient #1 <to1@example.com>', 'to2@example.com'],
cc=['Carbon Copy <cc1@example.com>', 'cc2@example.com'],
bcc=['Blind Copy <bcc1@example.com>', 'bcc2@example.com'])
"Subject",
"Message",
"From Name <from@example.com>",
["Recipient #1 <to1@example.com>", "to2@example.com"],
cc=["Carbon Copy <cc1@example.com>", "cc2@example.com"],
bcc=["Blind Copy <bcc1@example.com>", "bcc2@example.com"],
)
msg.send()
data = self.get_api_call_json()
self.assertEqual(data['from'], 'From Name <from@example.com>')
self.assertEqual(data['to'], ['Recipient #1 <to1@example.com>', 'to2@example.com'])
self.assertEqual(data['cc'], ['Carbon Copy <cc1@example.com>', 'cc2@example.com'])
self.assertEqual(data['bcc'], ['Blind Copy <bcc1@example.com>', 'bcc2@example.com'])
self.assertEqual(data["from"], "From Name <from@example.com>")
self.assertEqual(
data["to"], ["Recipient #1 <to1@example.com>", "to2@example.com"]
)
self.assertEqual(
data["cc"], ["Carbon Copy <cc1@example.com>", "cc2@example.com"]
)
self.assertEqual(
data["bcc"], ["Blind Copy <bcc1@example.com>", "bcc2@example.com"]
)
def test_email_message(self):
email = mail.EmailMessage(
'Subject', 'Body goes here', 'from@example.com',
['to1@example.com', 'Also To <to2@example.com>'],
bcc=['bcc1@example.com', 'Also BCC <bcc2@example.com>'],
cc=['cc1@example.com', 'Also CC <cc2@example.com>'],
headers={'Reply-To': 'another@example.com',
'X-MyHeader': 'my value',
'Message-ID': 'mycustommsgid@sales.example.com'}) # should override backend msgid
"Subject",
"Body goes here",
"from@example.com",
["to1@example.com", "Also To <to2@example.com>"],
bcc=["bcc1@example.com", "Also BCC <bcc2@example.com>"],
cc=["cc1@example.com", "Also CC <cc2@example.com>"],
headers={
"Reply-To": "another@example.com",
"X-MyHeader": "my value",
# should override backend msgid:
"Message-ID": "mycustommsgid@sales.example.com",
},
)
email.send()
data = self.get_api_call_json()
self.assertEqual(data['subject'], "Subject")
self.assertEqual(data['plain_body'], "Body goes here")
self.assertEqual(data['from'], "from@example.com")
self.assertEqual(data['to'], ['to1@example.com', 'Also To <to2@example.com>'])
self.assertEqual(data['bcc'], ['bcc1@example.com', 'Also BCC <bcc2@example.com>'])
self.assertEqual(data['cc'], ['cc1@example.com', 'Also CC <cc2@example.com>'])
self.assertEqual(data['reply_to'], 'another@example.com')
self.assertCountEqual(data['headers'], {
'Message-ID': 'mycustommsgid@sales.example.com',
'X-MyHeader': 'my value'
})
self.assertEqual(data["subject"], "Subject")
self.assertEqual(data["plain_body"], "Body goes here")
self.assertEqual(data["from"], "from@example.com")
self.assertEqual(data["to"], ["to1@example.com", "Also To <to2@example.com>"])
self.assertEqual(
data["bcc"], ["bcc1@example.com", "Also BCC <bcc2@example.com>"]
)
self.assertEqual(data["cc"], ["cc1@example.com", "Also CC <cc2@example.com>"])
self.assertEqual(data["reply_to"], "another@example.com")
self.assertCountEqual(
data["headers"],
{"Message-ID": "mycustommsgid@sales.example.com", "X-MyHeader": "my value"},
)
def test_html_message(self):
text_content = 'This is an important message.'
html_content = '<p>This is an <strong>important</strong> message.</p>'
email = mail.EmailMultiAlternatives('Subject', text_content,
'from@example.com', ['to@example.com'])
text_content = "This is an important message."
html_content = "<p>This is an <strong>important</strong> message.</p>"
email = mail.EmailMultiAlternatives(
"Subject", text_content, "from@example.com", ["to@example.com"]
)
email.attach_alternative(html_content, "text/html")
email.send()
data = self.get_api_call_json()
self.assertEqual(data['plain_body'], text_content)
self.assertEqual(data['html_body'], html_content)
self.assertEqual(data["plain_body"], text_content)
self.assertEqual(data["html_body"], html_content)
# Don't accidentally send the html part as an attachment:
self.assertNotIn('attachments', data)
self.assertNotIn("attachments", data)
def test_html_only_message(self):
html_content = '<p>This is an <strong>important</strong> message.</p>'
email = mail.EmailMessage('Subject', html_content, 'from@example.com', ['to@example.com'])
html_content = "<p>This is an <strong>important</strong> message.</p>"
email = mail.EmailMessage(
"Subject", html_content, "from@example.com", ["to@example.com"]
)
email.content_subtype = "html" # Main content is now text/html
email.send()
data = self.get_api_call_json()
self.assertNotIn('plain_body', data)
self.assertEqual(data['html_body'], html_content)
self.assertNotIn("plain_body", data)
self.assertEqual(data["html_body"], html_content)
def test_extra_headers(self):
self.message.extra_headers = {'X-Custom': 'string', 'X-Num': 123}
self.message.extra_headers = {"X-Custom": "string", "X-Num": 123}
self.message.send()
data = self.get_api_call_json()
self.assertCountEqual(data['headers'], {
'X-Custom': 'string',
'X-Num': 123
})
self.assertCountEqual(data["headers"], {"X-Custom": "string", "X-Num": 123})
def test_extra_headers_serialization_error(self):
self.message.extra_headers = {'X-Custom': Decimal(12.5)}
self.message.extra_headers = {"X-Custom": Decimal(12.5)}
with self.assertRaisesMessage(AnymailSerializationError, "Decimal"):
self.message.send()
@override_settings(ANYMAIL_IGNORE_UNSUPPORTED_FEATURES=True) # Postal only allows single reply-to
@override_settings(ANYMAIL_IGNORE_UNSUPPORTED_FEATURES=True)
def test_reply_to(self):
email = mail.EmailMessage('Subject', 'Body goes here', 'from@example.com', ['to1@example.com'],
reply_to=['reply@example.com', 'Other <reply2@example.com>'])
# Postal only allows single reply-to. Test handling for multiple reply
# addresses when ignoring errors:
email = mail.EmailMessage(
"Subject",
"Body goes here",
"from@example.com",
["to1@example.com"],
reply_to=["reply@example.com", "Other <reply2@example.com>"],
)
email.send()
data = self.get_api_call_json()
self.assertEqual(data['reply_to'], 'reply@example.com') # keeps first email
self.assertEqual(data["reply_to"], "reply@example.com") # keeps first email
def test_multiple_reply_to(self):
email = mail.EmailMessage('Subject', 'Body goes here', 'from@example.com', ['to1@example.com'],
reply_to=['reply@example.com', 'Other <reply2@example.com>'])
email = mail.EmailMessage(
"Subject",
"Body goes here",
"from@example.com",
["to1@example.com"],
reply_to=["reply@example.com", "Other <reply2@example.com>"],
)
with self.assertRaises(AnymailUnsupportedFeature):
email.send()
def test_attachments(self):
text_content = "* Item one\n* Item two\n* Item three"
self.message.attach(filename="test.txt", content=text_content, mimetype="text/plain")
self.message.attach(
filename="test.txt", content=text_content, mimetype="text/plain"
)
# Should guess mimetype if not provided...
png_content = b"PNG\xb4 pretend this is the contents of a png file"
@@ -154,19 +209,22 @@ class PostalBackendStandardEmailTests(PostalBackendMockAPITestCase):
# Should work with a MIMEBase object (also tests no filename)...
pdf_content = b"PDF\xb4 pretend this is valid pdf data"
mimeattachment = MIMEBase('application', 'pdf')
mimeattachment = MIMEBase("application", "pdf")
mimeattachment.set_payload(pdf_content)
self.message.attach(mimeattachment)
self.message.send()
data = self.get_api_call_json()
attachments = data['attachments']
attachments = data["attachments"]
self.assertEqual(len(attachments), 3)
self.assertEqual(attachments[0]["name"], "test.txt")
self.assertEqual(attachments[0]["content_type"], "text/plain")
self.assertEqual(decode_att(attachments[0]["data"]).decode('ascii'), text_content)
self.assertEqual(
decode_att(attachments[0]["data"]).decode("ascii"), text_content
)
self.assertEqual(attachments[1]["content_type"], "image/png") # inferred from filename
# content_type inferred from filename:
self.assertEqual(attachments[1]["content_type"], "image/png")
self.assertEqual(attachments[1]["name"], "test.png")
self.assertEqual(decode_att(attachments[1]["data"]), png_content)
@@ -175,24 +233,33 @@ class PostalBackendStandardEmailTests(PostalBackendMockAPITestCase):
self.assertEqual(decode_att(attachments[2]["data"]), pdf_content)
def test_unicode_attachment_correctly_decoded(self):
self.message.attach("Une pièce jointe.html", '<p>\u2019</p>', mimetype='text/html')
self.message.attach(
"Une pièce jointe.html", "<p>\u2019</p>", mimetype="text/html"
)
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['attachments'], [{
'name': 'Une pièce jointe.html',
'content_type': 'text/html',
'data': b64encode('<p>\u2019</p>'.encode('utf-8')).decode('ascii')
}])
self.assertEqual(
data["attachments"],
[
{
"name": "Une pièce jointe.html",
"content_type": "text/html",
"data": b64encode("<p>\u2019</p>".encode("utf-8")).decode("ascii"),
}
],
)
def test_embedded_images(self):
image_filename = SAMPLE_IMAGE_FILENAME
image_path = sample_image_path(image_filename)
cid = attach_inline_image_file(self.message, image_path) # Read from a png file
html_content = '<p>This has an <img src="cid:%s" alt="inline" /> image.</p>' % cid
html_content = (
'<p>This has an <img src="cid:%s" alt="inline" /> image.</p>' % cid
)
self.message.attach_alternative(html_content, "text/html")
with self.assertRaisesMessage(AnymailUnsupportedFeature, 'inline attachments'):
with self.assertRaisesMessage(AnymailUnsupportedFeature, "inline attachments"):
self.message.send()
def test_attached_images(self):
@@ -200,33 +267,38 @@ class PostalBackendStandardEmailTests(PostalBackendMockAPITestCase):
image_path = sample_image_path(image_filename)
image_data = sample_image_content(image_filename)
self.message.attach_file(image_path) # option 1: attach as a file
# option 1: attach as a file
self.message.attach_file(image_path)
image = MIMEImage(image_data) # option 2: construct the MIMEImage and attach it directly
# option 2: construct the MIMEImage and attach it directly
image = MIMEImage(image_data)
self.message.attach(image)
image_data_b64 = b64encode(image_data).decode('ascii')
image_data_b64 = b64encode(image_data).decode("ascii")
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['attachments'], [
{
'name': image_filename, # the named one
'content_type': 'image/png',
'data': image_data_b64,
},
{
'name': '', # the unnamed one
'content_type': 'image/png',
'data': image_data_b64,
},
])
self.assertEqual(
data["attachments"],
[
{
"name": image_filename, # the named one
"content_type": "image/png",
"data": image_data_b64,
},
{
"name": "", # the unnamed one
"content_type": "image/png",
"data": image_data_b64,
},
],
)
def test_multiple_html_alternatives(self):
# Multiple alternatives not allowed
self.message.attach_alternative("<p>First html is OK</p>", "text/html")
self.message.attach_alternative("<p>But not second html</p>", "text/html")
with self.assertRaisesMessage(AnymailUnsupportedFeature, 'multiple html parts'):
with self.assertRaisesMessage(AnymailUnsupportedFeature, "multiple html parts"):
self.message.send()
def test_html_alternative(self):
@@ -246,16 +318,16 @@ class PostalBackendStandardEmailTests(PostalBackendMockAPITestCase):
"""Empty to, cc, bcc, and reply_to shouldn't generate empty fields"""
self.message.send()
data = self.get_api_call_json()
self.assertNotIn('Cc', data)
self.assertNotIn('Bcc', data)
self.assertNotIn('ReplyTo', data)
self.assertNotIn("Cc", data)
self.assertNotIn("Bcc", data)
self.assertNotIn("ReplyTo", data)
# Test empty `to` -- but send requires at least one recipient somewhere (like cc)
# Test empty `to`--but send requires at least one recipient somewhere (like cc)
self.message.to = []
self.message.cc = ['cc@example.com']
self.message.cc = ["cc@example.com"]
self.message.send()
data = self.get_api_call_json()
self.assertNotIn('To', data)
self.assertNotIn("To", data)
def test_api_failure(self):
failure_response = b"""{
@@ -268,11 +340,17 @@ class PostalBackendStandardEmailTests(PostalBackendMockAPITestCase):
}"""
self.set_mock_response(status_code=200, raw=failure_response)
with self.assertRaisesMessage(AnymailAPIError, "Postal API response 200"):
mail.send_mail('Subject', 'Body', 'from@example.com', ['to@example.com'])
mail.send_mail("Subject", "Body", "from@example.com", ["to@example.com"])
# Make sure fail_silently is respected
self.set_mock_response(status_code=200, raw=failure_response)
sent = mail.send_mail('Subject', 'Body', 'from@example.com', ['to@example.com'], fail_silently=True)
sent = mail.send_mail(
"Subject",
"Body",
"from@example.com",
["to@example.com"],
fail_silently=True,
)
self.assertEqual(sent, 0)
def test_api_error_includes_details(self):
@@ -301,7 +379,7 @@ class PostalBackendStandardEmailTests(PostalBackendMockAPITestCase):
self.message.send()
@tag('postal')
@tag("postal")
class PostalBackendAnymailFeatureTests(PostalBackendMockAPITestCase):
"""Test backend support for Anymail added features"""
@@ -312,33 +390,33 @@ class PostalBackendAnymailFeatureTests(PostalBackendMockAPITestCase):
self.assertEqual(data["sender"], "anything@bounces.example.com")
def test_metadata(self):
self.message.metadata = {'user_id': "12345", 'items': 6}
with self.assertRaisesMessage(AnymailUnsupportedFeature, 'metadata'):
self.message.metadata = {"user_id": "12345", "items": 6}
with self.assertRaisesMessage(AnymailUnsupportedFeature, "metadata"):
self.message.send()
def test_send_at(self):
self.message.send_at = 1651820889 # 2022-05-06 07:08:09 UTC
with self.assertRaisesMessage(AnymailUnsupportedFeature, 'send_at'):
with self.assertRaisesMessage(AnymailUnsupportedFeature, "send_at"):
self.message.send()
def test_tags(self):
self.message.tags = ["receipt"]
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['tag'], "receipt")
self.assertEqual(data["tag"], "receipt")
self.message.tags = ["receipt", "repeat-user"]
with self.assertRaisesMessage(AnymailUnsupportedFeature, 'multiple tags'):
with self.assertRaisesMessage(AnymailUnsupportedFeature, "multiple tags"):
self.message.send()
def test_track_opens(self):
self.message.track_opens = True
with self.assertRaisesMessage(AnymailUnsupportedFeature, 'track_opens'):
with self.assertRaisesMessage(AnymailUnsupportedFeature, "track_opens"):
self.message.send()
def test_track_clicks(self):
self.message.track_clicks = True
with self.assertRaisesMessage(AnymailUnsupportedFeature, 'track_clicks'):
with self.assertRaisesMessage(AnymailUnsupportedFeature, "track_clicks"):
self.message.send()
def test_default_omits_options(self):
@@ -350,19 +428,19 @@ class PostalBackendAnymailFeatureTests(PostalBackendMockAPITestCase):
"""
self.message.send()
data = self.get_api_call_json()
self.assertNotIn('tag', data)
self.assertNotIn("tag", data)
def test_esp_extra(self):
self.message.esp_extra = {
'future_postal_option': 'some-value',
"future_postal_option": "some-value",
}
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['future_postal_option'], 'some-value')
self.assertEqual(data["future_postal_option"], "some-value")
# noinspection PyUnresolvedReferences
def test_send_attaches_anymail_status(self):
""" The anymail_status should be attached to the message when it is sent """
"""The anymail_status should be attached to the message when it is sent"""
response_content = b"""{
"status": "success",
"time": 1.08,
@@ -375,14 +453,22 @@ class PostalBackendAnymailFeatureTests(PostalBackendMockAPITestCase):
}
}"""
self.set_mock_response(raw=response_content)
msg = mail.EmailMessage('Subject', 'Message', 'from@example.com', ['Recipient <to1@example.com>'],)
msg = mail.EmailMessage(
"Subject",
"Message",
"from@example.com",
["Recipient <to1@example.com>"],
)
sent = msg.send()
self.assertEqual(sent, 1)
self.assertEqual(msg.anymail_status.status, {'queued'})
self.assertEqual(msg.anymail_status.status, {"queued"})
self.assertEqual(msg.anymail_status.message_id, 1531)
self.assertEqual(msg.anymail_status.recipients['to1@example.com'].status, 'queued')
self.assertEqual(msg.anymail_status.recipients['to1@example.com'].message_id,
1531)
self.assertEqual(
msg.anymail_status.recipients["to1@example.com"].status, "queued"
)
self.assertEqual(
msg.anymail_status.recipients["to1@example.com"].message_id, 1531
)
self.assertEqual(msg.anymail_status.esp_response.content, response_content)
# noinspection PyUnresolvedReferences
@@ -400,19 +486,27 @@ class PostalBackendAnymailFeatureTests(PostalBackendMockAPITestCase):
}
}"""
self.set_mock_response(raw=response_content)
msg = mail.EmailMessage('Subject', 'Message', 'from@example.com', cc=['cc@example.com'],)
msg = mail.EmailMessage(
"Subject",
"Message",
"from@example.com",
cc=["cc@example.com"],
)
sent = msg.send()
self.assertEqual(sent, 1)
self.assertEqual(msg.anymail_status.status, {'queued'})
self.assertEqual(msg.anymail_status.status, {"queued"})
self.assertEqual(msg.anymail_status.message_id, 1531)
self.assertEqual(msg.anymail_status.recipients['cc@example.com'].status, 'queued')
self.assertEqual(msg.anymail_status.recipients['cc@example.com'].message_id,
1531)
self.assertEqual(
msg.anymail_status.recipients["cc@example.com"].status, "queued"
)
self.assertEqual(
msg.anymail_status.recipients["cc@example.com"].message_id, 1531
)
self.assertEqual(msg.anymail_status.esp_response.content, response_content)
# noinspection PyUnresolvedReferences
def test_send_failed_anymail_status(self):
""" If the send fails, anymail_status should contain initial values"""
"""If the send fails, anymail_status should contain initial values"""
self.set_mock_response(status_code=500)
sent = self.message.send(fail_silently=True)
self.assertEqual(sent, 0)
@@ -423,9 +517,12 @@ class PostalBackendAnymailFeatureTests(PostalBackendMockAPITestCase):
# noinspection PyUnresolvedReferences
def test_send_unparsable_response(self):
"""If the send succeeds, but a non-JSON API response, should raise an API exception"""
mock_response = self.set_mock_response(status_code=200,
raw=b"yikes, this isn't a real response")
"""
If the send succeeds, but a non-JSON API response, should raise an API exception
"""
mock_response = self.set_mock_response(
status_code=200, raw=b"yikes, this isn't a real response"
)
with self.assertRaises(AnymailAPIError):
self.message.send()
self.assertIsNone(self.message.anymail_status.status)
@@ -435,17 +532,19 @@ class PostalBackendAnymailFeatureTests(PostalBackendMockAPITestCase):
def test_json_serialization_errors(self):
"""Try to provide more information about non-json-serializable data"""
self.message.tags = [Decimal('19.99')] # yeah, don't do this
self.message.tags = [Decimal("19.99")] # yeah, don't do this
with self.assertRaises(AnymailSerializationError) as cm:
self.message.send()
print(self.get_api_call_json())
err = cm.exception
self.assertIsInstance(err, TypeError) # compatibility with json.dumps
self.assertIn("Don't know how to send this data to Postal", str(err)) # our added context
self.assertRegex(str(err), r"Decimal.*is not JSON serializable") # original message
# our added context:
self.assertIn("Don't know how to send this data to Postal", str(err))
# original message:
self.assertRegex(str(err), r"Decimal.*is not JSON serializable")
@tag('postal')
@tag("postal")
class PostalBackendRecipientsRefusedTests(PostalBackendMockAPITestCase):
# Postal doesn't check email bounce or complaint lists at time of send --
# it always just queues the message. You'll need to listen for the "rejected"
@@ -453,20 +552,23 @@ class PostalBackendRecipientsRefusedTests(PostalBackendMockAPITestCase):
pass
@tag('postal')
class PostalBackendSessionSharingTestCase(SessionSharingTestCases, PostalBackendMockAPITestCase):
@tag("postal")
class PostalBackendSessionSharingTestCase(
SessionSharingTestCases, PostalBackendMockAPITestCase
):
"""Requests session sharing tests"""
pass # tests are defined in SessionSharingTestCases
@tag('postal')
@tag("postal")
@override_settings(EMAIL_BACKEND="anymail.backends.postal.EmailBackend")
class PostalBackendImproperlyConfiguredTests(AnymailTestMixin, SimpleTestCase):
"""Test ESP backend without required settings in place"""
def test_missing_api_key(self):
with self.assertRaises(ImproperlyConfigured) as cm:
mail.send_mail('Subject', 'Message', 'from@example.com', ['to@example.com'])
mail.send_mail("Subject", "Message", "from@example.com", ["to@example.com"])
errmsg = str(cm.exception)
self.assertRegex(errmsg, r'\bPOSTAL_API_KEY\b')
self.assertRegex(errmsg, r'\bANYMAIL_POSTAL_API_KEY\b')
self.assertRegex(errmsg, r"\bPOSTAL_API_KEY\b")
self.assertRegex(errmsg, r"\bANYMAIL_POSTAL_API_KEY\b")

View File

@@ -10,13 +10,16 @@ from anymail.exceptions import AnymailConfigurationError
from anymail.inbound import AnymailInboundMessage
from anymail.signals import AnymailInboundEvent
from anymail.webhooks.postal import PostalInboundWebhookView
from .utils import sample_image_content, sample_email_content
from .utils import sample_email_content, sample_image_content
from .utils_postal import ClientWithPostalSignature, make_key
from .webhook_cases import WebhookTestCase
@tag('postal')
@unittest.skipUnless(ClientWithPostalSignature, "Install 'cryptography' to run postal webhook tests")
@tag("postal")
@unittest.skipUnless(
ClientWithPostalSignature, "Install 'cryptography' to run postal webhook tests"
)
class PostalInboundTestCase(WebhookTestCase):
client_class = ClientWithPostalSignature
@@ -31,7 +34,9 @@ class PostalInboundTestCase(WebhookTestCase):
"id": 233980,
"rcpt_to": "test@inbound.example.com",
"mail_from": "envelope-from@example.org",
"message": b64encode(dedent("""\
"message": b64encode(
dedent(
"""\
Received: from mail.example.org by postal.example.com ...
Received: by mail.example.org for <test@inbound.example.com> ...
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.org; ...
@@ -58,20 +63,30 @@ class PostalInboundTestCase(WebhookTestCase):
<div dir=3D"ltr">It's a body=E2=80=A6</div>
--94eb2c05e174adb140055b6339c5--
""").encode('utf-8')).decode('ascii'),
""" # NOQA: E501
).encode("utf-8")
).decode("ascii"),
"base64": True,
}
response = self.client.post('/anymail/postal/inbound/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/postal/inbound/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.inbound_handler, sender=PostalInboundWebhookView,
event=ANY, esp_name='Postal')
kwargs = self.assert_handler_called_once_with(
self.inbound_handler,
sender=PostalInboundWebhookView,
event=ANY,
esp_name="Postal",
)
# AnymailInboundEvent
event = kwargs['event']
event = kwargs["event"]
self.assertIsInstance(event, AnymailInboundEvent)
self.assertEqual(event.event_type, 'inbound')
self.assertIsNone(event.timestamp) # Postal doesn't provide inbound event timestamp
self.assertEqual(event.event_type, "inbound")
# Postal doesn't provide inbound event timestamp:
self.assertIsNone(event.timestamp)
self.assertEqual(event.event_id, 233980)
self.assertIsInstance(event.message, AnymailInboundMessage)
self.assertEqual(event.esp_event, raw_event)
@@ -79,36 +94,44 @@ class PostalInboundTestCase(WebhookTestCase):
# AnymailInboundMessage - convenience properties
message = event.message
self.assertEqual(message.from_email.display_name, 'Displayed From')
self.assertEqual(message.from_email.addr_spec, 'from+test@example.org')
self.assertEqual([str(e) for e in message.to],
['Test Inbound <test@inbound.example.com>', 'other@example.com'])
self.assertEqual([str(e) for e in message.cc],
['cc@example.com'])
self.assertEqual(message.subject, 'Test subject')
self.assertEqual(message.from_email.display_name, "Displayed From")
self.assertEqual(message.from_email.addr_spec, "from+test@example.org")
self.assertEqual(
[str(e) for e in message.to],
["Test Inbound <test@inbound.example.com>", "other@example.com"],
)
self.assertEqual([str(e) for e in message.cc], ["cc@example.com"])
self.assertEqual(message.subject, "Test subject")
self.assertEqual(message.date.isoformat(" "), "2017-10-11 18:31:04-07:00")
self.assertEqual(message.text, "It's a body\N{HORIZONTAL ELLIPSIS}\n")
self.assertEqual(message.html, """<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\n""")
self.assertEqual(
message.html,
"""<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\n""",
)
self.assertEqual(message.envelope_sender, 'envelope-from@example.org')
self.assertEqual(message.envelope_recipient, 'test@inbound.example.com')
self.assertEqual(message.envelope_sender, "envelope-from@example.org")
self.assertEqual(message.envelope_recipient, "test@inbound.example.com")
self.assertIsNone(message.stripped_text)
self.assertIsNone(message.stripped_html)
self.assertIsNone(message.spam_detected)
self.assertIsNone(message.spam_score)
# AnymailInboundMessage - other headers
self.assertEqual(message['Message-ID'], "<CAEPk3R+4Zr@mail.example.org>")
self.assertEqual(message.get_all('Received'), [
"from mail.example.org by postal.example.com ...",
"by mail.example.org for <test@inbound.example.com> ...",
"by 10.10.1.71 with HTTP; Wed, 11 Oct 2017 18:31:04 -0700 (PDT)",
])
self.assertEqual(message["Message-ID"], "<CAEPk3R+4Zr@mail.example.org>")
self.assertEqual(
message.get_all("Received"),
[
"from mail.example.org by postal.example.com ...",
"by mail.example.org for <test@inbound.example.com> ...",
"by 10.10.1.71 with HTTP; Wed, 11 Oct 2017 18:31:04 -0700 (PDT)",
],
)
def test_attachments(self):
image_content = sample_image_content()
email_content = sample_email_content()
raw_mime = dedent("""\
raw_mime = dedent(
"""\
MIME-Version: 1.0
From: from@example.org
Subject: Attachments
@@ -143,41 +166,59 @@ class PostalInboundTestCase(WebhookTestCase):
{email_content}
--boundary0--
""").format(image_content_base64=b64encode(image_content).decode('ascii'),
email_content=email_content.decode('ascii'))
""" # NOQA: E501
).format(
image_content_base64=b64encode(image_content).decode("ascii"),
email_content=email_content.decode("ascii"),
)
raw_event = {
"id": 233980,
"rcpt_to": "test@inbound.example.com",
"mail_from": "envelope-from@example.org",
"message": b64encode(raw_mime.encode('utf-8')).decode('ascii'),
"message": b64encode(raw_mime.encode("utf-8")).decode("ascii"),
"base64": True,
}
response = self.client.post('/anymail/postal/inbound/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/postal/inbound/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.inbound_handler, sender=PostalInboundWebhookView,
event=ANY, esp_name='Postal')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.inbound_handler,
sender=PostalInboundWebhookView,
event=ANY,
esp_name="Postal",
)
event = kwargs["event"]
message = event.message
attachments = message.attachments # AnymailInboundMessage convenience accessor
self.assertEqual(len(attachments), 2)
self.assertEqual(attachments[0].get_filename(), 'test.txt')
self.assertEqual(attachments[0].get_content_type(), 'text/plain')
self.assertEqual(attachments[0].get_content_text(), 'test attachment')
self.assertEqual(attachments[1].get_content_type(), 'message/rfc822')
self.assertEqualIgnoringHeaderFolding(attachments[1].get_content_bytes(), email_content)
self.assertEqual(attachments[0].get_filename(), "test.txt")
self.assertEqual(attachments[0].get_content_type(), "text/plain")
self.assertEqual(attachments[0].get_content_text(), "test attachment")
self.assertEqual(attachments[1].get_content_type(), "message/rfc822")
self.assertEqualIgnoringHeaderFolding(
attachments[1].get_content_bytes(), email_content
)
inlines = message.inline_attachments
self.assertEqual(len(inlines), 1)
inline = inlines['abc123']
self.assertEqual(inline.get_filename(), 'image.png')
self.assertEqual(inline.get_content_type(), 'image/png')
inline = inlines["abc123"]
self.assertEqual(inline.get_filename(), "image.png")
self.assertEqual(inline.get_content_type(), "image/png")
self.assertEqual(inline.get_content_bytes(), image_content)
def test_misconfigured_tracking(self):
errmsg = "You seem to have set Postal's *tracking* webhook to Anymail's Postal *inbound* webhook URL."
with self.assertRaisesMessage(AnymailConfigurationError, errmsg):
self.client.post('/anymail/postal/inbound/', content_type='application/json',
data=json.dumps({"status": "Held"}))
with self.assertRaisesMessage(
AnymailConfigurationError,
"You seem to have set Postal's *tracking* webhook"
" to Anymail's Postal *inbound* webhook URL.",
):
self.client.post(
"/anymail/postal/inbound/",
content_type="application/json",
data=json.dumps({"status": "Held"}),
)

View File

@@ -9,20 +9,24 @@ from anymail.message import AnymailMessage
from .utils import AnymailTestMixin
ANYMAIL_TEST_POSTAL_API_KEY = os.getenv('ANYMAIL_TEST_POSTAL_API_KEY')
ANYMAIL_TEST_POSTAL_API_URL = os.getenv('ANYMAIL_TEST_POSTAL_API_URL')
ANYMAIL_TEST_POSTAL_DOMAIN = os.getenv('ANYMAIL_TEST_POSTAL_DOMAIN')
ANYMAIL_TEST_POSTAL_API_KEY = os.getenv("ANYMAIL_TEST_POSTAL_API_KEY")
ANYMAIL_TEST_POSTAL_API_URL = os.getenv("ANYMAIL_TEST_POSTAL_API_URL")
ANYMAIL_TEST_POSTAL_DOMAIN = os.getenv("ANYMAIL_TEST_POSTAL_DOMAIN")
@tag('postal', 'live')
@tag("postal", "live")
@unittest.skipUnless(
ANYMAIL_TEST_POSTAL_API_KEY and ANYMAIL_TEST_POSTAL_API_URL and ANYMAIL_TEST_POSTAL_DOMAIN,
"Set ANYMAIL_TEST_POSTAL_API_KEY and ANYMAIL_TEST_POSTAL_API_URL and ANYMAIL_TEST_POSTAL_DOMAIN "
"environment variables to run Postal integration tests")
@override_settings(ANYMAIL_POSTAL_API_KEY=ANYMAIL_TEST_POSTAL_API_KEY,
ANYMAIL_POSTAL_API_URL=ANYMAIL_TEST_POSTAL_API_URL,
EMAIL_BACKEND="anymail.backends.postal.EmailBackend")
ANYMAIL_TEST_POSTAL_API_KEY
and ANYMAIL_TEST_POSTAL_API_URL
and ANYMAIL_TEST_POSTAL_DOMAIN,
"Set ANYMAIL_TEST_POSTAL_API_KEY and ANYMAIL_TEST_POSTAL_API_URL and"
" ANYMAIL_TEST_POSTAL_DOMAIN environment variables to run Postal integration tests",
)
@override_settings(
ANYMAIL_POSTAL_API_KEY=ANYMAIL_TEST_POSTAL_API_KEY,
ANYMAIL_POSTAL_API_URL=ANYMAIL_TEST_POSTAL_API_URL,
EMAIL_BACKEND="anymail.backends.postal.EmailBackend",
)
class PostalBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
"""Postal API integration tests
@@ -34,10 +38,14 @@ class PostalBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
def setUp(self):
super().setUp()
self.from_email = 'from@%s' % ANYMAIL_TEST_POSTAL_DOMAIN
self.message = AnymailMessage('Anymail Postal integration test', 'Text content',
self.from_email, ['test+to1@anymail.dev'])
self.message.attach_alternative('<p>HTML content</p>', "text/html")
self.from_email = "from@%s" % ANYMAIL_TEST_POSTAL_DOMAIN
self.message = AnymailMessage(
"Anymail Postal integration test",
"Text content",
self.from_email,
["test+to1@anymail.dev"],
)
self.message.attach_alternative("<p>HTML content</p>", "text/html")
def test_simple_send(self):
# Example of getting the Postal send status and message id from the message
@@ -45,12 +53,13 @@ class PostalBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
self.assertEqual(sent_count, 1)
anymail_status = self.message.anymail_status
sent_status = anymail_status.recipients['test+to1@anymail.dev'].status
message_id = anymail_status.recipients['test+to1@anymail.dev'].message_id
sent_status = anymail_status.recipients["test+to1@anymail.dev"].status
message_id = anymail_status.recipients["test+to1@anymail.dev"].message_id
self.assertEqual(sent_status, 'queued')
self.assertEqual(sent_status, "queued")
self.assertGreater(len(message_id), 0) # non-empty string
self.assertEqual(anymail_status.status, {sent_status}) # set of all recipient statuses
# set of all recipient statuses:
self.assertEqual(anymail_status.status, {sent_status})
self.assertEqual(anymail_status.message_id, message_id)
def test_all_options(self):
@@ -70,23 +79,32 @@ class PostalBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
message.attach("attachment2.csv", "ID,Name\n1,Amy Lina", "text/csv")
message.send()
self.assertEqual(message.anymail_status.status, {'queued'})
self.assertEqual(message.anymail_status.recipients['test+to1@anymail.dev'].status, 'queued')
self.assertEqual(message.anymail_status.recipients['test+to2@anymail.dev'].status, 'queued')
self.assertEqual(message.anymail_status.status, {"queued"})
self.assertEqual(
message.anymail_status.recipients["test+to1@anymail.dev"].status, "queued"
)
self.assertEqual(
message.anymail_status.recipients["test+to2@anymail.dev"].status, "queued"
)
# distinct messages should have different message_ids:
self.assertNotEqual(message.anymail_status.recipients['test+to1@anymail.dev'].message_id,
message.anymail_status.recipients['teset+to2@anymail.dev'].message_id)
self.assertNotEqual(
message.anymail_status.recipients["test+to1@anymail.dev"].message_id,
message.anymail_status.recipients["teset+to2@anymail.dev"].message_id,
)
def test_invalid_from(self):
self.message.from_email = 'webmaster@localhost' # Django's default From
self.message.from_email = "webmaster@localhost" # Django's default From
with self.assertRaises(AnymailAPIError) as cm:
self.message.send()
err = cm.exception
response = err.response.json()
self.assertEqual(err.status_code, 200)
self.assertEqual(response['status'], 'error')
self.assertIn("The From address is not authorised to send mail from this server", response['data']['message'])
self.assertIn("UnauthenticatedFromAddress", response['data']['code'])
self.assertEqual(response["status"], "error")
self.assertIn(
"The From address is not authorised to send mail from this server",
response["data"]["message"],
)
self.assertIn("UnauthenticatedFromAddress", response["data"]["code"])
@override_settings(ANYMAIL_POSTAL_API_KEY="Hey, that's not an API key!")
def test_invalid_server_token(self):
@@ -95,6 +113,9 @@ class PostalBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
err = cm.exception
response = err.response.json()
self.assertEqual(err.status_code, 200)
self.assertEqual(response['status'], 'error')
self.assertIn("The API token provided in X-Server-API-Key was not valid.", response['data']['message'])
self.assertIn("InvalidServerAPIKey", response['data']['code'])
self.assertEqual(response["status"], "error")
self.assertIn(
"The API token provided in X-Server-API-Key was not valid.",
response["data"]["message"],
)
self.assertIn("InvalidServerAPIKey", response["data"]["code"])

View File

@@ -9,12 +9,15 @@ from django.test import tag
from anymail.exceptions import AnymailConfigurationError
from anymail.signals import AnymailTrackingEvent
from anymail.webhooks.postal import PostalTrackingWebhookView
from .utils_postal import ClientWithPostalSignature, make_key
from .webhook_cases import WebhookTestCase
@tag('postal')
@unittest.skipUnless(ClientWithPostalSignature, "Install 'cryptography' to run postal webhook tests")
@tag("postal")
@unittest.skipUnless(
ClientWithPostalSignature, "Install 'cryptography' to run postal webhook tests"
)
class PostalWebhookSecurityTestCase(WebhookTestCase):
client_class = ClientWithPostalSignature
@@ -25,24 +28,35 @@ class PostalWebhookSecurityTestCase(WebhookTestCase):
self.client.set_private_key(make_key())
def test_failed_signature_check(self):
response = self.client.post('/anymail/postal/tracking/',
content_type='application/json', data=json.dumps({'some': 'data'}),
HTTP_X_POSTAL_SIGNATURE=b64encode('invalid'.encode('utf-8')))
response = self.client.post(
"/anymail/postal/tracking/",
content_type="application/json",
data=json.dumps({"some": "data"}),
HTTP_X_POSTAL_SIGNATURE=b64encode("invalid".encode("utf-8")),
)
self.assertEqual(response.status_code, 400)
response = self.client.post('/anymail/postal/tracking/',
content_type='application/json', data=json.dumps({'some': 'data'}),
HTTP_X_POSTAL_SIGNATURE='garbage')
response = self.client.post(
"/anymail/postal/tracking/",
content_type="application/json",
data=json.dumps({"some": "data"}),
HTTP_X_POSTAL_SIGNATURE="garbage",
)
self.assertEqual(response.status_code, 400)
response = self.client.post('/anymail/postal/tracking/',
content_type='application/json', data=json.dumps({'some': 'data'}),
HTTP_X_POSTAL_SIGNATURE='')
response = self.client.post(
"/anymail/postal/tracking/",
content_type="application/json",
data=json.dumps({"some": "data"}),
HTTP_X_POSTAL_SIGNATURE="",
)
self.assertEqual(response.status_code, 400)
@tag('postal')
@unittest.skipUnless(ClientWithPostalSignature, "Install 'cryptography' to run postal webhook tests")
@tag("postal")
@unittest.skipUnless(
ClientWithPostalSignature, "Install 'cryptography' to run postal webhook tests"
)
class PostalDeliveryTestCase(WebhookTestCase):
client_class = ClientWithPostalSignature
@@ -61,13 +75,14 @@ class PostalDeliveryTestCase(WebhookTestCase):
"id": 233843,
"token": "McC2tuqg7mhx",
"direction": "outgoing",
"message_id": "7b82aac4-5d63-41b8-8e35-9faa31a892dc@rp.postal.example.com",
"message_id": "7b82aac4-5d63-41b8-8e35-9faa31a892dc"
"@rp.postal.example.com",
"to": "bounce@example.com",
"from": "sender@example.com",
"subject": "...",
"timestamp": 1606436187.8883688,
"spam_status": "NotChecked",
"tag": None
"tag": None,
},
"bounce": {
"id": 233864,
@@ -79,35 +94,42 @@ class PostalDeliveryTestCase(WebhookTestCase):
"subject": "Mail delivery failed: returning message to sender",
"timestamp": 1606436523.6060522,
"spam_status": "NotChecked",
"tag": None
"tag": None,
},
"details": "details",
"output": "server output",
"sent_with_ssl": None,
"timestamp": 1606753101.9110143,
"time": None
"time": None,
},
"uuid": "0fcc831f-92b9-4e2b-97f2-d873abc77fab"
"uuid": "0fcc831f-92b9-4e2b-97f2-d873abc77fab",
}
response = self.client.post('/anymail/postal/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/postal/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=PostalTrackingWebhookView,
event=ANY, esp_name='Postal')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=PostalTrackingWebhookView,
event=ANY,
esp_name="Postal",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "bounced")
self.assertEqual(event.esp_event, raw_event)
self.assertEqual(event.timestamp, datetime.fromtimestamp(1606753101, tz=timezone.utc))
self.assertEqual(
event.timestamp, datetime.fromtimestamp(1606753101, tz=timezone.utc)
)
self.assertEqual(event.message_id, 233843)
self.assertEqual(event.event_id, "0fcc831f-92b9-4e2b-97f2-d873abc77fab")
self.assertEqual(event.recipient, "bounce@example.com")
self.assertEqual(event.reject_reason, "bounced")
self.assertEqual(event.description,
"details")
self.assertEqual(event.mta_response,
"server output")
self.assertEqual(event.description, "details")
self.assertEqual(event.mta_response, "server output")
def test_deferred_event(self):
raw_event = {
@@ -118,41 +140,49 @@ class PostalDeliveryTestCase(WebhookTestCase):
"id": 1564,
"token": "Kmo8CRdjuM7B",
"direction": "outgoing",
"message_id": "7b095c0e-2c98-4e68-a41f-7bd217a83925@rp.postal.example.com",
"message_id": "7b095c0e-2c98-4e68-a41f-7bd217a83925"
"@rp.postal.example.com",
"to": "deferred@example.com",
"from": "test@postal.example.com",
"subject": "Test Message at November 30, 2020 16:03",
"timestamp": 1606752235.195664,
"spam_status": "NotChecked",
"tag": None
"tag": None,
},
"status": "SoftFail",
"details": "details",
"output": "server output",
"sent_with_ssl": None,
"timestamp": 1606753101.9110143,
"time": None
"time": None,
},
"uuid": "0fcc831f-92b9-4e2b-97f2-d873abc77fab"
"uuid": "0fcc831f-92b9-4e2b-97f2-d873abc77fab",
}
response = self.client.post('/anymail/postal/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/postal/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=PostalTrackingWebhookView,
event=ANY, esp_name='Postal')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=PostalTrackingWebhookView,
event=ANY,
esp_name="Postal",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "deferred")
self.assertEqual(event.esp_event, raw_event)
self.assertEqual(event.timestamp, datetime.fromtimestamp(1606753101, tz=timezone.utc))
self.assertEqual(
event.timestamp, datetime.fromtimestamp(1606753101, tz=timezone.utc)
)
self.assertEqual(event.message_id, 1564)
self.assertEqual(event.event_id, "0fcc831f-92b9-4e2b-97f2-d873abc77fab")
self.assertEqual(event.recipient, "deferred@example.com")
self.assertEqual(event.reject_reason, None)
self.assertEqual(event.description,
"details")
self.assertEqual(event.mta_response,
"server output")
self.assertEqual(event.description, "details")
self.assertEqual(event.mta_response, "server output")
def test_queued_event(self):
raw_event = {
@@ -163,41 +193,53 @@ class PostalDeliveryTestCase(WebhookTestCase):
"id": 1568,
"token": "VRvQMS20Bb4Y",
"direction": "outgoing",
"message_id": "ec7b6375-4045-451a-9503-2a23a607c1c1@rp.postal.example.com",
"message_id": "ec7b6375-4045-451a-9503-2a23a607c1c1"
"@rp.postal.example.com",
"to": "suppressed@example.com",
"from": "test@example.com",
"subject": "Test Message at November 30, 2020 16:12",
"timestamp": 1606752750.993815,
"spam_status": "NotChecked",
"tag": None
"tag": None,
},
"status": "Held",
"details": "Recipient (suppressed@example.com) is on the suppression list",
"details": "Recipient (suppressed@example.com)"
" is on the suppression list",
"output": "server output",
"sent_with_ssl": None,
"timestamp": 1606752751.8933666,
"time": None
"time": None,
},
"uuid": "9be13015-2e54-456c-bf66-eacbe33da824"
"uuid": "9be13015-2e54-456c-bf66-eacbe33da824",
}
response = self.client.post('/anymail/postal/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/postal/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=PostalTrackingWebhookView,
event=ANY, esp_name='Postal')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=PostalTrackingWebhookView,
event=ANY,
esp_name="Postal",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "queued")
self.assertEqual(event.esp_event, raw_event)
self.assertEqual(event.timestamp, datetime.fromtimestamp(1606753101, tz=timezone.utc))
self.assertEqual(
event.timestamp, datetime.fromtimestamp(1606753101, tz=timezone.utc)
)
self.assertEqual(event.message_id, 1568)
self.assertEqual(event.event_id, "9be13015-2e54-456c-bf66-eacbe33da824")
self.assertEqual(event.recipient, "suppressed@example.com")
self.assertEqual(event.reject_reason, None)
self.assertEqual(event.description,
"Recipient (suppressed@example.com) is on the suppression list")
self.assertEqual(event.mta_response,
"server output")
self.assertEqual(
event.description,
"Recipient (suppressed@example.com) is on the suppression list",
)
self.assertEqual(event.mta_response, "server output")
def test_failed_event(self):
raw_event = {
@@ -208,41 +250,49 @@ class PostalDeliveryTestCase(WebhookTestCase):
"id": 1571,
"token": "MzWWQPubXXWz",
"direction": "outgoing",
"message_id": "cfb29da8ed1e4ed5a6c8a0f24d7a9ef3@rp.postal.example.com",
"message_id": "cfb29da8ed1e4ed5a6c8a0f24d7a9ef3"
"@rp.postal.example.com",
"to": "failed@example.com",
"from": "test@example.com",
"subject": "Message delivery failed...",
"timestamp": 1606753318.072171,
"spam_status": "NotChecked",
"tag": None
"tag": None,
},
"status": "HardFail",
"details": "Could not deliver",
"output": "server output",
"sent_with_ssl": None,
"timestamp": 1606753318.7010343,
"time": None
"time": None,
},
"uuid": "5fec5077-dae7-4989-94d5-e1963f3e9181"
"uuid": "5fec5077-dae7-4989-94d5-e1963f3e9181",
}
response = self.client.post('/anymail/postal/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/postal/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=PostalTrackingWebhookView,
event=ANY, esp_name='Postal')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=PostalTrackingWebhookView,
event=ANY,
esp_name="Postal",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "failed")
self.assertEqual(event.esp_event, raw_event)
self.assertEqual(event.timestamp, datetime.fromtimestamp(1606753101, tz=timezone.utc))
self.assertEqual(
event.timestamp, datetime.fromtimestamp(1606753101, tz=timezone.utc)
)
self.assertEqual(event.message_id, 1571)
self.assertEqual(event.event_id, "5fec5077-dae7-4989-94d5-e1963f3e9181")
self.assertEqual(event.recipient, "failed@example.com")
self.assertEqual(event.reject_reason, None)
self.assertEqual(event.description,
"Could not deliver")
self.assertEqual(event.mta_response,
"server output")
self.assertEqual(event.description, "Could not deliver")
self.assertEqual(event.mta_response, "server output")
def test_delivered_event(self):
raw_event = {
@@ -253,33 +303,43 @@ class PostalDeliveryTestCase(WebhookTestCase):
"id": 1563,
"token": "zw6psSlgo6ki",
"direction": "outgoing",
"message_id": "c462ad36-be49-469c-b7b2-dfd317eb40fa@rp.postal.example.com",
"message_id": "c462ad36-be49-469c-b7b2-dfd317eb40fa"
"@rp.postal.example.com",
"to": "recipient@example.com",
"from": "test@example.com",
"subject": "Test Message at November 30, 2020 16:01",
"timestamp": 1606752104.699201,
"spam_status": "NotChecked",
"tag": "welcome-email"
"tag": "welcome-email",
},
"status": "Sent",
"details": "Message for recipient@example.com accepted",
"output": "250 2.0.0 OK\n",
"sent_with_ssl": False,
"timestamp": 1606752106.9858062,
"time": 0.89
"time": 0.89,
},
"uuid": "58e8d7ee-2cd5-4db2-9af3-3f436105795a"
"uuid": "58e8d7ee-2cd5-4db2-9af3-3f436105795a",
}
response = self.client.post('/anymail/postal/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/postal/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=PostalTrackingWebhookView,
event=ANY, esp_name='Postal')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=PostalTrackingWebhookView,
event=ANY,
esp_name="Postal",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "delivered")
self.assertEqual(event.esp_event, raw_event)
self.assertEqual(event.timestamp, datetime.fromtimestamp(1606753101, tz=timezone.utc))
self.assertEqual(
event.timestamp, datetime.fromtimestamp(1606753101, tz=timezone.utc)
)
self.assertEqual(event.message_id, 1563)
self.assertEqual(event.recipient, "recipient@example.com")
self.assertEqual(event.tags, ["welcome-email"])
@@ -300,24 +360,34 @@ class PostalDeliveryTestCase(WebhookTestCase):
"subject": "test",
"timestamp": 1606756008.718169,
"spam_status": "NotSpam",
"tag": None
"tag": None,
},
"status": "HardFail",
"details": "Received a 400 from https://anymail.example.com/anymail/postal/tracking/.",
"details": "Received a 400 from https://anymail.example.com/"
"anymail/postal/tracking/.",
"output": "Not found",
"sent_with_ssl": False,
"timestamp": 1606756014.1078613,
"time": 0.15
"time": 0.15,
},
"uuid": "a01724c0-0d1a-4090-89aa-c3da5a683375"
"uuid": "a01724c0-0d1a-4090-89aa-c3da5a683375",
}
response = self.client.post('/anymail/postal/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/postal/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
self.assertEqual(self.tracking_handler.call_count, 0)
def test_misconfigured_inbound(self):
errmsg = "You seem to have set Postal's *inbound* webhook to Anymail's Postal *tracking* webhook URL."
with self.assertRaisesMessage(AnymailConfigurationError, errmsg):
self.client.post('/anymail/postal/tracking/', content_type='application/json',
data=json.dumps({"rcpt_to": "to@example.org"}))
with self.assertRaisesMessage(
AnymailConfigurationError,
"You seem to have set Postal's *inbound* webhook"
" to Anymail's Postal *tracking* webhook URL.",
):
self.client.post(
"/anymail/postal/tracking/",
content_type="application/json",
data=json.dumps({"rcpt_to": "to@example.org"}),
)

File diff suppressed because it is too large Load Diff

View File

@@ -9,38 +9,35 @@ from anymail.inbound import AnymailInboundMessage
from anymail.signals import AnymailInboundEvent
from anymail.webhooks.postmark import PostmarkInboundWebhookView
from .utils import sample_image_content, sample_email_content
from .utils import sample_email_content, sample_image_content
from .webhook_cases import WebhookTestCase
@tag('postmark')
@tag("postmark")
class PostmarkInboundTestCase(WebhookTestCase):
def test_inbound_basics(self):
raw_event = {
"FromFull": {
"Email": "from+test@example.org",
"Name": "Displayed From",
"MailboxHash": "test"
"MailboxHash": "test",
},
"ToFull": [{
"Email": "test@inbound.example.com",
"Name": "Test Inbound",
"MailboxHash": ""
}, {
"Email": "other@example.com",
"Name": "",
"MailboxHash": ""
}],
"CcFull": [{
"Email": "cc@example.com",
"Name": "",
"MailboxHash": ""
}],
"BccFull": [{
"Email": "bcc@example.com",
"Name": "Postmark documents blind cc on inbound email (?)",
"MailboxHash": ""
}],
"ToFull": [
{
"Email": "test@inbound.example.com",
"Name": "Test Inbound",
"MailboxHash": "",
},
{"Email": "other@example.com", "Name": "", "MailboxHash": ""},
],
"CcFull": [{"Email": "cc@example.com", "Name": "", "MailboxHash": ""}],
"BccFull": [
{
"Email": "bcc@example.com",
"Name": "Postmark documents blind cc on inbound email (?)",
"MailboxHash": "",
}
],
"OriginalRecipient": "test@inbound.example.com",
"ReplyTo": "from+test@milter.example.org",
"Subject": "Test subject",
@@ -50,51 +47,59 @@ class PostmarkInboundTestCase(WebhookTestCase):
"HtmlBody": "<div>Test body html</div>",
"StrippedTextReply": "stripped plaintext body",
"Tag": "",
"Headers": [{
"Name": "Received",
"Value": "from mail.example.org by inbound.postmarkapp.com ..."
}, {
"Name": "X-Spam-Checker-Version",
"Value": "SpamAssassin 3.4.0 (2014-02-07) onp-pm-smtp-inbound01b-aws-useast2b"
}, {
"Name": "X-Spam-Status",
"Value": "No"
}, {
"Name": "X-Spam-Score",
"Value": "1.7"
}, {
"Name": "X-Spam-Tests",
"Value": "SPF_PASS"
}, {
"Name": "Received-SPF",
"Value": "Pass (sender SPF authorized) identity=mailfrom; client-ip=333.3.3.3;"
" helo=mail-02.example.org; envelope-from=envelope-from@example.org;"
" receiver=test@inbound.example.com"
}, {
"Name": "Received",
"Value": "by mail.example.org for <test@inbound.example.com> ..."
}, {
"Name": "Received",
"Value": "by 10.10.1.71 with HTTP; Wed, 11 Oct 2017 18:31:04 -0700 (PDT)"
}, {
"Name": "MIME-Version",
"Value": "1.0"
}, {
"Name": "Message-ID",
"Value": "<CAEPk3R+4Zr@mail.example.org>"
}],
"Headers": [
{
"Name": "Received",
"Value": "from mail.example.org by inbound.postmarkapp.com ...",
},
{
"Name": "X-Spam-Checker-Version",
"Value": "SpamAssassin 3.4.0 (2014-02-07)"
" onp-pm-smtp-inbound01b-aws-useast2b",
},
{"Name": "X-Spam-Status", "Value": "No"},
{"Name": "X-Spam-Score", "Value": "1.7"},
{"Name": "X-Spam-Tests", "Value": "SPF_PASS"},
{
"Name": "Received-SPF",
"Value": "Pass (sender SPF authorized) identity=mailfrom;"
" client-ip=333.3.3.3;"
" helo=mail-02.example.org;"
" envelope-from=envelope-from@example.org;"
" receiver=test@inbound.example.com",
},
{
"Name": "Received",
"Value": "by mail.example.org for <test@inbound.example.com> ...",
},
{
"Name": "Received",
"Value": "by 10.10.1.71 with HTTP;"
" Wed, 11 Oct 2017 18:31:04 -0700 (PDT)",
},
{"Name": "MIME-Version", "Value": "1.0"},
{"Name": "Message-ID", "Value": "<CAEPk3R+4Zr@mail.example.org>"},
],
}
response = self.client.post('/anymail/postmark/inbound/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/postmark/inbound/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.inbound_handler, sender=PostmarkInboundWebhookView,
event=ANY, esp_name='Postmark')
kwargs = self.assert_handler_called_once_with(
self.inbound_handler,
sender=PostmarkInboundWebhookView,
event=ANY,
esp_name="Postmark",
)
# AnymailInboundEvent
event = kwargs['event']
event = kwargs["event"]
self.assertIsInstance(event, AnymailInboundEvent)
self.assertEqual(event.event_type, 'inbound')
self.assertIsNone(event.timestamp) # Postmark doesn't provide inbound event timestamp
self.assertEqual(event.event_type, "inbound")
# Postmark doesn't provide inbound event timestamp:
self.assertIsNone(event.timestamp)
self.assertEqual(event.event_id, "22c74902-a0c1-4511-804f2-341342852c90")
self.assertIsInstance(event.message, AnymailInboundMessage)
self.assertEqual(event.esp_event, raw_event)
@@ -102,132 +107,220 @@ class PostmarkInboundTestCase(WebhookTestCase):
# AnymailInboundMessage - convenience properties
message = event.message
self.assertEqual(message.from_email.display_name, 'Displayed From')
self.assertEqual(message.from_email.addr_spec, 'from+test@example.org')
self.assertEqual([str(e) for e in message.to],
['Test Inbound <test@inbound.example.com>', 'other@example.com'])
self.assertEqual([str(e) for e in message.cc],
['cc@example.com'])
self.assertEqual(message.subject, 'Test subject')
self.assertEqual(message.from_email.display_name, "Displayed From")
self.assertEqual(message.from_email.addr_spec, "from+test@example.org")
self.assertEqual(
[str(e) for e in message.to],
["Test Inbound <test@inbound.example.com>", "other@example.com"],
)
self.assertEqual([str(e) for e in message.cc], ["cc@example.com"])
self.assertEqual(message.subject, "Test subject")
self.assertEqual(message.date.isoformat(" "), "2017-10-11 18:31:04-07:00")
self.assertEqual(message.text, 'Test body plain')
self.assertEqual(message.html, '<div>Test body html</div>')
self.assertEqual(message.text, "Test body plain")
self.assertEqual(message.html, "<div>Test body html</div>")
self.assertEqual(message.envelope_sender, 'envelope-from@example.org')
self.assertEqual(message.envelope_recipient, 'test@inbound.example.com')
self.assertEqual(message.stripped_text, 'stripped plaintext body')
self.assertIsNone(message.stripped_html) # Postmark doesn't provide stripped html
self.assertEqual(message.envelope_sender, "envelope-from@example.org")
self.assertEqual(message.envelope_recipient, "test@inbound.example.com")
self.assertEqual(message.stripped_text, "stripped plaintext body")
# Postmark doesn't provide stripped html:
self.assertIsNone(message.stripped_html)
self.assertIs(message.spam_detected, False)
self.assertEqual(message.spam_score, 1.7)
# AnymailInboundMessage - other headers
self.assertEqual(message['Message-ID'], "<CAEPk3R+4Zr@mail.example.org>")
self.assertEqual(message['Reply-To'], "from+test@milter.example.org")
self.assertEqual(message.get_all('Received'), [
"from mail.example.org by inbound.postmarkapp.com ...",
"by mail.example.org for <test@inbound.example.com> ...",
"by 10.10.1.71 with HTTP; Wed, 11 Oct 2017 18:31:04 -0700 (PDT)",
])
self.assertEqual(message["Message-ID"], "<CAEPk3R+4Zr@mail.example.org>")
self.assertEqual(message["Reply-To"], "from+test@milter.example.org")
self.assertEqual(
message.get_all("Received"),
[
"from mail.example.org by inbound.postmarkapp.com ...",
"by mail.example.org for <test@inbound.example.com> ...",
"by 10.10.1.71 with HTTP; Wed, 11 Oct 2017 18:31:04 -0700 (PDT)",
],
)
def test_attachments(self):
image_content = sample_image_content()
email_content = sample_email_content()
raw_event = {
"Attachments": [{
"Name": "test.txt",
"Content": b64encode('test attachment'.encode('utf-8')).decode('ascii'),
"ContentType": "text/plain",
"ContentLength": len('test attachment')
}, {
"Name": "image.png",
"Content": b64encode(image_content).decode('ascii'),
"ContentType": "image/png",
"ContentID": "abc123",
"ContentLength": len(image_content)
}, {
"Name": "bounce.txt",
"Content": b64encode(email_content).decode('ascii'),
"ContentType": 'message/rfc822; charset="us-ascii"',
"ContentLength": len(email_content)
}]
"Attachments": [
{
"Name": "test.txt",
"Content": b64encode("test attachment".encode("utf-8")).decode(
"ascii"
),
"ContentType": "text/plain",
"ContentLength": len("test attachment"),
},
{
"Name": "image.png",
"Content": b64encode(image_content).decode("ascii"),
"ContentType": "image/png",
"ContentID": "abc123",
"ContentLength": len(image_content),
},
{
"Name": "bounce.txt",
"Content": b64encode(email_content).decode("ascii"),
"ContentType": 'message/rfc822; charset="us-ascii"',
"ContentLength": len(email_content),
},
]
}
response = self.client.post('/anymail/postmark/inbound/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/postmark/inbound/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.inbound_handler, sender=PostmarkInboundWebhookView,
event=ANY, esp_name='Postmark')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.inbound_handler,
sender=PostmarkInboundWebhookView,
event=ANY,
esp_name="Postmark",
)
event = kwargs["event"]
message = event.message
attachments = message.attachments # AnymailInboundMessage convenience accessor
self.assertEqual(len(attachments), 2)
self.assertEqual(attachments[0].get_filename(), 'test.txt')
self.assertEqual(attachments[0].get_content_type(), 'text/plain')
self.assertEqual(attachments[0].get_content_text(), 'test attachment')
self.assertEqual(attachments[1].get_content_type(), 'message/rfc822')
self.assertEqualIgnoringHeaderFolding(attachments[1].get_content_bytes(), email_content)
self.assertEqual(attachments[0].get_filename(), "test.txt")
self.assertEqual(attachments[0].get_content_type(), "text/plain")
self.assertEqual(attachments[0].get_content_text(), "test attachment")
self.assertEqual(attachments[1].get_content_type(), "message/rfc822")
self.assertEqualIgnoringHeaderFolding(
attachments[1].get_content_bytes(), email_content
)
inlines = message.inline_attachments
self.assertEqual(len(inlines), 1)
inline = inlines['abc123']
self.assertEqual(inline.get_filename(), 'image.png')
self.assertEqual(inline.get_content_type(), 'image/png')
inline = inlines["abc123"]
self.assertEqual(inline.get_filename(), "image.png")
self.assertEqual(inline.get_content_type(), "image/png")
self.assertEqual(inline.get_content_bytes(), image_content)
def test_envelope_sender(self):
# Anymail extracts envelope-sender from Postmark Received-SPF header
raw_event = {
"Headers": [{
"Name": "Received-SPF",
"Value": "Pass (sender SPF authorized) identity=mailfrom; client-ip=333.3.3.3;"
" helo=mail-02.example.org; envelope-from=envelope-from@example.org;"
" receiver=test@inbound.example.com"
}],
"Headers": [
{
"Name": "Received-SPF",
"Value": "Pass (sender SPF authorized) identity=mailfrom;"
" client-ip=333.3.3.3;"
" helo=mail-02.example.org;"
" envelope-from=envelope-from@example.org;"
" receiver=test@inbound.example.com",
}
],
}
response = self.client.post('/anymail/postmark/inbound/', content_type='application/json',
data=json.dumps(raw_event))
response = self.client.post(
"/anymail/postmark/inbound/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
self.assertEqual(self.get_kwargs(self.inbound_handler)['event'].message.envelope_sender,
"envelope-from@example.org")
self.assertEqual(
self.get_kwargs(self.inbound_handler)["event"].message.envelope_sender,
"envelope-from@example.org",
)
# Allow neutral SPF response
self.client.post(
'/anymail/postmark/inbound/', content_type='application/json', data=json.dumps({"Headers": [{
"Name": "Received-SPF",
"Value": "Neutral (no SPF record exists) identity=mailfrom; envelope-from=envelope-from@example.org"
}]}))
self.assertEqual(self.get_kwargs(self.inbound_handler)['event'].message.envelope_sender,
"envelope-from@example.org")
"/anymail/postmark/inbound/",
content_type="application/json",
data=json.dumps(
{
"Headers": [
{
"Name": "Received-SPF",
"Value": "Neutral (no SPF record exists)"
" identity=mailfrom;"
" envelope-from=envelope-from@example.org",
}
]
}
),
)
self.assertEqual(
self.get_kwargs(self.inbound_handler)["event"].message.envelope_sender,
"envelope-from@example.org",
)
# Ignore fail/softfail
self.client.post(
'/anymail/postmark/inbound/', content_type='application/json', data=json.dumps({"Headers": [{
"Name": "Received-SPF",
"Value": "Fail (sender not SPF authorized) identity=mailfrom; envelope-from=spoofed@example.org"
}]}))
self.assertIsNone(self.get_kwargs(self.inbound_handler)['event'].message.envelope_sender)
"/anymail/postmark/inbound/",
content_type="application/json",
data=json.dumps(
{
"Headers": [
{
"Name": "Received-SPF",
"Value": "Fail (sender not SPF authorized)"
" identity=mailfrom;"
" envelope-from=spoofed@example.org",
}
]
}
),
)
self.assertIsNone(
self.get_kwargs(self.inbound_handler)["event"].message.envelope_sender
)
# Ignore garbage
self.client.post(
'/anymail/postmark/inbound/', content_type='application/json', data=json.dumps({"Headers": [{
"Name": "Received-SPF",
"Value": "ThisIsNotAValidReceivedSPFHeader@example.org"
}]}))
self.assertIsNone(self.get_kwargs(self.inbound_handler)['event'].message.envelope_sender)
"/anymail/postmark/inbound/",
content_type="application/json",
data=json.dumps(
{
"Headers": [
{
"Name": "Received-SPF",
"Value": "ThisIsNotAValidReceivedSPFHeader@example.org",
}
]
}
),
)
self.assertIsNone(
self.get_kwargs(self.inbound_handler)["event"].message.envelope_sender
)
# Ignore multiple Received-SPF headers
self.client.post(
'/anymail/postmark/inbound/', content_type='application/json', data=json.dumps({"Headers": [{
"Name": "Received-SPF",
"Value": "Fail (sender not SPF authorized) identity=mailfrom; envelope-from=spoofed@example.org"
}, {
"Name": "Received-SPF",
"Value": "Pass (malicious sender added this) identity=mailfrom; envelope-from=spoofed@example.org"
}]}))
self.assertIsNone(self.get_kwargs(self.inbound_handler)['event'].message.envelope_sender)
"/anymail/postmark/inbound/",
content_type="application/json",
data=json.dumps(
{
"Headers": [
{
"Name": "Received-SPF",
"Value": "Fail (sender not SPF authorized)"
" identity=mailfrom;"
" envelope-from=spoofed@example.org",
},
{
"Name": "Received-SPF",
"Value": "Pass (malicious sender added this)"
" identity=mailfrom;"
" envelope-from=spoofed@example.org",
},
]
}
),
)
self.assertIsNone(
self.get_kwargs(self.inbound_handler)["event"].message.envelope_sender
)
def test_misconfigured_tracking(self):
errmsg = "You seem to have set Postmark's *Delivery* webhook to Anymail's Postmark *inbound* webhook URL."
errmsg = (
"You seem to have set Postmark's *Delivery* webhook"
" to Anymail's Postmark *inbound* webhook URL."
)
with self.assertRaisesMessage(AnymailConfigurationError, errmsg):
self.client.post('/anymail/postmark/inbound/', content_type='application/json',
data=json.dumps({"RecordType": "Delivery"}))
self.client.post(
"/anymail/postmark/inbound/",
content_type="application/json",
data=json.dumps({"RecordType": "Delivery"}),
)

View File

@@ -9,20 +9,23 @@ from anymail.message import AnymailMessage
from .utils import AnymailTestMixin, sample_image_path
# For most integration tests, Postmark's sandboxed "POSTMARK_API_TEST" token is used.
# But to test template sends, a real Postmark server token and template id are needed:
ANYMAIL_TEST_POSTMARK_SERVER_TOKEN = os.getenv('ANYMAIL_TEST_POSTMARK_SERVER_TOKEN')
ANYMAIL_TEST_POSTMARK_TEMPLATE_ID = os.getenv('ANYMAIL_TEST_POSTMARK_TEMPLATE_ID')
ANYMAIL_TEST_POSTMARK_DOMAIN = os.getenv('ANYMAIL_TEST_POSTMARK_DOMAIN')
ANYMAIL_TEST_POSTMARK_SERVER_TOKEN = os.getenv("ANYMAIL_TEST_POSTMARK_SERVER_TOKEN")
ANYMAIL_TEST_POSTMARK_TEMPLATE_ID = os.getenv("ANYMAIL_TEST_POSTMARK_TEMPLATE_ID")
ANYMAIL_TEST_POSTMARK_DOMAIN = os.getenv("ANYMAIL_TEST_POSTMARK_DOMAIN")
@tag('postmark', 'live')
@unittest.skipUnless(ANYMAIL_TEST_POSTMARK_DOMAIN,
"Set ANYMAIL_TEST_POSTMARK_DOMAIN environment variable "
"to run Postmark template integration tests")
@override_settings(ANYMAIL_POSTMARK_SERVER_TOKEN="POSTMARK_API_TEST",
EMAIL_BACKEND="anymail.backends.postmark.EmailBackend")
@tag("postmark", "live")
@unittest.skipUnless(
ANYMAIL_TEST_POSTMARK_DOMAIN,
"Set ANYMAIL_TEST_POSTMARK_DOMAIN environment variable "
"to run Postmark template integration tests",
)
@override_settings(
ANYMAIL_POSTMARK_SERVER_TOKEN="POSTMARK_API_TEST",
EMAIL_BACKEND="anymail.backends.postmark.EmailBackend",
)
class PostmarkBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
"""Postmark API integration tests
@@ -32,10 +35,14 @@ class PostmarkBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
def setUp(self):
super().setUp()
self.from_email = 'from@%s' % ANYMAIL_TEST_POSTMARK_DOMAIN
self.message = AnymailMessage('Anymail Postmark integration test', 'Text content',
self.from_email, ['test+to1@anymail.dev'])
self.message.attach_alternative('<p>HTML content</p>', "text/html")
self.from_email = "from@%s" % ANYMAIL_TEST_POSTMARK_DOMAIN
self.message = AnymailMessage(
"Anymail Postmark integration test",
"Text content",
self.from_email,
["test+to1@anymail.dev"],
)
self.message.attach_alternative("<p>HTML content</p>", "text/html")
def test_simple_send(self):
# Example of getting the Postmark send status and message id from the message
@@ -43,12 +50,13 @@ class PostmarkBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
self.assertEqual(sent_count, 1)
anymail_status = self.message.anymail_status
sent_status = anymail_status.recipients['test+to1@anymail.dev'].status
message_id = anymail_status.recipients['test+to1@anymail.dev'].message_id
sent_status = anymail_status.recipients["test+to1@anymail.dev"].status
message_id = anymail_status.recipients["test+to1@anymail.dev"].message_id
self.assertEqual(sent_status, 'sent')
self.assertEqual(sent_status, "sent")
self.assertGreater(len(message_id), 0) # non-empty string
self.assertEqual(anymail_status.status, {sent_status}) # set of all recipient statuses
# set of all recipient statuses:
self.assertEqual(anymail_status.status, {sent_status})
self.assertEqual(anymail_status.message_id, message_id)
def test_all_options(self):
@@ -61,7 +69,6 @@ class PostmarkBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
bcc=["test+bcc1@anymail.dev", "Blind Copy 2 <test+bcc2@anymail.dev>"],
reply_to=["reply1@example.com", "Reply 2 <reply2@example.com>"],
headers={"X-Anymail-Test": "value"},
# no send_at support
metadata={"meta1": "simple string", "meta2": 2},
tags=["tag 1"], # max one tag
@@ -75,18 +82,25 @@ class PostmarkBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
message.attach_alternative(
"<p><b>HTML:</b> with <a href='http://example.com'>link</a>"
"and image: <img src='cid:%s'></div>" % cid,
"text/html")
"text/html",
)
message.send()
self.assertEqual(message.anymail_status.status, {'sent'})
self.assertEqual(message.anymail_status.recipients['test+to1@anymail.dev'].status, 'sent')
self.assertEqual(message.anymail_status.recipients['test+to2@anymail.dev'].status, 'sent')
self.assertEqual(message.anymail_status.status, {"sent"})
self.assertEqual(
message.anymail_status.recipients["test+to1@anymail.dev"].status, "sent"
)
self.assertEqual(
message.anymail_status.recipients["test+to2@anymail.dev"].status, "sent"
)
# distinct messages should have different message_ids:
self.assertNotEqual(message.anymail_status.recipients['test+to1@anymail.dev'].message_id,
message.anymail_status.recipients['test+to2@anymail.dev'].message_id)
self.assertNotEqual(
message.anymail_status.recipients["test+to1@anymail.dev"].message_id,
message.anymail_status.recipients["test+to2@anymail.dev"].message_id,
)
def test_invalid_from(self):
self.message.from_email = 'webmaster@localhost' # Django's default From
self.message.from_email = "webmaster@localhost" # Django's default From
with self.assertRaises(AnymailAPIError) as cm:
self.message.send()
err = cm.exception
@@ -98,7 +112,9 @@ class PostmarkBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
and ANYMAIL_TEST_POSTMARK_TEMPLATE_ID
and ANYMAIL_TEST_POSTMARK_DOMAIN,
"Set ANYMAIL_TEST_POSTMARK_SERVER_TOKEN and ANYMAIL_TEST_POSTMARK_TEMPLATE_ID "
"and ANYMAIL_TEST_POSTMARK_DOMAIN environment variables to run Postmark template integration tests")
"and ANYMAIL_TEST_POSTMARK_DOMAIN environment variables to run Postmark "
"template integration tests",
)
@override_settings(ANYMAIL_POSTMARK_SERVER_TOKEN=ANYMAIL_TEST_POSTMARK_SERVER_TOKEN)
def test_template(self):
message = AnymailMessage(
@@ -112,7 +128,7 @@ class PostmarkBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
merge_global_data={"name": "Valued Customer"},
)
message.send()
self.assertEqual(message.anymail_status.status, {'sent'})
self.assertEqual(message.anymail_status.status, {"sent"})
@override_settings(ANYMAIL_POSTMARK_SERVER_TOKEN="Hey, that's not a server token!")
def test_invalid_server_token(self):

View File

@@ -8,19 +8,23 @@ from django.utils.timezone import get_fixed_timezone
from anymail.exceptions import AnymailConfigurationError
from anymail.signals import AnymailTrackingEvent
from anymail.webhooks.postmark import PostmarkTrackingWebhookView
from .webhook_cases import WebhookBasicAuthTestCase, WebhookTestCase
@tag('postmark')
@tag("postmark")
class PostmarkWebhookSecurityTestCase(WebhookBasicAuthTestCase):
def call_webhook(self):
return self.client.post('/anymail/postmark/tracking/',
content_type='application/json', data=json.dumps({}))
return self.client.post(
"/anymail/postmark/tracking/",
content_type="application/json",
data=json.dumps({}),
)
# Actual tests are in WebhookBasicAuthTestCase
@tag('postmark')
@tag("postmark")
class PostmarkDeliveryTestCase(WebhookTestCase):
def test_bounce_event(self):
raw_event = {
@@ -31,8 +35,10 @@ class PostmarkDeliveryTestCase(WebhookTestCase):
"ServerID": 23,
"Name": "Hard bounce",
"MessageID": "2706ee8a-737c-4285-b032-ccd317af53ed",
"Description": "The server was unable to deliver your message (ex: unknown user, mailbox not found).",
"Details": "smtp;550 5.1.1 The email account that you tried to reach does not exist.",
"Description": "The server was unable to deliver your message"
" (ex: unknown user, mailbox not found).",
"Details": "smtp;550 5.1.1 The email account that you tried to reach"
" does not exist.",
"Email": "bounce@example.com",
"From": "sender@example.com",
"BouncedAt": "2016-04-27T16:28:50.3963933-04:00",
@@ -40,27 +46,50 @@ class PostmarkDeliveryTestCase(WebhookTestCase):
"Inactive": True,
"CanActivate": True,
"Subject": "Postmark event test",
"Content": "..."
"Content": "...",
}
response = self.client.post('/anymail/postmark/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/postmark/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=PostmarkTrackingWebhookView,
event=ANY, esp_name='Postmark')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=PostmarkTrackingWebhookView,
event=ANY,
esp_name="Postmark",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "bounced")
self.assertEqual(event.esp_event, raw_event)
self.assertEqual(event.timestamp, datetime(2016, 4, 27, 16, 28, 50, microsecond=396393,
tzinfo=get_fixed_timezone(-4*60)))
self.assertEqual(
event.timestamp,
datetime(
2016,
4,
27,
16,
28,
50,
microsecond=396393,
tzinfo=get_fixed_timezone(-4 * 60),
),
)
self.assertEqual(event.message_id, "2706ee8a-737c-4285-b032-ccd317af53ed")
self.assertEqual(event.event_id, "901542550")
self.assertEqual(event.recipient, "bounce@example.com")
self.assertEqual(event.reject_reason, "bounced")
self.assertEqual(event.description,
"The server was unable to deliver your message (ex: unknown user, mailbox not found).")
self.assertEqual(event.mta_response,
"smtp;550 5.1.1 The email account that you tried to reach does not exist.")
self.assertEqual(
event.description,
"The server was unable to deliver your message"
" (ex: unknown user, mailbox not found).",
)
self.assertEqual(
event.mta_response,
"smtp;550 5.1.1 The email account that you tried to reach does not exist.",
)
def test_delivered_event(self):
raw_event = {
@@ -74,19 +103,37 @@ class PostmarkDeliveryTestCase(WebhookTestCase):
"userid": "12345", # Postmark metadata is always converted to string
},
"DeliveredAt": "2014-08-01T13:28:10.2735393-04:00",
"Details": "Test delivery webhook details"
"Details": "Test delivery webhook details",
}
response = self.client.post('/anymail/postmark/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/postmark/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=PostmarkTrackingWebhookView,
event=ANY, esp_name='Postmark')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=PostmarkTrackingWebhookView,
event=ANY,
esp_name="Postmark",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "delivered")
self.assertEqual(event.esp_event, raw_event)
self.assertEqual(event.timestamp, datetime(2014, 8, 1, 13, 28, 10, microsecond=273539,
tzinfo=get_fixed_timezone(-4*60)))
self.assertEqual(
event.timestamp,
datetime(
2014,
8,
1,
13,
28,
10,
microsecond=273539,
tzinfo=get_fixed_timezone(-4 * 60),
),
)
self.assertEqual(event.message_id, "883953f4-6105-42a2-a16a-77a8eac79483")
self.assertEqual(event.recipient, "recipient@example.com")
self.assertEqual(event.tags, ["welcome-email"])
@@ -104,22 +151,42 @@ class PostmarkDeliveryTestCase(WebhookTestCase):
"Geo": {},
"MessageID": "f4830d10-9c35-4f0c-bca3-3d9b459821f8",
"ReceivedAt": "2016-04-27T16:21:41.2493688-04:00",
"Recipient": "recipient@example.com"
"Recipient": "recipient@example.com",
}
response = self.client.post('/anymail/postmark/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/postmark/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=PostmarkTrackingWebhookView,
event=ANY, esp_name='Postmark')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=PostmarkTrackingWebhookView,
event=ANY,
esp_name="Postmark",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "opened")
self.assertEqual(event.esp_event, raw_event)
self.assertEqual(event.timestamp, datetime(2016, 4, 27, 16, 21, 41, microsecond=249368,
tzinfo=get_fixed_timezone(-4*60)))
self.assertEqual(
event.timestamp,
datetime(
2016,
4,
27,
16,
21,
41,
microsecond=249368,
tzinfo=get_fixed_timezone(-4 * 60),
),
)
self.assertEqual(event.message_id, "f4830d10-9c35-4f0c-bca3-3d9b459821f8")
self.assertEqual(event.recipient, "recipient@example.com")
self.assertEqual(event.user_agent, "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0")
self.assertEqual(
event.user_agent, "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0"
)
self.assertEqual(event.tags, [])
self.assertEqual(event.metadata, {})
@@ -130,12 +197,12 @@ class PostmarkDeliveryTestCase(WebhookTestCase):
"Client": {
"Name": "Chrome 35.0.1916.153",
"Company": "Google",
"Family": "Chrome"
"Family": "Chrome",
},
"OS": {
"Name": "OS X 10.7 Lion",
"Company": "Apple Computer, Inc.",
"Family": "OS X 10"
"Family": "OS X 10",
},
"Platform": "Desktop",
"UserAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) etc.",
@@ -148,27 +215,38 @@ class PostmarkDeliveryTestCase(WebhookTestCase):
"City": "Novi Sad",
"Zip": "21000",
"Coords": "45.2517,19.8369",
"IP": "8.8.8.8"
"IP": "8.8.8.8",
},
"MessageID": "f4830d10-9c35-4f0c-bca3-3d9b459821f8",
"ReceivedAt": "2017-10-25T15:21:11.9065619Z",
"Tag": "welcome-email",
"Recipient": "recipient@example.com"
"Recipient": "recipient@example.com",
}
response = self.client.post('/anymail/postmark/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/postmark/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=PostmarkTrackingWebhookView,
event=ANY, esp_name='Postmark')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=PostmarkTrackingWebhookView,
event=ANY,
esp_name="Postmark",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "clicked")
self.assertEqual(event.esp_event, raw_event)
self.assertEqual(event.timestamp, datetime(2017, 10, 25, 15, 21, 11, microsecond=906561,
tzinfo=timezone.utc))
self.assertEqual(
event.timestamp,
datetime(2017, 10, 25, 15, 21, 11, microsecond=906561, tzinfo=timezone.utc),
)
self.assertEqual(event.message_id, "f4830d10-9c35-4f0c-bca3-3d9b459821f8")
self.assertEqual(event.recipient, "recipient@example.com")
self.assertEqual(event.user_agent, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) etc.")
self.assertEqual(
event.user_agent, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) etc."
)
self.assertEqual(event.click_url, "https://example.com/click/me")
self.assertEqual(event.tags, ["welcome-email"])
self.assertEqual(event.metadata, {})
@@ -191,19 +269,37 @@ class PostmarkDeliveryTestCase(WebhookTestCase):
"DumpAvailable": True,
"Inactive": True,
"CanActivate": False,
"Subject": "Postmark event test"
"Subject": "Postmark event test",
}
response = self.client.post('/anymail/postmark/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/postmark/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=PostmarkTrackingWebhookView,
event=ANY, esp_name='Postmark')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=PostmarkTrackingWebhookView,
event=ANY,
esp_name="Postmark",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "complained")
self.assertEqual(event.esp_event, raw_event)
self.assertEqual(event.timestamp, datetime(2016, 4, 27, 16, 28, 50, microsecond=396393,
tzinfo=get_fixed_timezone(-4*60)))
self.assertEqual(
event.timestamp,
datetime(
2016,
4,
27,
16,
28,
50,
microsecond=396393,
tzinfo=get_fixed_timezone(-4 * 60),
),
)
self.assertEqual(event.message_id, "2706ee8a-737c-4285-b032-ccd317af53ed")
self.assertEqual(event.event_id, "901542550")
self.assertEqual(event.recipient, "spam@example.com")
@@ -212,14 +308,23 @@ class PostmarkDeliveryTestCase(WebhookTestCase):
self.assertEqual(event.mta_response, "Test spam complaint details")
def test_misconfigured_inbound(self):
errmsg = "You seem to have set Postmark's *inbound* webhook to Anymail's Postmark *tracking* webhook URL."
errmsg = (
"You seem to have set Postmark's *inbound* webhook"
" to Anymail's Postmark *tracking* webhook URL."
)
with self.assertRaisesMessage(AnymailConfigurationError, errmsg):
self.client.post('/anymail/postmark/tracking/', content_type='application/json',
data=json.dumps({"FromFull": {"Email": "from@example.org"}}))
self.client.post(
"/anymail/postmark/tracking/",
content_type="application/json",
data=json.dumps({"FromFull": {"Email": "from@example.org"}}),
)
with self.assertRaisesMessage(AnymailConfigurationError, errmsg):
self.client.post('/anymail/postmark/tracking/', content_type='application/json',
data=json.dumps({"RecordType": "Inbound"}))
self.client.post(
"/anymail/postmark/tracking/",
content_type="application/json",
data=json.dumps({"RecordType": "Inbound"}),
)
def test_unsubscribe(self):
raw_event = {
@@ -233,23 +338,32 @@ class PostmarkDeliveryTestCase(WebhookTestCase):
"SuppressSending": True,
"SuppressionReason": "ManualSuppression",
"Tag": "welcome-email",
"Metadata": {
"example": "value",
"example_2": "value"
}
"Metadata": {"example": "value", "example_2": "value"},
}
response = self.client.post('/anymail/postmark/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/postmark/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=PostmarkTrackingWebhookView,
event=ANY, esp_name='Postmark')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=PostmarkTrackingWebhookView,
event=ANY,
esp_name="Postmark",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "unsubscribed")
self.assertEqual(event.esp_event, raw_event)
self.assertEqual(event.timestamp, datetime(2022, 6, 5, 17, 17, 32, tzinfo=timezone.utc))
self.assertEqual(
event.timestamp, datetime(2022, 6, 5, 17, 17, 32, tzinfo=timezone.utc)
)
self.assertEqual(event.message_id, "a4909a96-73d7-4c49-b148-a54522d3f7ac")
self.assertEqual(event.recipient, "john@example.com",)
self.assertEqual(
event.recipient,
"john@example.com",
)
self.assertEqual(event.reject_reason, "unsubscribed")
def test_resubscribe(self):
@@ -264,23 +378,32 @@ class PostmarkDeliveryTestCase(WebhookTestCase):
"SuppressSending": False,
"SuppressionReason": None,
"Tag": "welcome-email",
"Metadata": {
"example": "value",
"example_2": "value"
}
"Metadata": {"example": "value", "example_2": "value"},
}
response = self.client.post('/anymail/postmark/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/postmark/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=PostmarkTrackingWebhookView,
event=ANY, esp_name='Postmark')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=PostmarkTrackingWebhookView,
event=ANY,
esp_name="Postmark",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "subscribed")
self.assertEqual(event.esp_event, raw_event)
self.assertEqual(event.timestamp, datetime(2022, 6, 5, 17, 17, 32, tzinfo=timezone.utc))
self.assertEqual(
event.timestamp, datetime(2022, 6, 5, 17, 17, 32, tzinfo=timezone.utc)
)
self.assertEqual(event.message_id, "a4909a96-73d7-4c49-b148-a54522d3f7ac")
self.assertEqual(event.recipient, "john@example.com",)
self.assertEqual(
event.recipient,
"john@example.com",
)
self.assertEqual(event.reject_reason, None)
def test_subscription_change_bounce(self):
@@ -295,21 +418,30 @@ class PostmarkDeliveryTestCase(WebhookTestCase):
"SuppressSending": True,
"SuppressionReason": "HardBounce",
"Tag": "my-tag",
"Metadata": {
"example": "value",
"example_2": "value"
}
"Metadata": {"example": "value", "example_2": "value"},
}
response = self.client.post('/anymail/postmark/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/postmark/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=PostmarkTrackingWebhookView,
event=ANY, esp_name='Postmark')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=PostmarkTrackingWebhookView,
event=ANY,
esp_name="Postmark",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "bounced")
self.assertEqual(event.esp_event, raw_event)
self.assertEqual(event.timestamp, datetime(2022, 6, 5, 17, 17, 32, tzinfo=timezone.utc))
self.assertEqual(
event.timestamp, datetime(2022, 6, 5, 17, 17, 32, tzinfo=timezone.utc)
)
self.assertEqual(event.message_id, "b4cb783d-78ed-43f2-983b-63f55c712dc8")
self.assertEqual(event.recipient, "john@example.com",)
self.assertEqual(
event.recipient,
"john@example.com",
)
self.assertEqual(event.reject_reason, "bounced")

View File

@@ -3,7 +3,7 @@ from django.dispatch import receiver
from anymail.backends.test import EmailBackend as TestEmailBackend
from anymail.exceptions import AnymailCancelSend, AnymailRecipientsRefused
from anymail.message import AnymailRecipientStatus
from anymail.signals import pre_send, post_send
from anymail.signals import post_send, pre_send
from .test_general_backend import TestBackendTestCase
@@ -13,13 +13,16 @@ class TestPreSendSignal(TestBackendTestCase):
def test_pre_send(self):
"""Pre-send receivers invoked for each message, before sending"""
@receiver(pre_send, weak=False)
def handle_pre_send(sender, message, esp_name, **kwargs):
self.assertEqual(self.get_send_count(), 0) # not sent yet
self.assertEqual(sender, TestEmailBackend)
self.assertEqual(message, self.message)
self.assertEqual(esp_name, "Test") # the TestEmailBackend's ESP is named "Test"
# the TestEmailBackend's ESP is named "Test":
self.assertEqual(esp_name, "Test")
self.receiver_called = True
self.addCleanup(pre_send.disconnect, receiver=handle_pre_send)
self.receiver_called = False
@@ -29,25 +32,34 @@ class TestPreSendSignal(TestBackendTestCase):
def test_modify_message_in_pre_send(self):
"""Pre-send receivers can modify message"""
@receiver(pre_send, weak=False)
def handle_pre_send(sender, message, esp_name, **kwargs):
message.to = [email for email in message.to if not email.startswith('bad')]
message.to = [email for email in message.to if not email.startswith("bad")]
message.body += "\nIf you have received this message in error, ignore it"
self.addCleanup(pre_send.disconnect, receiver=handle_pre_send)
self.message.to = ['legit@example.com', 'bad@example.com']
self.message.to = ["legit@example.com", "bad@example.com"]
self.message.send()
params = self.get_send_params()
self.assertEqual([email.addr_spec for email in params['to']], # params['to'] is EmailAddress list
['legit@example.com'])
self.assertRegex(params['text_body'],
r"If you have received this message in error, ignore it$")
self.assertEqual(
# params['to'] is EmailAddress list:
[email.addr_spec for email in params["to"]],
["legit@example.com"],
)
self.assertRegex(
params["text_body"],
r"If you have received this message in error, ignore it$",
)
def test_cancel_in_pre_send(self):
"""Pre-send receiver can cancel the send"""
@receiver(pre_send, weak=False)
def cancel_pre_send(sender, message, esp_name, **kwargs):
raise AnymailCancelSend("whoa there")
self.addCleanup(pre_send.disconnect, receiver=cancel_pre_send)
self.message.send()
@@ -59,17 +71,20 @@ class TestPostSendSignal(TestBackendTestCase):
def test_post_send(self):
"""Post-send receiver called for each message, after sending"""
@receiver(post_send, weak=False)
def handle_post_send(sender, message, status, esp_name, **kwargs):
self.assertEqual(self.get_send_count(), 1) # already sent
self.assertEqual(sender, TestEmailBackend)
self.assertEqual(message, self.message)
self.assertEqual(status.status, {'sent'})
self.assertEqual(status.status, {"sent"})
self.assertEqual(status.message_id, 0)
self.assertEqual(status.recipients['to@example.com'].status, 'sent')
self.assertEqual(status.recipients['to@example.com'].message_id, 0)
self.assertEqual(esp_name, "Test") # the TestEmailBackend's ESP is named "Test"
self.assertEqual(status.recipients["to@example.com"].status, "sent")
self.assertEqual(status.recipients["to@example.com"].message_id, 0)
# the TestEmailBackend's ESP is named "Test":
self.assertEqual(esp_name, "Test")
self.receiver_called = True
self.addCleanup(post_send.disconnect, receiver=handle_post_send)
self.receiver_called = False
@@ -78,14 +93,17 @@ class TestPostSendSignal(TestBackendTestCase):
def test_post_send_exception(self):
"""All post-send receivers called, even if one throws"""
@receiver(post_send, weak=False)
def handler_1(sender, message, status, esp_name, **kwargs):
raise ValueError("oops")
self.addCleanup(post_send.disconnect, receiver=handler_1)
@receiver(post_send, weak=False)
def handler_2(sender, message, status, esp_name, **kwargs):
self.handler_2_called = True
self.addCleanup(post_send.disconnect, receiver=handler_2)
self.handler_2_called = False
@@ -95,14 +113,18 @@ class TestPostSendSignal(TestBackendTestCase):
def test_rejected_recipients(self):
"""Post-send receiver even if AnymailRecipientsRefused is raised"""
@receiver(post_send, weak=False)
def handle_post_send(sender, message, status, esp_name, **kwargs):
self.receiver_called = True
self.addCleanup(post_send.disconnect, receiver=handle_post_send)
self.message.anymail_test_response = {
'recipient_status': {
'to@example.com': AnymailRecipientStatus(message_id=None, status='rejected')
"recipient_status": {
"to@example.com": AnymailRecipientStatus(
message_id=None, status="rejected"
)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,15 +9,22 @@ from anymail.inbound import AnymailInboundMessage
from anymail.signals import AnymailInboundEvent
from anymail.webhooks.sendgrid import SendGridInboundWebhookView
from .utils import dedent_bytes, sample_image_content, sample_email_content, encode_multipart, make_fileobj
from .utils import (
dedent_bytes,
encode_multipart,
make_fileobj,
sample_email_content,
sample_image_content,
)
from .webhook_cases import WebhookTestCase
@tag('sendgrid')
@tag("sendgrid")
class SendgridInboundTestCase(WebhookTestCase):
def test_inbound_basics(self):
raw_event = {
'headers': dedent("""\
"headers": dedent(
"""\
Received: from mail.example.org by mx987654321.sendgrid.net ...
Received: by mail.example.org for <test@inbound.example.com> ...
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.org; ...
@@ -30,109 +37,144 @@ class SendgridInboundTestCase(WebhookTestCase):
To: "Test Inbound" <test@inbound.example.com>, other@example.com
Cc: cc@example.com
Content-Type: multipart/mixed; boundary="94eb2c115edcf35387055b61f849"
"""),
'from': 'Displayed From <from+test@example.org>',
'to': 'Test Inbound <test@inbound.example.com>, other@example.com',
'subject': "Test subject",
'text': "Test body plain",
'html': "<div>Test body html</div>",
'attachments': "0",
'charsets': '{"to":"UTF-8","html":"UTF-8","subject":"UTF-8","from":"UTF-8","text":"UTF-8"}',
'envelope': '{"to":["test@inbound.example.com"],"from":"envelope-from@example.org"}',
'sender_ip': "10.10.1.71",
'dkim': "{@example.org : pass}", # yep, SendGrid uses not-exactly-json for this field
'SPF': "pass",
'spam_score': "1.7",
'spam_report': 'Spam detection software, running on the system "mx987654321.sendgrid.net", '
'has identified this incoming email as possible spam...',
"""
),
"from": "Displayed From <from+test@example.org>",
"to": "Test Inbound <test@inbound.example.com>, other@example.com",
"subject": "Test subject",
"text": "Test body plain",
"html": "<div>Test body html</div>",
"attachments": "0",
"charsets": '{"to":"UTF-8","html":"UTF-8",'
'"subject":"UTF-8","from":"UTF-8","text":"UTF-8"}',
"envelope": '{"to":["test@inbound.example.com"],'
'"from":"envelope-from@example.org"}',
"sender_ip": "10.10.1.71",
# yep, SendGrid uses not-exactly-json for this field:
"dkim": "{@example.org : pass}",
"SPF": "pass",
"spam_score": "1.7",
"spam_report": "Spam detection software, running on the system"
' "mx987654321.sendgrid.net", '
"has identified this incoming email as possible spam...",
}
response = self.client.post('/anymail/sendgrid/inbound/', data=raw_event)
response = self.client.post("/anymail/sendgrid/inbound/", data=raw_event)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.inbound_handler, sender=SendGridInboundWebhookView,
event=ANY, esp_name='SendGrid')
kwargs = self.assert_handler_called_once_with(
self.inbound_handler,
sender=SendGridInboundWebhookView,
event=ANY,
esp_name="SendGrid",
)
# AnymailInboundEvent
event = kwargs['event']
event = kwargs["event"]
self.assertIsInstance(event, AnymailInboundEvent)
self.assertEqual(event.event_type, 'inbound')
self.assertEqual(event.event_type, "inbound")
self.assertIsNone(event.timestamp)
self.assertIsNone(event.event_id)
self.assertIsInstance(event.message, AnymailInboundMessage)
self.assertEqual(event.esp_event.POST.dict(), raw_event) # esp_event is a Django HttpRequest
# esp_event is a Django HttpRequest:
self.assertEqual(event.esp_event.POST.dict(), raw_event)
# AnymailInboundMessage - convenience properties
message = event.message
self.assertEqual(message.from_email.display_name, 'Displayed From')
self.assertEqual(message.from_email.addr_spec, 'from+test@example.org')
self.assertEqual([str(e) for e in message.to],
['Test Inbound <test@inbound.example.com>', 'other@example.com'])
self.assertEqual([str(e) for e in message.cc],
['cc@example.com'])
self.assertEqual(message.subject, 'Test subject')
self.assertEqual(message.from_email.display_name, "Displayed From")
self.assertEqual(message.from_email.addr_spec, "from+test@example.org")
self.assertEqual(
[str(e) for e in message.to],
["Test Inbound <test@inbound.example.com>", "other@example.com"],
)
self.assertEqual([str(e) for e in message.cc], ["cc@example.com"])
self.assertEqual(message.subject, "Test subject")
self.assertEqual(message.date.isoformat(" "), "2017-10-11 18:31:04-07:00")
self.assertEqual(message.text, 'Test body plain')
self.assertEqual(message.html, '<div>Test body html</div>')
self.assertEqual(message.text, "Test body plain")
self.assertEqual(message.html, "<div>Test body html</div>")
self.assertEqual(message.envelope_sender, 'envelope-from@example.org')
self.assertEqual(message.envelope_recipient, 'test@inbound.example.com')
self.assertEqual(message.envelope_sender, "envelope-from@example.org")
self.assertEqual(message.envelope_recipient, "test@inbound.example.com")
self.assertIsNone(message.stripped_text)
self.assertIsNone(message.stripped_html)
self.assertIsNone(message.spam_detected) # SendGrid doesn't give a simple yes/no; check the score yourself
# SendGrid doesn't give a simple spam yes/no; check the score yourself:
self.assertIsNone(message.spam_detected)
self.assertEqual(message.spam_score, 1.7)
# AnymailInboundMessage - other headers
self.assertEqual(message['Message-ID'], "<CAEPk3R+4Zr@mail.example.org>")
self.assertEqual(message.get_all('Received'), [
"from mail.example.org by mx987654321.sendgrid.net ...",
"by mail.example.org for <test@inbound.example.com> ...",
"by 10.10.1.71 with HTTP; Wed, 11 Oct 2017 18:31:04 -0700 (PDT)",
])
self.assertEqual(message["Message-ID"], "<CAEPk3R+4Zr@mail.example.org>")
self.assertEqual(
message.get_all("Received"),
[
"from mail.example.org by mx987654321.sendgrid.net ...",
"by mail.example.org for <test@inbound.example.com> ...",
"by 10.10.1.71 with HTTP; Wed, 11 Oct 2017 18:31:04 -0700 (PDT)",
],
)
def test_attachments(self):
att1 = BytesIO('test attachment'.encode('utf-8'))
att1.name = 'test.txt'
att1 = BytesIO("test attachment".encode("utf-8"))
att1.name = "test.txt"
image_content = sample_image_content()
att2 = BytesIO(image_content)
att2.name = 'image.png'
att2.name = "image.png"
email_content = sample_email_content()
att3 = BytesIO(email_content)
att3.name = '\\share\\mail\\forwarded.msg'
att3.name = "\\share\\mail\\forwarded.msg"
att3.content_type = 'message/rfc822; charset="us-ascii"'
raw_event = {
'headers': '',
'attachments': '3',
'attachment-info': json.dumps({
"attachment3": {"filename": "\\share\\mail\\forwarded.msg",
"charset": "US-ASCII", "type": "message/rfc822"},
"attachment2": {"filename": "image.png", "type": "image/png", "content-id": "abc123"},
"attachment1": {"filename": "test.txt", "charset": "UTF-8", "type": "text/plain"},
}),
'content-ids': '{"abc123": "attachment2"}',
'attachment1': att1,
'attachment2': att2, # inline
'attachment3': att3,
"headers": "",
"attachments": "3",
"attachment-info": json.dumps(
{
"attachment3": {
"filename": "\\share\\mail\\forwarded.msg",
"charset": "US-ASCII",
"type": "message/rfc822",
},
"attachment2": {
"filename": "image.png",
"type": "image/png",
"content-id": "abc123",
},
"attachment1": {
"filename": "test.txt",
"charset": "UTF-8",
"type": "text/plain",
},
}
),
"content-ids": '{"abc123": "attachment2"}',
"attachment1": att1,
"attachment2": att2, # inline
"attachment3": att3,
}
response = self.client.post('/anymail/sendgrid/inbound/', data=raw_event)
response = self.client.post("/anymail/sendgrid/inbound/", data=raw_event)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.inbound_handler, sender=SendGridInboundWebhookView,
event=ANY, esp_name='SendGrid')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.inbound_handler,
sender=SendGridInboundWebhookView,
event=ANY,
esp_name="SendGrid",
)
event = kwargs["event"]
message = event.message
attachments = message.attachments # AnymailInboundMessage convenience accessor
self.assertEqual(len(attachments), 2)
self.assertEqual(attachments[0].get_filename(), 'test.txt')
self.assertEqual(attachments[0].get_content_type(), 'text/plain')
self.assertEqual(attachments[0].get_content_text(), 'test attachment')
self.assertEqual(attachments[1].get_filename(), 'forwarded.msg') # Django strips path
self.assertEqual(attachments[1].get_content_type(), 'message/rfc822')
self.assertEqualIgnoringHeaderFolding(attachments[1].get_content_bytes(), email_content)
self.assertEqual(attachments[0].get_filename(), "test.txt")
self.assertEqual(attachments[0].get_content_type(), "text/plain")
self.assertEqual(attachments[0].get_content_text(), "test attachment")
# Django strips path:
self.assertEqual(attachments[1].get_filename(), "forwarded.msg")
self.assertEqual(attachments[1].get_content_type(), "message/rfc822")
self.assertEqualIgnoringHeaderFolding(
attachments[1].get_content_bytes(), email_content
)
inlines = message.inline_attachments
self.assertEqual(len(inlines), 1)
inline = inlines['abc123']
self.assertEqual(inline.get_filename(), 'image.png')
self.assertEqual(inline.get_content_type(), 'image/png')
inline = inlines["abc123"]
self.assertEqual(inline.get_filename(), "image.png")
self.assertEqual(inline.get_content_type(), "image/png")
self.assertEqual(inline.get_content_bytes(), image_content)
def test_filtered_attachment_filenames(self):
@@ -140,44 +182,61 @@ class SendgridInboundTestCase(WebhookTestCase):
# Django's multipart/form-data filename filtering. (The attachments are lost,
# but shouldn't cause errors in the inbound webhook.)
filenames = [
"", "path\\", "path/"
".", "path\\.", "path/.",
"..", "path\\..", "path/..",
"",
"path\\",
"path/" ".",
"path\\.",
"path/.",
"..",
"path\\..",
"path/..",
]
num_attachments = len(filenames)
payload = {
"attachment%d" % (i+1): make_fileobj("content", filename=filenames[i], content_type="text/pdf")
"attachment%d"
% (i + 1): make_fileobj(
"content", filename=filenames[i], content_type="text/pdf"
)
for i in range(num_attachments)
}
attachment_info = {
key: {"filename": value.name, "type": "text/pdf"}
for key, value in payload.items()
}
payload.update({
'headers': '',
'attachments': str(num_attachments),
'attachment-info': json.dumps(attachment_info),
})
payload.update(
{
"headers": "",
"attachments": str(num_attachments),
"attachment-info": json.dumps(attachment_info),
}
)
# Must do our own form-data encoding to properly test empty attachment filenames.
# Must do our own multipart/form-data encoding for empty filenames:
response = self.client.post('/anymail/sendgrid/inbound/',
data=encode_multipart("BoUnDaRy", payload),
content_type="multipart/form-data; boundary=BoUnDaRy")
# Must do our own form-data encoding to properly test empty attachment
# filenames. Must do our own multipart/form-data encoding for empty filenames:
response = self.client.post(
"/anymail/sendgrid/inbound/",
data=encode_multipart("BoUnDaRy", payload),
content_type="multipart/form-data; boundary=BoUnDaRy",
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.inbound_handler, sender=SendGridInboundWebhookView,
event=ANY, esp_name='SendGrid')
kwargs = self.assert_handler_called_once_with(
self.inbound_handler,
sender=SendGridInboundWebhookView,
event=ANY,
esp_name="SendGrid",
)
# Different Django releases strip different filename patterns.
# Just verify that at least some attachments got dropped (so the test is valid)
# without causing an error in the inbound webhook:
attachments = kwargs['event'].message.attachments
attachments = kwargs["event"].message.attachments
self.assertLess(len(attachments), num_attachments)
def test_inbound_mime(self):
# SendGrid has an option to send the full, raw MIME message
raw_event = {
'email': dedent("""\
"email": dedent(
"""\
From: A tester <test@example.org>
Date: Thu, 12 Oct 2017 18:03:30 -0700
Message-ID: <CAEPk3RKEx@mail.example.org>
@@ -199,36 +258,48 @@ class SendgridInboundTestCase(WebhookTestCase):
<div dir=3D"ltr">It's a body=E2=80=A6</div>
--94eb2c05e174adb140055b6339c5--
"""),
'from': 'A tester <test@example.org>',
'to': 'test@inbound.example.com',
'subject': "Raw MIME test",
'charsets': '{"to":"UTF-8","subject":"UTF-8","from":"UTF-8"}',
'envelope': '{"to":["test@inbound.example.com"],"from":"envelope-from@example.org"}',
'sender_ip': "10.10.1.71",
'dkim': "{@example.org : pass}", # yep, SendGrid uses not-exactly-json for this field
'SPF': "pass",
'spam_score': "1.7",
'spam_report': 'Spam detection software, running on the system "mx987654321.sendgrid.net", '
'has identified this incoming email as possible spam...',
""" # NOQA: E501
),
"from": "A tester <test@example.org>",
"to": "test@inbound.example.com",
"subject": "Raw MIME test",
"charsets": '{"to":"UTF-8","subject":"UTF-8","from":"UTF-8"}',
"envelope": '{"to":["test@inbound.example.com"],'
'"from":"envelope-from@example.org"}',
"sender_ip": "10.10.1.71",
# yep, SendGrid uses not-exactly-json for this field:
"dkim": "{@example.org : pass}",
"SPF": "pass",
"spam_score": "1.7",
"spam_report": "Spam detection software, running on the system"
' "mx987654321.sendgrid.net", '
"has identified this incoming email as possible spam...",
}
response = self.client.post('/anymail/sendgrid/inbound/', data=raw_event)
response = self.client.post("/anymail/sendgrid/inbound/", data=raw_event)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.inbound_handler, sender=SendGridInboundWebhookView,
event=ANY, esp_name='SendGrid')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.inbound_handler,
sender=SendGridInboundWebhookView,
event=ANY,
esp_name="SendGrid",
)
event = kwargs["event"]
message = event.message
self.assertEqual(message.envelope_sender, 'envelope-from@example.org')
self.assertEqual(message.envelope_recipient, 'test@inbound.example.com')
self.assertEqual(message.subject, 'Raw MIME test')
self.assertEqual(message.envelope_sender, "envelope-from@example.org")
self.assertEqual(message.envelope_recipient, "test@inbound.example.com")
self.assertEqual(message.subject, "Raw MIME test")
self.assertEqual(message.text, "It's a body\N{HORIZONTAL ELLIPSIS}\n")
self.assertEqual(message.html, """<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\n""")
self.assertEqual(
message.html,
"""<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\n""",
)
def test_inbound_charsets(self):
# Captured (sanitized) from actual SendGrid inbound webhook payload 7/2020,
# using a test message constructed with a variety of charsets:
raw_post = dedent_bytes(b"""\
raw_post = dedent_bytes(
b"""\
--xYzZY
Content-Disposition: form-data; name="headers"
@@ -262,14 +333,22 @@ class SendgridInboundTestCase(WebhookTestCase):
{"to":"UTF-8","cc":"UTF-8","html":"iso-8859-1","subject":"cp850","from":"UTF-8","text":"windows-1252"}
--xYzZY--
""").replace(b"\n", b"\r\n")
"""
).replace(b"\n", b"\r\n")
response = self.client.post('/anymail/sendgrid/inbound/', data=raw_post,
content_type="multipart/form-data; boundary=xYzZY")
response = self.client.post(
"/anymail/sendgrid/inbound/",
data=raw_post,
content_type="multipart/form-data; boundary=xYzZY",
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.inbound_handler, sender=SendGridInboundWebhookView,
event=ANY, esp_name='SendGrid')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.inbound_handler,
sender=SendGridInboundWebhookView,
event=ANY,
esp_name="SendGrid",
)
event = kwargs["event"]
message = event.message
self.assertEqual(message.from_email.display_name, "Opérateur de test")

View File

@@ -10,40 +10,53 @@ from anymail.message import AnymailMessage
from .utils import AnymailTestMixin, sample_image_path
ANYMAIL_TEST_SENDGRID_API_KEY = os.getenv('ANYMAIL_TEST_SENDGRID_API_KEY')
ANYMAIL_TEST_SENDGRID_TEMPLATE_ID = os.getenv('ANYMAIL_TEST_SENDGRID_TEMPLATE_ID')
ANYMAIL_TEST_SENDGRID_DOMAIN = os.getenv('ANYMAIL_TEST_SENDGRID_DOMAIN')
ANYMAIL_TEST_SENDGRID_API_KEY = os.getenv("ANYMAIL_TEST_SENDGRID_API_KEY")
ANYMAIL_TEST_SENDGRID_TEMPLATE_ID = os.getenv("ANYMAIL_TEST_SENDGRID_TEMPLATE_ID")
ANYMAIL_TEST_SENDGRID_DOMAIN = os.getenv("ANYMAIL_TEST_SENDGRID_DOMAIN")
@tag('sendgrid', 'live')
@unittest.skipUnless(ANYMAIL_TEST_SENDGRID_API_KEY and ANYMAIL_TEST_SENDGRID_DOMAIN,
"Set ANYMAIL_TEST_SENDGRID_API_KEY and ANYMAIL_TEST_SENDGRID_DOMAIN "
"environment variables to run SendGrid integration tests")
@override_settings(ANYMAIL_SENDGRID_API_KEY=ANYMAIL_TEST_SENDGRID_API_KEY,
ANYMAIL_SENDGRID_SEND_DEFAULTS={"esp_extra": {
"mail_settings": {"sandbox_mode": {"enable": True}},
}},
EMAIL_BACKEND="anymail.backends.sendgrid.EmailBackend")
@tag("sendgrid", "live")
@unittest.skipUnless(
ANYMAIL_TEST_SENDGRID_API_KEY and ANYMAIL_TEST_SENDGRID_DOMAIN,
"Set ANYMAIL_TEST_SENDGRID_API_KEY and ANYMAIL_TEST_SENDGRID_DOMAIN "
"environment variables to run SendGrid integration tests",
)
@override_settings(
ANYMAIL_SENDGRID_API_KEY=ANYMAIL_TEST_SENDGRID_API_KEY,
ANYMAIL_SENDGRID_SEND_DEFAULTS={
"esp_extra": {
"mail_settings": {"sandbox_mode": {"enable": True}},
}
},
EMAIL_BACKEND="anymail.backends.sendgrid.EmailBackend",
)
class SendGridBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
"""SendGrid v3 API integration tests
"""
SendGrid v3 API integration tests
These tests run against the **live** SendGrid API, using the
environment variable `ANYMAIL_TEST_SENDGRID_API_KEY` as the API key
If those variables are not set, these tests won't run.
The SEND_DEFAULTS above force SendGrid's v3 sandbox mode, which avoids sending mail.
(Sandbox sends also don't show in the activity feed, so disable that for live debugging.)
(Sandbox sends also don't show in the activity feed, so disable that for live
debugging.)
The tests also use SendGrid's "sink domain" @sink.sendgrid.net for recipient addresses.
The tests also use SendGrid's "sink domain" @sink.sendgrid.net for recipient
addresses.
https://support.sendgrid.com/hc/en-us/articles/201995663-Safely-Test-Your-Sending-Speed
"""
def setUp(self):
super().setUp()
self.from_email = 'from@%s' % ANYMAIL_TEST_SENDGRID_DOMAIN
self.message = AnymailMessage('Anymail SendGrid integration test', 'Text content',
self.from_email, ['to@sink.sendgrid.net'])
self.message.attach_alternative('<p>HTML content</p>', "text/html")
self.from_email = "from@%s" % ANYMAIL_TEST_SENDGRID_DOMAIN
self.message = AnymailMessage(
"Anymail SendGrid integration test",
"Text content",
self.from_email,
["to@sink.sendgrid.net"],
)
self.message.attach_alternative("<p>HTML content</p>", "text/html")
def test_simple_send(self):
# Example of getting the SendGrid send status and message id from the message
@@ -51,12 +64,13 @@ class SendGridBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
self.assertEqual(sent_count, 1)
anymail_status = self.message.anymail_status
sent_status = anymail_status.recipients['to@sink.sendgrid.net'].status
message_id = anymail_status.recipients['to@sink.sendgrid.net'].message_id
sent_status = anymail_status.recipients["to@sink.sendgrid.net"].status
message_id = anymail_status.recipients["to@sink.sendgrid.net"].message_id
self.assertEqual(sent_status, 'queued') # SendGrid always queues
self.assertEqual(sent_status, "queued") # SendGrid always queues
self.assertUUIDIsValid(message_id) # Anymail generates a UUID tracking id
self.assertEqual(anymail_status.status, {sent_status}) # set of all recipient statuses
# set of all recipient statuses:
self.assertEqual(anymail_status.status, {sent_status})
self.assertEqual(anymail_status.message_id, message_id)
def test_all_options(self):
@@ -68,15 +82,16 @@ class SendGridBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
to=["to1@sink.sendgrid.net", '"Recipient 2, OK?" <to2@sink.sendgrid.net>'],
cc=["cc1@sink.sendgrid.net", "Copy 2 <cc2@sink.sendgrid.net>"],
bcc=["bcc1@sink.sendgrid.net", "Blind Copy 2 <bcc2@sink.sendgrid.net>"],
reply_to=['"Reply, with comma" <reply@example.com>'], # v3 only supports single reply-to
# v3 only supports single reply-to:
reply_to=['"Reply, with comma" <reply@example.com>'],
headers={"X-Anymail-Test": "value", "X-Anymail-Count": 3},
metadata={"meta1": "simple string", "meta2": 2},
send_at=send_at,
tags=["tag 1", "tag 2"],
track_clicks=True,
track_opens=True,
# esp_extra={'asm': {'group_id': 1}}, # this breaks activity feed if you don't have an asm group
# this breaks activity feed if you don't have an asm group:
# esp_extra={'asm': {'group_id': 1}},
)
message.attach("attachment1.txt", "Here is some\ntext for you", "text/plain")
message.attach("attachment2.csv", "ID,Name\n1,Amy Lina", "text/csv")
@@ -84,10 +99,12 @@ class SendGridBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
message.attach_alternative(
"<p><b>HTML:</b> with <a href='http://example.com'>link</a>"
"and image: <img src='cid:%s'></div>" % cid,
"text/html")
"text/html",
)
message.send()
self.assertEqual(message.anymail_status.status, {'queued'}) # SendGrid always queues
# SendGrid always queues:
self.assertEqual(message.anymail_status.status, {"queued"})
def test_merge_data(self):
message = AnymailMessage(
@@ -97,37 +114,40 @@ class SendGridBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
to=["to1@sink.sendgrid.net", "Recipient 2 <to2@sink.sendgrid.net>"],
reply_to=['"Merge data in reply name: %field%" <reply@example.com>'],
merge_data={
'to1@sink.sendgrid.net': {'field': 'one'},
'to2@sink.sendgrid.net': {'field': 'two'},
"to1@sink.sendgrid.net": {"field": "one"},
"to2@sink.sendgrid.net": {"field": "two"},
},
esp_extra={
'merge_field_format': '%{}%',
"merge_field_format": "%{}%",
},
)
message.send()
recipient_status = message.anymail_status.recipients
self.assertEqual(recipient_status['to1@sink.sendgrid.net'].status, 'queued')
self.assertEqual(recipient_status['to2@sink.sendgrid.net'].status, 'queued')
self.assertEqual(recipient_status["to1@sink.sendgrid.net"].status, "queued")
self.assertEqual(recipient_status["to2@sink.sendgrid.net"].status, "queued")
@unittest.skipUnless(ANYMAIL_TEST_SENDGRID_TEMPLATE_ID,
"Set the ANYMAIL_TEST_SENDGRID_TEMPLATE_ID environment variable "
"to a template in your SendGrid account to test stored templates")
@unittest.skipUnless(
ANYMAIL_TEST_SENDGRID_TEMPLATE_ID,
"Set the ANYMAIL_TEST_SENDGRID_TEMPLATE_ID environment variable "
"to a template in your SendGrid account to test stored templates",
)
def test_stored_template(self):
message = AnymailMessage(
from_email=formataddr(("Test From", self.from_email)),
to=["to@sink.sendgrid.net"],
# Anymail's live test template has merge fields "name", "order_no", and "dept"...
# Anymail's live test template has
# merge fields "name", "order_no", and "dept"...
template_id=ANYMAIL_TEST_SENDGRID_TEMPLATE_ID,
merge_data={
'to@sink.sendgrid.net': {
'name': "Test Recipient",
'order_no': "12345",
"to@sink.sendgrid.net": {
"name": "Test Recipient",
"order_no": "12345",
},
},
merge_global_data={'dept': "Fulfillment"},
merge_global_data={"dept": "Fulfillment"},
)
message.send()
self.assertEqual(message.anymail_status.status, {'queued'})
self.assertEqual(message.anymail_status.status, {"queued"})
@override_settings(ANYMAIL_SENDGRID_API_KEY="Hey, that's not an API key!")
def test_invalid_api_key(self):

View File

@@ -6,43 +6,58 @@ from django.test import tag
from anymail.signals import AnymailTrackingEvent
from anymail.webhooks.sendgrid import SendGridTrackingWebhookView
from .webhook_cases import WebhookBasicAuthTestCase, WebhookTestCase
@tag('sendgrid')
@tag("sendgrid")
class SendGridWebhookSecurityTestCase(WebhookBasicAuthTestCase):
def call_webhook(self):
return self.client.post('/anymail/sendgrid/tracking/',
content_type='application/json', data=json.dumps([]))
return self.client.post(
"/anymail/sendgrid/tracking/",
content_type="application/json",
data=json.dumps([]),
)
# Actual tests are in WebhookBasicAuthTestCase
@tag('sendgrid')
@tag("sendgrid")
class SendGridDeliveryTestCase(WebhookTestCase):
def test_processed_event(self):
raw_events = [{
"email": "recipient@example.com",
"timestamp": 1461095246,
"anymail_id": "3c2f4df8-c6dd-4cd2-9b91-6582b81a0349",
"smtp-id": "<wrfRRvF7Q0GgwUo2CvDmEA@ismtpd0006p1sjc2.sendgrid.net>",
"sg_event_id": "ZyjAM5rnQmuI1KFInHQ3Nw",
"sg_message_id": "wrfRRvF7Q0GgwUo2CvDmEA.filter0425p1mdw1.13037.57168B4A1D.0",
"event": "processed",
"category": ["tag1", "tag2"],
"custom1": "value1",
"custom2": "value2",
}]
response = self.client.post('/anymail/sendgrid/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"email": "recipient@example.com",
"timestamp": 1461095246,
"anymail_id": "3c2f4df8-c6dd-4cd2-9b91-6582b81a0349",
"smtp-id": "<wrfRRvF7Q0GgwUo2CvDmEA@ismtpd0006p1sjc2.sendgrid.net>",
"sg_event_id": "ZyjAM5rnQmuI1KFInHQ3Nw",
"sg_message_id": "wrfRRvF7Q0GgwUo2CvDmEA"
".filter0425p1mdw1.13037.57168B4A1D.0",
"event": "processed",
"category": ["tag1", "tag2"],
"custom1": "value1",
"custom2": "value2",
}
]
response = self.client.post(
"/anymail/sendgrid/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendGridTrackingWebhookView,
event=ANY, esp_name='SendGrid')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendGridTrackingWebhookView,
event=ANY,
esp_name="SendGrid",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "queued")
self.assertEqual(event.timestamp, datetime(2016, 4, 19, 19, 47, 26, tzinfo=timezone.utc))
self.assertEqual(
event.timestamp, datetime(2016, 4, 19, 19, 47, 26, tzinfo=timezone.utc)
)
self.assertEqual(event.esp_event, raw_events[0])
self.assertEqual(event.message_id, "3c2f4df8-c6dd-4cd2-9b91-6582b81a0349")
self.assertEqual(event.event_id, "ZyjAM5rnQmuI1KFInHQ3Nw")
@@ -51,52 +66,75 @@ class SendGridDeliveryTestCase(WebhookTestCase):
self.assertEqual(event.metadata, {"custom1": "value1", "custom2": "value2"})
def test_delivered_event(self):
raw_events = [{
"ip": "167.89.17.173",
"response": "250 2.0.0 OK 1461095248 m143si2210036ioe.159 - gsmtp ",
"smtp-id": "<wrfRRvF7Q0GgwUo2CvDmEA@ismtpd0006p1sjc2.sendgrid.net>",
"sg_event_id": "nOSv8m0eTQ-vxvwNwt3fZQ",
"sg_message_id": "wrfRRvF7Q0GgwUo2CvDmEA.filter0425p1mdw1.13037.57168B4A1D.0",
"tls": 1,
"event": "delivered",
"email": "recipient@example.com",
"timestamp": 1461095250,
"anymail_id": "4ab185c2-0171-492f-9ce0-27de258efc99"
}]
response = self.client.post('/anymail/sendgrid/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"ip": "167.89.17.173",
"response": "250 2.0.0 OK 1461095248 m143si2210036ioe.159 - gsmtp ",
"smtp-id": "<wrfRRvF7Q0GgwUo2CvDmEA@ismtpd0006p1sjc2.sendgrid.net>",
"sg_event_id": "nOSv8m0eTQ-vxvwNwt3fZQ",
"sg_message_id": "wrfRRvF7Q0GgwUo2CvDmEA"
".filter0425p1mdw1.13037.57168B4A1D.0",
"tls": 1,
"event": "delivered",
"email": "recipient@example.com",
"timestamp": 1461095250,
"anymail_id": "4ab185c2-0171-492f-9ce0-27de258efc99",
}
]
response = self.client.post(
"/anymail/sendgrid/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendGridTrackingWebhookView,
event=ANY, esp_name='SendGrid')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendGridTrackingWebhookView,
event=ANY,
esp_name="SendGrid",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "delivered")
self.assertEqual(event.timestamp, datetime(2016, 4, 19, 19, 47, 30, tzinfo=timezone.utc))
self.assertEqual(
event.timestamp, datetime(2016, 4, 19, 19, 47, 30, tzinfo=timezone.utc)
)
self.assertEqual(event.esp_event, raw_events[0])
self.assertEqual(event.message_id, "4ab185c2-0171-492f-9ce0-27de258efc99")
self.assertEqual(event.event_id, "nOSv8m0eTQ-vxvwNwt3fZQ")
self.assertEqual(event.recipient, "recipient@example.com")
self.assertEqual(event.mta_response, "250 2.0.0 OK 1461095248 m143si2210036ioe.159 - gsmtp ")
self.assertEqual(
event.mta_response, "250 2.0.0 OK 1461095248 m143si2210036ioe.159 - gsmtp "
)
self.assertEqual(event.tags, [])
self.assertEqual(event.metadata, {})
def test_dropped_invalid_event(self):
raw_events = [{
"email": "invalid@invalid",
"anymail_id": "c74002d9-7ccb-4f67-8b8c-766cec03c9a6",
"timestamp": 1461095250,
"smtp-id": "<wrfRRvF7Q0GgwUo2CvDmEA@ismtpd0006p1sjc2.sendgrid.net>",
"sg_event_id": "3NPOePGOTkeM_U3fgWApfg",
"sg_message_id": "filter0093p1las1.9128.5717FB8127.0",
"reason": "Invalid",
"event": "dropped"
}]
response = self.client.post('/anymail/sendgrid/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"email": "invalid@invalid",
"anymail_id": "c74002d9-7ccb-4f67-8b8c-766cec03c9a6",
"timestamp": 1461095250,
"smtp-id": "<wrfRRvF7Q0GgwUo2CvDmEA@ismtpd0006p1sjc2.sendgrid.net>",
"sg_event_id": "3NPOePGOTkeM_U3fgWApfg",
"sg_message_id": "filter0093p1las1.9128.5717FB8127.0",
"reason": "Invalid",
"event": "dropped",
}
]
response = self.client.post(
"/anymail/sendgrid/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendGridTrackingWebhookView,
event=ANY, esp_name='SendGrid')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendGridTrackingWebhookView,
event=ANY,
esp_name="SendGrid",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "rejected")
self.assertEqual(event.esp_event, raw_events[0])
@@ -107,22 +145,31 @@ class SendGridDeliveryTestCase(WebhookTestCase):
self.assertEqual(event.mta_response, None)
def test_dropped_unsubscribed_event(self):
raw_events = [{
"email": "unsubscribe@example.com",
"anymail_id": "a36ec0f9-aabe-45c7-9a84-3e17afb5cb65",
"timestamp": 1461095250,
"smtp-id": "<wrfRRvF7Q0GgwUo2CvDmEA@ismtpd0006p1sjc2.sendgrid.net>",
"sg_event_id": "oxy9OLwMTAy5EsuZn1qhIg",
"sg_message_id": "filter0199p1las1.4745.5717FB6F5.0",
"reason": "Unsubscribed Address",
"event": "dropped"
}]
response = self.client.post('/anymail/sendgrid/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"email": "unsubscribe@example.com",
"anymail_id": "a36ec0f9-aabe-45c7-9a84-3e17afb5cb65",
"timestamp": 1461095250,
"smtp-id": "<wrfRRvF7Q0GgwUo2CvDmEA@ismtpd0006p1sjc2.sendgrid.net>",
"sg_event_id": "oxy9OLwMTAy5EsuZn1qhIg",
"sg_message_id": "filter0199p1las1.4745.5717FB6F5.0",
"reason": "Unsubscribed Address",
"event": "dropped",
}
]
response = self.client.post(
"/anymail/sendgrid/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendGridTrackingWebhookView,
event=ANY, esp_name='SendGrid')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendGridTrackingWebhookView,
event=ANY,
esp_name="SendGrid",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "rejected")
self.assertEqual(event.esp_event, raw_events[0])
@@ -133,112 +180,170 @@ class SendGridDeliveryTestCase(WebhookTestCase):
self.assertEqual(event.mta_response, None)
def test_bounce_event(self):
raw_events = [{
"ip": "167.89.17.173",
"status": "5.1.1",
"smtp-id": "<wrfRRvF7Q0GgwUo2CvDmEA@ismtpd0006p1sjc2.sendgrid.net>",
"sg_event_id": "lC0Rc-FuQmKbnxCWxX1jRQ",
"reason": "550 5.1.1 The email account that you tried to reach does not exist.",
"sg_message_id": "Lli-03HcQ5-JLybO9fXsJg.filter0077p1las1.21536.5717FC482.0",
"tls": 1,
"event": "bounce",
"email": "noreply@example.com",
"timestamp": 1461095250,
"anymail_id": "de212213-bb66-4302-8f3f-20acdb7a104e",
"type": "bounce"
}]
response = self.client.post('/anymail/sendgrid/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"ip": "167.89.17.173",
"status": "5.1.1",
"smtp-id": "<wrfRRvF7Q0GgwUo2CvDmEA@ismtpd0006p1sjc2.sendgrid.net>",
"sg_event_id": "lC0Rc-FuQmKbnxCWxX1jRQ",
"reason": "550 5.1.1"
" The email account that you tried to reach does not exist.",
"sg_message_id": "Lli-03HcQ5-JLybO9fXsJg"
".filter0077p1las1.21536.5717FC482.0",
"tls": 1,
"event": "bounce",
"email": "noreply@example.com",
"timestamp": 1461095250,
"anymail_id": "de212213-bb66-4302-8f3f-20acdb7a104e",
"type": "bounce",
}
]
response = self.client.post(
"/anymail/sendgrid/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendGridTrackingWebhookView,
event=ANY, esp_name='SendGrid')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendGridTrackingWebhookView,
event=ANY,
esp_name="SendGrid",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "bounced")
self.assertEqual(event.esp_event, raw_events[0])
self.assertEqual(event.message_id, "de212213-bb66-4302-8f3f-20acdb7a104e")
self.assertEqual(event.event_id, "lC0Rc-FuQmKbnxCWxX1jRQ")
self.assertEqual(event.recipient, "noreply@example.com")
self.assertEqual(event.mta_response, "550 5.1.1 The email account that you tried to reach does not exist.")
self.assertEqual(
event.mta_response,
"550 5.1.1 The email account that you tried to reach does not exist.",
)
def test_deferred_event(self):
raw_events = [{
"response": "Email was deferred due to the following reason(s): [IPs were throttled by recipient server]",
"smtp-id": "<wrfRRvF7Q0GgwUo2CvDmEA@ismtpd0006p1sjc2.sendgrid.net>",
"sg_event_id": "b_syL5UiTvWC_Ky5L6Bs5Q",
"sg_message_id": "u9Gvi3mzT6iC2poAb58_qQ.filter0465p1mdw1.8054.5718271B40.0",
"event": "deferred",
"email": "recipient@example.com",
"attempt": "1",
"timestamp": 1461200990,
"anymail_id": "ccf83222-0d7e-4542-8beb-893122afa757",
}]
response = self.client.post('/anymail/sendgrid/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"response": "Email was deferred due to the following reason(s):"
" [IPs were throttled by recipient server]",
"smtp-id": "<wrfRRvF7Q0GgwUo2CvDmEA@ismtpd0006p1sjc2.sendgrid.net>",
"sg_event_id": "b_syL5UiTvWC_Ky5L6Bs5Q",
"sg_message_id": "u9Gvi3mzT6iC2poAb58_qQ.filter0465p1mdw1"
".8054.5718271B40.0",
"event": "deferred",
"email": "recipient@example.com",
"attempt": "1",
"timestamp": 1461200990,
"anymail_id": "ccf83222-0d7e-4542-8beb-893122afa757",
}
]
response = self.client.post(
"/anymail/sendgrid/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendGridTrackingWebhookView,
event=ANY, esp_name='SendGrid')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendGridTrackingWebhookView,
event=ANY,
esp_name="SendGrid",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "deferred")
self.assertEqual(event.esp_event, raw_events[0])
self.assertEqual(event.message_id, "ccf83222-0d7e-4542-8beb-893122afa757")
self.assertEqual(event.event_id, "b_syL5UiTvWC_Ky5L6Bs5Q")
self.assertEqual(event.recipient, "recipient@example.com")
self.assertEqual(event.mta_response,
"Email was deferred due to the following reason(s): [IPs were throttled by recipient server]")
self.assertEqual(
event.mta_response,
"Email was deferred due to the following reason(s):"
" [IPs were throttled by recipient server]",
)
def test_open_event(self):
raw_events = [{
"email": "recipient@example.com",
"timestamp": 1461095250,
"ip": "66.102.6.229",
"sg_event_id": "MjIwNDg5NTgtZGE3OC00NDI1LWFiMmMtMDUyZTU2ZmFkOTFm",
"sg_message_id": "wrfRRvF7Q0GgwUo2CvDmEA.filter0425p1mdw1.13037.57168B4A1D.0",
"anymail_id": "44920b35-3e31-478b-bb67-b4f5e0c85ebc",
"useragent": "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0",
"event": "open"
}]
response = self.client.post('/anymail/sendgrid/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"email": "recipient@example.com",
"timestamp": 1461095250,
"ip": "66.102.6.229",
"sg_event_id": "MjIwNDg5NTgtZGE3OC00NDI1LWFiMmMtMDUyZTU2ZmFkOTFm",
"sg_message_id": "wrfRRvF7Q0GgwUo2CvDmEA"
".filter0425p1mdw1.13037.57168B4A1D.0",
"anymail_id": "44920b35-3e31-478b-bb67-b4f5e0c85ebc",
"useragent": "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0",
"event": "open",
}
]
response = self.client.post(
"/anymail/sendgrid/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendGridTrackingWebhookView,
event=ANY, esp_name='SendGrid')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendGridTrackingWebhookView,
event=ANY,
esp_name="SendGrid",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "opened")
self.assertEqual(event.esp_event, raw_events[0])
self.assertEqual(event.message_id, "44920b35-3e31-478b-bb67-b4f5e0c85ebc")
self.assertEqual(event.event_id, "MjIwNDg5NTgtZGE3OC00NDI1LWFiMmMtMDUyZTU2ZmFkOTFm")
self.assertEqual(
event.event_id, "MjIwNDg5NTgtZGE3OC00NDI1LWFiMmMtMDUyZTU2ZmFkOTFm"
)
self.assertEqual(event.recipient, "recipient@example.com")
self.assertEqual(event.user_agent, "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0")
self.assertEqual(
event.user_agent, "Mozilla/5.0 (Windows NT 5.1; rv:11.0) Gecko Firefox/11.0"
)
def test_click_event(self):
raw_events = [{
"ip": "24.130.34.103",
"sg_event_id": "OTdlOGUzYjctYjc5Zi00OWE4LWE4YWUtNjIxNjk2ZTJlNGVi",
"sg_message_id": "_fjPjuJfRW-IPs5SuvYotg.filter0590p1mdw1.2098.57168CFC4B.0",
"useragent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36",
"anymail_id": "75de5af9-a090-4325-87f9-8c599ad66f60",
"event": "click",
"url_offset": {"index": 0, "type": "html"},
"email": "recipient@example.com",
"timestamp": 1461095250,
"url": "http://www.example.com"
}]
response = self.client.post('/anymail/sendgrid/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"ip": "24.130.34.103",
"sg_event_id": "OTdlOGUzYjctYjc5Zi00OWE4LWE4YWUtNjIxNjk2ZTJlNGVi",
"sg_message_id": "_fjPjuJfRW-IPs5SuvYotg"
".filter0590p1mdw1.2098.57168CFC4B.0",
"useragent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4)"
" AppleWebKit/537.36",
"anymail_id": "75de5af9-a090-4325-87f9-8c599ad66f60",
"event": "click",
"url_offset": {"index": 0, "type": "html"},
"email": "recipient@example.com",
"timestamp": 1461095250,
"url": "http://www.example.com",
}
]
response = self.client.post(
"/anymail/sendgrid/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendGridTrackingWebhookView,
event=ANY, esp_name='SendGrid')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendGridTrackingWebhookView,
event=ANY,
esp_name="SendGrid",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "clicked")
self.assertEqual(event.esp_event, raw_events[0])
self.assertEqual(event.message_id, "75de5af9-a090-4325-87f9-8c599ad66f60")
self.assertEqual(event.event_id, "OTdlOGUzYjctYjc5Zi00OWE4LWE4YWUtNjIxNjk2ZTJlNGVi")
self.assertEqual(
event.event_id, "OTdlOGUzYjctYjc5Zi00OWE4LWE4YWUtNjIxNjk2ZTJlNGVi"
)
self.assertEqual(event.recipient, "recipient@example.com")
self.assertEqual(event.user_agent, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36")
self.assertEqual(
event.user_agent,
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit/537.36",
)
self.assertEqual(event.click_url, "http://www.example.com")
def test_compatibility_message_id_from_smtp_id(self):
@@ -246,23 +351,35 @@ class SendGridDeliveryTestCase(WebhookTestCase):
# the `message_id`, and relied on SendGrid passing that to webhooks as
# 'smtp-id'. Make sure webhooks extract message_id for messages sent
# with earlier Anymail versions. (See issue #108.)
raw_events = [{
"ip": "167.89.17.173",
"response": "250 2.0.0 OK 1461095248 m143si2210036ioe.159 - gsmtp ",
"smtp-id": "<152712433591.85282.8340115595767222398@example.com>",
"sg_event_id": "nOSv8m0eTQ-vxvwNwt3fZQ",
"sg_message_id": "wrfRRvF7Q0GgwUo2CvDmEA.filter0425p1mdw1.13037.57168B4A1D.0",
"tls": 1,
"event": "delivered",
"email": "recipient@example.com",
"timestamp": 1461095250,
}]
response = self.client.post('/anymail/sendgrid/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"ip": "167.89.17.173",
"response": "250 2.0.0 OK 1461095248 m143si2210036ioe.159 - gsmtp ",
"smtp-id": "<152712433591.85282.8340115595767222398@example.com>",
"sg_event_id": "nOSv8m0eTQ-vxvwNwt3fZQ",
"sg_message_id": "wrfRRvF7Q0GgwUo2CvDmEA.filter0425p1mdw1"
".13037.57168B4A1D.0",
"tls": 1,
"event": "delivered",
"email": "recipient@example.com",
"timestamp": 1461095250,
}
]
response = self.client.post(
"/anymail/sendgrid/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendGridTrackingWebhookView,
event=ANY, esp_name='SendGrid')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendGridTrackingWebhookView,
event=ANY,
esp_name="SendGrid",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.message_id, "<152712433591.85282.8340115595767222398@example.com>")
self.assertEqual(
event.message_id, "<152712433591.85282.8340115595767222398@example.com>"
)
self.assertEqual(event.metadata, {}) # smtp-id not left in metadata

View File

@@ -1,5 +1,5 @@
import json
from base64 import b64encode, b64decode
from base64 import b64decode, b64encode
from datetime import date, datetime, timezone
from decimal import Decimal
from email.mime.base import MIMEBase
@@ -7,47 +7,76 @@ from email.mime.image import MIMEImage
from django.core import mail
from django.test import SimpleTestCase, override_settings, tag
from django.utils.timezone import get_fixed_timezone, override as override_current_timezone
from django.utils.timezone import (
get_fixed_timezone,
override as override_current_timezone,
)
from anymail.exceptions import (AnymailAPIError, AnymailConfigurationError, AnymailSerializationError,
AnymailUnsupportedFeature)
from anymail.exceptions import (
AnymailAPIError,
AnymailConfigurationError,
AnymailSerializationError,
AnymailUnsupportedFeature,
)
from anymail.message import attach_inline_image_file
from .mock_requests_backend import RequestsBackendMockAPITestCase, SessionSharingTestCases
from .utils import sample_image_content, sample_image_path, SAMPLE_IMAGE_FILENAME, AnymailTestMixin
from .mock_requests_backend import (
RequestsBackendMockAPITestCase,
SessionSharingTestCases,
)
from .utils import (
SAMPLE_IMAGE_FILENAME,
AnymailTestMixin,
sample_image_content,
sample_image_path,
)
@tag('sendinblue')
@override_settings(EMAIL_BACKEND='anymail.backends.sendinblue.EmailBackend',
ANYMAIL={'SENDINBLUE_API_KEY': 'test_api_key'})
@tag("sendinblue")
@override_settings(
EMAIL_BACKEND="anymail.backends.sendinblue.EmailBackend",
ANYMAIL={"SENDINBLUE_API_KEY": "test_api_key"},
)
class SendinBlueBackendMockAPITestCase(RequestsBackendMockAPITestCase):
# SendinBlue v3 success responses are empty
DEFAULT_RAW_RESPONSE = b'{"messageId":"<201801020304.1234567890@smtp-relay.mailin.fr>"}'
DEFAULT_STATUS_CODE = 201 # SendinBlue v3 uses '201 Created' for success (in most cases)
DEFAULT_RAW_RESPONSE = (
b'{"messageId":"<201801020304.1234567890@smtp-relay.mailin.fr>"}'
)
DEFAULT_STATUS_CODE = (
201 # SendinBlue v3 uses '201 Created' for success (in most cases)
)
def setUp(self):
super().setUp()
# Simple message useful for many tests
self.message = mail.EmailMultiAlternatives('Subject', 'Text Body', 'from@example.com', ['to@example.com'])
self.message = mail.EmailMultiAlternatives(
"Subject", "Text Body", "from@example.com", ["to@example.com"]
)
@tag('sendinblue')
@tag("sendinblue")
class SendinBlueBackendStandardEmailTests(SendinBlueBackendMockAPITestCase):
"""Test backend support for Django standard email features"""
def test_send_mail(self):
"""Test basic API for simple send"""
mail.send_mail('Subject here', 'Here is the message.',
'from@sender.example.com', ['to@example.com'], fail_silently=False)
self.assert_esp_called('https://api.sendinblue.com/v3/smtp/email')
mail.send_mail(
"Subject here",
"Here is the message.",
"from@sender.example.com",
["to@example.com"],
fail_silently=False,
)
self.assert_esp_called("https://api.sendinblue.com/v3/smtp/email")
http_headers = self.get_api_call_headers()
self.assertEqual(http_headers["api-key"], "test_api_key")
self.assertEqual(http_headers["Content-Type"], "application/json")
data = self.get_api_call_json()
self.assertEqual(data['subject'], "Subject here")
self.assertEqual(data['textContent'], "Here is the message.")
self.assertEqual(data['sender'], {'email': "from@sender.example.com"})
self.assertEqual(data['to'], [{'email': "to@example.com"}])
self.assertEqual(data["subject"], "Subject here")
self.assertEqual(data["textContent"], "Here is the message.")
self.assertEqual(data["sender"], {"email": "from@sender.example.com"})
self.assertEqual(data["to"], [{"email": "to@example.com"}])
def test_name_addr(self):
"""Make sure RFC2822 name-addr format (with display-name) is allowed
@@ -55,77 +84,114 @@ class SendinBlueBackendStandardEmailTests(SendinBlueBackendMockAPITestCase):
(Test both sender and recipient addresses)
"""
msg = mail.EmailMessage(
'Subject', 'Message', 'From Name <from@example.com>',
['Recipient #1 <to1@example.com>', 'to2@example.com'],
cc=['Carbon Copy <cc1@example.com>', 'cc2@example.com'],
bcc=['Blind Copy <bcc1@example.com>', 'bcc2@example.com'])
"Subject",
"Message",
"From Name <from@example.com>",
["Recipient #1 <to1@example.com>", "to2@example.com"],
cc=["Carbon Copy <cc1@example.com>", "cc2@example.com"],
bcc=["Blind Copy <bcc1@example.com>", "bcc2@example.com"],
)
msg.send()
data = self.get_api_call_json()
self.assertEqual(data['sender'], {'email': "from@example.com", 'name': "From Name"})
self.assertEqual(data['to'], [{'email': "to1@example.com", 'name': "Recipient #1"},
{'email': "to2@example.com"}])
self.assertEqual(data['cc'], [{'email': "cc1@example.com", 'name': "Carbon Copy"},
{'email': "cc2@example.com"}])
self.assertEqual(data['bcc'], [{'email': "bcc1@example.com", 'name': "Blind Copy"},
{'email': "bcc2@example.com"}])
self.assertEqual(
data["sender"], {"email": "from@example.com", "name": "From Name"}
)
self.assertEqual(
data["to"],
[
{"email": "to1@example.com", "name": "Recipient #1"},
{"email": "to2@example.com"},
],
)
self.assertEqual(
data["cc"],
[
{"email": "cc1@example.com", "name": "Carbon Copy"},
{"email": "cc2@example.com"},
],
)
self.assertEqual(
data["bcc"],
[
{"email": "bcc1@example.com", "name": "Blind Copy"},
{"email": "bcc2@example.com"},
],
)
def test_email_message(self):
email = mail.EmailMessage(
'Subject', 'Body goes here', 'from@example.com',
['to1@example.com', 'Also To <to2@example.com>'],
bcc=['bcc1@example.com', 'Also BCC <bcc2@example.com>'],
cc=['cc1@example.com', 'Also CC <cc2@example.com>'],
headers={'Reply-To': 'another@example.com',
'X-MyHeader': 'my value',
'Message-ID': '<mycustommsgid@sales.example.com>'}) # should override backend msgid
"Subject",
"Body goes here",
"from@example.com",
["to1@example.com", "Also To <to2@example.com>"],
bcc=["bcc1@example.com", "Also BCC <bcc2@example.com>"],
cc=["cc1@example.com", "Also CC <cc2@example.com>"],
headers={
"Reply-To": "another@example.com",
"X-MyHeader": "my value",
# should override backend msgid:
"Message-ID": "<mycustommsgid@sales.example.com>",
},
)
email.send()
data = self.get_api_call_json()
self.assertEqual(data['sender'], {'email': "from@example.com"})
self.assertEqual(data['subject'], "Subject")
self.assertEqual(data['textContent'], "Body goes here")
self.assertEqual(data['replyTo'], {'email': "another@example.com"})
self.assertEqual(data['headers'], {
'X-MyHeader': "my value",
'Message-ID': "<mycustommsgid@sales.example.com>",
})
self.assertEqual(data["sender"], {"email": "from@example.com"})
self.assertEqual(data["subject"], "Subject")
self.assertEqual(data["textContent"], "Body goes here")
self.assertEqual(data["replyTo"], {"email": "another@example.com"})
self.assertEqual(
data["headers"],
{
"X-MyHeader": "my value",
"Message-ID": "<mycustommsgid@sales.example.com>",
},
)
def test_html_message(self):
text_content = 'This is an important message.'
html_content = '<p>This is an <strong>important</strong> message.</p>'
email = mail.EmailMultiAlternatives('Subject', text_content,
'from@example.com', ['to@example.com'])
text_content = "This is an important message."
html_content = "<p>This is an <strong>important</strong> message.</p>"
email = mail.EmailMultiAlternatives(
"Subject", text_content, "from@example.com", ["to@example.com"]
)
email.attach_alternative(html_content, "text/html")
email.send()
data = self.get_api_call_json()
self.assertEqual(data['textContent'], text_content)
self.assertEqual(data['htmlContent'], html_content)
self.assertEqual(data["textContent"], text_content)
self.assertEqual(data["htmlContent"], html_content)
# Don't accidentally send the html part as an attachment:
self.assertNotIn('attachments', data)
self.assertNotIn("attachments", data)
def test_html_only_message(self):
html_content = '<p>This is an <strong>important</strong> message.</p>'
email = mail.EmailMessage('Subject', html_content, 'from@example.com', ['to@example.com'])
html_content = "<p>This is an <strong>important</strong> message.</p>"
email = mail.EmailMessage(
"Subject", html_content, "from@example.com", ["to@example.com"]
)
email.content_subtype = "html" # Main content is now text/html
email.send()
data = self.get_api_call_json()
self.assertEqual(data['htmlContent'], html_content)
self.assertNotIn('textContent', data)
self.assertEqual(data["htmlContent"], html_content)
self.assertNotIn("textContent", data)
def test_extra_headers(self):
self.message.extra_headers = {'X-Custom': 'string', 'X-Num': 123,
'Reply-To': '"Do Not Reply" <noreply@example.com>'}
self.message.extra_headers = {
"X-Custom": "string",
"X-Num": 123,
"Reply-To": '"Do Not Reply" <noreply@example.com>',
}
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['headers']['X-Custom'], 'string')
self.assertEqual(data["headers"]["X-Custom"], "string")
# Header values must be strings (changed 11/2022)
self.assertEqual(data['headers']['X-Num'], "123")
self.assertEqual(data["headers"]["X-Num"], "123")
# Reply-To must be moved to separate param
self.assertNotIn('Reply-To', data['headers'])
self.assertEqual(data['replyTo'], {'name': "Do Not Reply", 'email': "noreply@example.com"})
self.assertNotIn("Reply-To", data["headers"])
self.assertEqual(
data["replyTo"], {"name": "Do Not Reply", "email": "noreply@example.com"}
)
def test_extra_headers_serialization_error(self):
self.message.extra_headers = {'X-Custom': Decimal(12.5)}
self.message.extra_headers = {"X-Custom": Decimal(12.5)}
with self.assertRaisesMessage(AnymailSerializationError, "Decimal"):
self.message.send()
@@ -133,25 +199,37 @@ class SendinBlueBackendStandardEmailTests(SendinBlueBackendMockAPITestCase):
self.message.reply_to = ['"Reply recipient" <reply@example.com']
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['replyTo'], {'name': "Reply recipient", 'email': "reply@example.com"})
self.assertEqual(
data["replyTo"], {"name": "Reply recipient", "email": "reply@example.com"}
)
def test_multiple_reply_to(self):
# SendinBlue v3 only allows a single reply address
self.message.reply_to = ['"Reply recipient" <reply@example.com', 'reply2@example.com']
self.message.reply_to = [
'"Reply recipient" <reply@example.com',
"reply2@example.com",
]
with self.assertRaises(AnymailUnsupportedFeature):
self.message.send()
@override_settings(ANYMAIL_IGNORE_UNSUPPORTED_FEATURES=True)
def test_multiple_reply_to_ignore_unsupported(self):
# Should use first Reply-To if ignoring unsupported features
self.message.reply_to = ['"Reply recipient" <reply@example.com', 'reply2@example.com']
self.message.reply_to = [
'"Reply recipient" <reply@example.com',
"reply2@example.com",
]
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['replyTo'], {'name': "Reply recipient", 'email': "reply@example.com"})
self.assertEqual(
data["replyTo"], {"name": "Reply recipient", "email": "reply@example.com"}
)
def test_attachments(self):
text_content = "* Item one\n* Item two\n* Item three"
self.message.attach(filename="test.txt", content=text_content, mimetype="text/plain")
self.message.attach(
filename="test.txt", content=text_content, mimetype="text/plain"
)
# Should guess mimetype if not provided...
png_content = b"PNG\xb4 pretend this is the contents of a png file"
@@ -159,31 +237,41 @@ class SendinBlueBackendStandardEmailTests(SendinBlueBackendMockAPITestCase):
# Should work with a MIMEBase object (also tests no filename)...
pdf_content = b"PDF\xb4 pretend this is valid pdf data"
mimeattachment = MIMEBase('application', 'pdf')
mimeattachment = MIMEBase("application", "pdf")
mimeattachment.set_payload(pdf_content)
self.message.attach(mimeattachment)
self.message.send()
data = self.get_api_call_json()
self.assertEqual(len(data['attachment']), 3)
self.assertEqual(len(data["attachment"]), 3)
attachments = data['attachment']
self.assertEqual(attachments[0], {
'name': "test.txt",
'content': b64encode(text_content.encode('utf-8')).decode('ascii')})
self.assertEqual(attachments[1], {
'name': "test.png",
'content': b64encode(png_content).decode('ascii')})
self.assertEqual(attachments[2], {
'name': "",
'content': b64encode(pdf_content).decode('ascii')})
attachments = data["attachment"]
self.assertEqual(
attachments[0],
{
"name": "test.txt",
"content": b64encode(text_content.encode("utf-8")).decode("ascii"),
},
)
self.assertEqual(
attachments[1],
{"name": "test.png", "content": b64encode(png_content).decode("ascii")},
)
self.assertEqual(
attachments[2],
{"name": "", "content": b64encode(pdf_content).decode("ascii")},
)
def test_unicode_attachment_correctly_decoded(self):
self.message.attach("Une pièce jointe.html", '<p>\u2019</p>', mimetype='text/html')
self.message.attach(
"Une pièce jointe.html", "<p>\u2019</p>", mimetype="text/html"
)
self.message.send()
attachment = self.get_api_call_json()['attachment'][0]
self.assertEqual(attachment['name'], 'Une pièce jointe.html')
self.assertEqual(b64decode(attachment['content']).decode('utf-8'), '<p>\u2019</p>')
attachment = self.get_api_call_json()["attachment"][0]
self.assertEqual(attachment["name"], "Une pièce jointe.html")
self.assertEqual(
b64decode(attachment["content"]).decode("utf-8"), "<p>\u2019</p>"
)
def test_embedded_images(self):
# SendinBlue doesn't support inline image
@@ -193,7 +281,9 @@ class SendinBlueBackendStandardEmailTests(SendinBlueBackendMockAPITestCase):
image_path = sample_image_path(image_filename)
cid = attach_inline_image_file(self.message, image_path) # Read from a png file
html_content = '<p>This has an <img src="cid:%s" alt="inline" /> image.</p>' % cid
html_content = (
'<p>This has an <img src="cid:%s" alt="inline" /> image.</p>' % cid
)
self.message.attach_alternative(html_content, "text/html")
with self.assertRaises(AnymailUnsupportedFeature):
@@ -204,28 +294,38 @@ class SendinBlueBackendStandardEmailTests(SendinBlueBackendMockAPITestCase):
image_path = sample_image_path(image_filename)
image_data = sample_image_content(image_filename)
self.message.attach_file(image_path) # option 1: attach as a file
# option 1: attach as a file
self.message.attach_file(image_path)
image = MIMEImage(image_data) # option 2: construct the MIMEImage and attach it directly
# option 2: construct the MIMEImage and attach it directly
image = MIMEImage(image_data)
self.message.attach(image)
self.message.send()
image_data_b64 = b64encode(image_data).decode('ascii')
image_data_b64 = b64encode(image_data).decode("ascii")
data = self.get_api_call_json()
self.assertEqual(data['attachment'][0], {
'name': image_filename, # the named one
'content': image_data_b64,
})
self.assertEqual(data['attachment'][1], {
'name': '', # the unnamed one
'content': image_data_b64,
})
self.assertEqual(
data["attachment"][0],
{
"name": image_filename, # the named one
"content": image_data_b64,
},
)
self.assertEqual(
data["attachment"][1],
{
"name": "", # the unnamed one
"content": image_data_b64,
},
)
def test_multiple_html_alternatives(self):
self.message.body = "Text body"
self.message.attach_alternative("<p>First html is OK</p>", "text/html")
self.message.attach_alternative("<p>And maybe second html, too</p>", "text/html")
self.message.attach_alternative(
"<p>And maybe second html, too</p>", "text/html"
)
with self.assertRaises(AnymailUnsupportedFeature):
self.message.send()
@@ -240,11 +340,17 @@ class SendinBlueBackendStandardEmailTests(SendinBlueBackendMockAPITestCase):
def test_api_failure(self):
self.set_mock_response(status_code=400)
with self.assertRaisesMessage(AnymailAPIError, "SendinBlue API response 400"):
mail.send_mail('Subject', 'Body', 'from@example.com', ['to@example.com'])
mail.send_mail("Subject", "Body", "from@example.com", ["to@example.com"])
# Make sure fail_silently is respected
self.set_mock_response(status_code=400)
sent = mail.send_mail('Subject', 'Body', 'from@example.com', ['to@example.com'], fail_silently=True)
sent = mail.send_mail(
"Subject",
"Body",
"from@example.com",
["to@example.com"],
fail_silently=True,
)
self.assertEqual(sent, 0)
def test_api_error_includes_details(self):
@@ -267,26 +373,26 @@ class SendinBlueBackendStandardEmailTests(SendinBlueBackendMockAPITestCase):
self.message.send()
@tag('sendinblue')
@tag("sendinblue")
class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase):
"""Test backend support for Anymail added features"""
def test_envelope_sender(self):
# SendinBlue does not have a way to change envelope sender.
self.message.envelope_sender = "anything@bounces.example.com"
with self.assertRaisesMessage(AnymailUnsupportedFeature, 'envelope_sender'):
with self.assertRaisesMessage(AnymailUnsupportedFeature, "envelope_sender"):
self.message.send()
def test_metadata(self):
self.message.metadata = {'user_id': "12345", 'items': 6, 'float': 98.6}
self.message.metadata = {"user_id": "12345", "items": 6, "float": 98.6}
self.message.send()
data = self.get_api_call_json()
metadata = json.loads(data['headers']['X-Mailin-custom'])
self.assertEqual(metadata['user_id'], "12345")
self.assertEqual(metadata['items'], 6)
self.assertEqual(metadata['float'], 98.6)
metadata = json.loads(data["headers"]["X-Mailin-custom"])
self.assertEqual(metadata["user_id"], "12345")
self.assertEqual(metadata["items"], 6)
self.assertEqual(metadata["float"], 98.6)
def test_send_at(self):
utc_plus_6 = get_fixed_timezone(6 * 60)
@@ -294,47 +400,49 @@ class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase):
with override_current_timezone(utc_plus_6):
# Timezone-aware datetime converted to UTC:
self.message.send_at = datetime(2016, 3, 4, 5, 6, 7, 8000, tzinfo=utc_minus_8)
self.message.send_at = datetime(
2016, 3, 4, 5, 6, 7, 8000, tzinfo=utc_minus_8
)
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['scheduledAt'], "2016-03-04T05:06:07.008-08:00")
self.assertEqual(data["scheduledAt"], "2016-03-04T05:06:07.008-08:00")
# Explicit UTC:
self.message.send_at = datetime(2016, 3, 4, 5, 6, 7, tzinfo=timezone.utc)
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['scheduledAt'], "2016-03-04T05:06:07.000+00:00")
self.assertEqual(data["scheduledAt"], "2016-03-04T05:06:07.000+00:00")
# Timezone-naive datetime assumed to be Django current_timezone
# (also checks stripping microseconds)
self.message.send_at = datetime(2022, 10, 11, 12, 13, 14, 567)
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['scheduledAt'], "2022-10-11T12:13:14.000+06:00")
self.assertEqual(data["scheduledAt"], "2022-10-11T12:13:14.000+06:00")
# Date-only treated as midnight in current timezone
self.message.send_at = date(2022, 10, 22)
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['scheduledAt'], "2022-10-22T00:00:00.000+06:00")
self.assertEqual(data["scheduledAt"], "2022-10-22T00:00:00.000+06:00")
# POSIX timestamp
self.message.send_at = 1651820889 # 2022-05-06 07:08:09 UTC
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['scheduledAt'], "2022-05-06T07:08:09.000+00:00")
self.assertEqual(data["scheduledAt"], "2022-05-06T07:08:09.000+00:00")
# String passed unchanged (this is *not* portable between ESPs)
self.message.send_at = "2022-10-13T18:02:00.123-11:30"
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['scheduledAt'], "2022-10-13T18:02:00.123-11:30")
self.assertEqual(data["scheduledAt"], "2022-10-13T18:02:00.123-11:30")
def test_tag(self):
self.message.tags = ["receipt", "multiple"]
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['tags'], ["receipt", "multiple"])
self.assertEqual(data["tags"], ["receipt", "multiple"])
def test_tracking(self):
# Test one way...
@@ -353,36 +461,36 @@ class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase):
def test_template_id(self):
# subject, body, and from_email must be None for SendinBlue template send:
message = mail.EmailMessage(
subject='My Subject',
subject="My Subject",
body=None,
from_email='from@example.com',
to=['Recipient <to@example.com>'], # single 'to' recommended (all 'to' get the same message)
cc=['Recipient <cc1@example.com>', 'Recipient <cc2@example.com>'],
bcc=['Recipient <bcc@example.com>'],
reply_to=['Recipient <reply@example.com>'],
from_email="from@example.com",
# single 'to' recommended (all 'to' get the same message)
to=["Recipient <to@example.com>"],
cc=["Recipient <cc1@example.com>", "Recipient <cc2@example.com>"],
bcc=["Recipient <bcc@example.com>"],
reply_to=["Recipient <reply@example.com>"],
)
message.template_id = 12 # SendinBlue uses per-account numeric ID to identify templates
# SendinBlue uses per-account numeric ID to identify templates:
message.template_id = 12
message.send()
data = self.get_api_call_json()
self.assertEqual(data['templateId'], 12)
self.assertEqual(data['subject'], 'My Subject')
self.assertEqual(data['to'], [{'email': "to@example.com", 'name': 'Recipient'}])
self.assertEqual(data["templateId"], 12)
self.assertEqual(data["subject"], "My Subject")
self.assertEqual(data["to"], [{"email": "to@example.com", "name": "Recipient"}])
def test_merge_data(self):
self.message.merge_data = {
'alice@example.com': {':name': "Alice", ':group': "Developers"},
'bob@example.com': {':name': "Bob"}, # and leave :group undefined
"alice@example.com": {":name": "Alice", ":group": "Developers"},
"bob@example.com": {":name": "Bob"}, # and leave :group undefined
}
with self.assertRaises(AnymailUnsupportedFeature):
self.message.send()
def test_merge_global_data(self):
self.message.merge_global_data = {
'a': 'b'
}
self.message.merge_global_data = {"a": "b"}
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data['params'], {'a': 'b'})
self.assertEqual(data["params"], {"a": "b"})
def test_default_omits_options(self):
"""Make sure by default we don't send any ESP-specific options.
@@ -393,59 +501,74 @@ class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase):
"""
self.message.send()
data = self.get_api_call_json()
self.assertNotIn('attachment', data)
self.assertNotIn('tag', data)
self.assertNotIn('headers', data)
self.assertNotIn('replyTo', data)
self.assertNotIn('atributes', data)
self.assertNotIn("attachment", data)
self.assertNotIn("tag", data)
self.assertNotIn("headers", data)
self.assertNotIn("replyTo", data)
self.assertNotIn("atributes", data)
def test_esp_extra(self):
# SendinBlue doesn't offer any esp-extra but we will test
# with some extra of SendGrid to see if it's work in the future
self.message.esp_extra = {
'ip_pool_name': "transactional",
'asm': { # subscription management
'group_id': 1,
"ip_pool_name": "transactional",
"asm": { # subscription management
"group_id": 1,
},
'tracking_settings': {
'subscription_tracking': {
'enable': True,
'substitution_tag': '[unsubscribe_url]',
"tracking_settings": {
"subscription_tracking": {
"enable": True,
"substitution_tag": "[unsubscribe_url]",
},
},
}
self.message.send()
data = self.get_api_call_json()
# merged from esp_extra:
self.assertEqual(data['ip_pool_name'], "transactional")
self.assertEqual(data['asm'], {'group_id': 1})
self.assertEqual(data['tracking_settings']['subscription_tracking'],
{'enable': True, 'substitution_tag': "[unsubscribe_url]"})
self.assertEqual(data["ip_pool_name"], "transactional")
self.assertEqual(data["asm"], {"group_id": 1})
self.assertEqual(
data["tracking_settings"]["subscription_tracking"],
{"enable": True, "substitution_tag": "[unsubscribe_url]"},
)
# noinspection PyUnresolvedReferences
def test_send_attaches_anymail_status(self):
""" The anymail_status should be attached to the message when it is sent """
# the DEFAULT_RAW_RESPONSE above is the *only* success response SendinBlue returns,
# so no need to override it here
msg = mail.EmailMessage('Subject', 'Message', 'from@example.com', ['to1@example.com'], )
"""The anymail_status should be attached to the message when it is sent"""
# the DEFAULT_RAW_RESPONSE above is the *only* success response SendinBlue
# returns, so no need to override it here
msg = mail.EmailMessage(
"Subject",
"Message",
"from@example.com",
["to1@example.com"],
)
sent = msg.send()
self.assertEqual(sent, 1)
self.assertEqual(msg.anymail_status.status, {'queued'})
self.assertEqual(msg.anymail_status.recipients['to1@example.com'].status, 'queued')
self.assertEqual(msg.anymail_status.esp_response.content, self.DEFAULT_RAW_RESPONSE)
self.assertEqual(msg.anymail_status.status, {"queued"})
self.assertEqual(
msg.anymail_status.recipients["to1@example.com"].status, "queued"
)
self.assertEqual(
msg.anymail_status.esp_response.content, self.DEFAULT_RAW_RESPONSE
)
self.assertEqual(
msg.anymail_status.message_id,
json.loads(msg.anymail_status.esp_response.content.decode('utf-8'))['messageId']
json.loads(msg.anymail_status.esp_response.content.decode("utf-8"))[
"messageId"
],
)
self.assertEqual(
msg.anymail_status.recipients['to1@example.com'].message_id,
json.loads(msg.anymail_status.esp_response.content.decode('utf-8'))['messageId']
msg.anymail_status.recipients["to1@example.com"].message_id,
json.loads(msg.anymail_status.esp_response.content.decode("utf-8"))[
"messageId"
],
)
# noinspection PyUnresolvedReferences
def test_send_failed_anymail_status(self):
""" If the send fails, anymail_status should contain initial values"""
"""If the send fails, anymail_status should contain initial values"""
self.set_mock_response(status_code=500)
sent = self.message.send(fail_silently=True)
self.assertEqual(sent, 0)
@@ -455,18 +578,22 @@ class SendinBlueBackendAnymailFeatureTests(SendinBlueBackendMockAPITestCase):
def test_json_serialization_errors(self):
"""Try to provide more information about non-json-serializable data"""
self.message.esp_extra = {'total': Decimal('19.99')}
self.message.esp_extra = {"total": Decimal("19.99")}
with self.assertRaises(AnymailSerializationError) as cm:
self.message.send()
err = cm.exception
self.assertIsInstance(err, TypeError) # compatibility with json.dumps
self.assertIn("Don't know how to send this data to SendinBlue", str(err)) # our added context
self.assertRegex(str(err), r"Decimal.*is not JSON serializable") # original message
# our added context:
self.assertIn("Don't know how to send this data to SendinBlue", str(err))
# original message
self.assertRegex(str(err), r"Decimal.*is not JSON serializable")
@tag('sendinblue')
@tag("sendinblue")
class SendinBlueBackendRecipientsRefusedTests(SendinBlueBackendMockAPITestCase):
"""Should raise AnymailRecipientsRefused when *all* recipients are rejected or invalid"""
"""
Should raise AnymailRecipientsRefused when *all* recipients are rejected or invalid
"""
# SendinBlue doesn't check email bounce or complaint lists at time of send --
# it always just queues the message. You'll need to listen for the "rejected"
@@ -474,17 +601,22 @@ class SendinBlueBackendRecipientsRefusedTests(SendinBlueBackendMockAPITestCase):
pass # not applicable to this backend
@tag('sendinblue')
class SendinBlueBackendSessionSharingTestCase(SessionSharingTestCases, SendinBlueBackendMockAPITestCase):
@tag("sendinblue")
class SendinBlueBackendSessionSharingTestCase(
SessionSharingTestCases, SendinBlueBackendMockAPITestCase
):
"""Requests session sharing tests"""
pass # tests are defined in SessionSharingTestCases
@tag('sendinblue')
@tag("sendinblue")
@override_settings(EMAIL_BACKEND="anymail.backends.sendinblue.EmailBackend")
class SendinBlueBackendImproperlyConfiguredTests(AnymailTestMixin, SimpleTestCase):
"""Test ESP backend without required settings in place"""
def test_missing_auth(self):
with self.assertRaisesRegex(AnymailConfigurationError, r'\bSENDINBLUE_API_KEY\b'):
mail.send_mail('Subject', 'Message', 'from@example.com', ['to@example.com'])
with self.assertRaisesRegex(
AnymailConfigurationError, r"\bSENDINBLUE_API_KEY\b"
):
mail.send_mail("Subject", "Message", "from@example.com", ["to@example.com"])

View File

@@ -10,17 +10,21 @@ from anymail.message import AnymailMessage
from .utils import AnymailTestMixin
ANYMAIL_TEST_SENDINBLUE_API_KEY = os.getenv('ANYMAIL_TEST_SENDINBLUE_API_KEY')
ANYMAIL_TEST_SENDINBLUE_DOMAIN = os.getenv('ANYMAIL_TEST_SENDINBLUE_DOMAIN')
ANYMAIL_TEST_SENDINBLUE_API_KEY = os.getenv("ANYMAIL_TEST_SENDINBLUE_API_KEY")
ANYMAIL_TEST_SENDINBLUE_DOMAIN = os.getenv("ANYMAIL_TEST_SENDINBLUE_DOMAIN")
@tag('sendinblue', 'live')
@unittest.skipUnless(ANYMAIL_TEST_SENDINBLUE_API_KEY and ANYMAIL_TEST_SENDINBLUE_DOMAIN,
"Set ANYMAIL_TEST_SENDINBLUE_API_KEY and ANYMAIL_TEST_SENDINBLUE_DOMAIN "
"environment variables to run SendinBlue integration tests")
@override_settings(ANYMAIL_SENDINBLUE_API_KEY=ANYMAIL_TEST_SENDINBLUE_API_KEY,
ANYMAIL_SENDINBLUE_SEND_DEFAULTS=dict(),
EMAIL_BACKEND="anymail.backends.sendinblue.EmailBackend")
@tag("sendinblue", "live")
@unittest.skipUnless(
ANYMAIL_TEST_SENDINBLUE_API_KEY and ANYMAIL_TEST_SENDINBLUE_DOMAIN,
"Set ANYMAIL_TEST_SENDINBLUE_API_KEY and ANYMAIL_TEST_SENDINBLUE_DOMAIN "
"environment variables to run SendinBlue integration tests",
)
@override_settings(
ANYMAIL_SENDINBLUE_API_KEY=ANYMAIL_TEST_SENDINBLUE_API_KEY,
ANYMAIL_SENDINBLUE_SEND_DEFAULTS=dict(),
EMAIL_BACKEND="anymail.backends.sendinblue.EmailBackend",
)
class SendinBlueBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
"""SendinBlue v3 API integration tests
@@ -36,10 +40,14 @@ class SendinBlueBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
def setUp(self):
super().setUp()
self.from_email = 'from@%s' % ANYMAIL_TEST_SENDINBLUE_DOMAIN
self.message = AnymailMessage('Anymail SendinBlue integration test', 'Text content',
self.from_email, ['test+to1@anymail.dev'])
self.message.attach_alternative('<p>HTML content</p>', "text/html")
self.from_email = "from@%s" % ANYMAIL_TEST_SENDINBLUE_DOMAIN
self.message = AnymailMessage(
"Anymail SendinBlue integration test",
"Text content",
self.from_email,
["test+to1@anymail.dev"],
)
self.message.attach_alternative("<p>HTML content</p>", "text/html")
def test_simple_send(self):
# Example of getting the SendinBlue send status and message id from the message
@@ -47,12 +55,14 @@ class SendinBlueBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
self.assertEqual(sent_count, 1)
anymail_status = self.message.anymail_status
sent_status = anymail_status.recipients['test+to1@anymail.dev'].status
message_id = anymail_status.recipients['test+to1@anymail.dev'].message_id
sent_status = anymail_status.recipients["test+to1@anymail.dev"].status
message_id = anymail_status.recipients["test+to1@anymail.dev"].message_id
self.assertEqual(sent_status, 'queued') # SendinBlue always queues
self.assertRegex(message_id, r'\<.+@.+\>') # Message-ID can be ...@smtp-relay.mail.fr or .sendinblue.com
self.assertEqual(anymail_status.status, {sent_status}) # set of all recipient statuses
self.assertEqual(sent_status, "queued") # SendinBlue always queues
# Message-ID can be ...@smtp-relay.mail.fr or .sendinblue.com:
self.assertRegex(message_id, r"\<.+@.+\>")
# set of all recipient statuses:
self.assertEqual(anymail_status.status, {sent_status})
self.assertEqual(anymail_status.message_id, message_id)
def test_all_options(self):
@@ -64,27 +74,32 @@ class SendinBlueBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
to=["test+to1@anymail.dev", '"Recipient 2, OK?" <test+to2@anymail.dev>'],
cc=["test+cc1@anymail.dev", "Copy 2 <test+cc2@anymail.dev>"],
bcc=["test+bcc1@anymail.dev", "Blind Copy 2 <test+bcc2@anymail.dev>"],
reply_to=['"Reply, with comma" <reply@example.com>'], # SendinBlue API v3 only supports single reply-to
# SendinBlue API v3 only supports single reply-to
reply_to=['"Reply, with comma" <reply@example.com>'],
headers={"X-Anymail-Test": "value", "X-Anymail-Count": 3},
metadata={"meta1": "simple string", "meta2": 2},
send_at=send_at,
tags=["tag 1", "tag 2"],
)
message.attach_alternative('<p>HTML content</p>', "text/html") # SendinBlue requires an HTML body
# SendinBlue requires an HTML body:
message.attach_alternative("<p>HTML content</p>", "text/html")
message.attach("attachment1.txt", "Here is some\ntext for you", "text/plain")
message.attach("attachment2.csv", "ID,Name\n1,Amy Lina", "text/csv")
message.send()
self.assertEqual(message.anymail_status.status, {'queued'}) # SendinBlue always queues
self.assertRegex(message.anymail_status.message_id, r'\<.+@.+\>')
# SendinBlue always queues:
self.assertEqual(message.anymail_status.status, {"queued"})
self.assertRegex(message.anymail_status.message_id, r"\<.+@.+\>")
def test_template(self):
message = AnymailMessage(
template_id=5, # There is a *new-style* template with this id in the Anymail test account
from_email=formataddr(('Sender', self.from_email)), # Override template sender
to=["Recipient <test+to1@anymail.dev>"], # No batch send (so max one recipient suggested)
# There is a *new-style* template with this id in the Anymail test account:
template_id=5,
# Override template sender:
from_email=formataddr("Sender", self.from_email),
# No batch send (so max one recipient suggested):
to=["Recipient <test+to1@anymail.dev>"],
reply_to=["Do not reply <reply@example.dev>"],
tags=["using-template"],
headers={"X-Anymail-Test": "group: A, variation: C"},
@@ -98,20 +113,25 @@ class SendinBlueBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
)
# Normal attachments don't work with Sendinblue templates:
# message.attach("attachment1.txt", "Here is some\ntext for you", "text/plain")
# message.attach("attachment1.txt", "Here is some\ntext", "text/plain")
# If you can host the attachment content on some publicly-accessible URL,
# this *non-portable* alternative allows sending attachments with templates:
message.esp_extra = {
'attachment': [{
'name': 'attachment1.txt',
# URL where Sendinblue can download the attachment content while sending:
'url': 'https://raw.githubusercontent.com/anymail/django-anymail/main/AUTHORS.txt',
}]
"attachment": [
{
"name": "attachment1.txt",
# URL where Sendinblue can download
# the attachment content while sending:
"url": "https://raw.githubusercontent.com/anymail"
"/django-anymail/main/AUTHORS.txt",
}
]
}
message.send()
self.assertEqual(message.anymail_status.status, {'queued'}) # SendinBlue always queues
self.assertRegex(message.anymail_status.message_id, r'\<.+@.+\>')
# SendinBlue always queues:
self.assertEqual(message.anymail_status.status, {"queued"})
self.assertRegex(message.anymail_status.message_id, r"\<.+@.+\>")
@override_settings(ANYMAIL_SENDINBLUE_API_KEY="Hey, that's not an API key!")
def test_invalid_api_key(self):

View File

@@ -6,19 +6,23 @@ from django.test import tag
from anymail.signals import AnymailTrackingEvent
from anymail.webhooks.sendinblue import SendinBlueTrackingWebhookView
from .webhook_cases import WebhookBasicAuthTestCase, WebhookTestCase
@tag('sendinblue')
@tag("sendinblue")
class SendinBlueWebhookSecurityTestCase(WebhookBasicAuthTestCase):
def call_webhook(self):
return self.client.post('/anymail/sendinblue/tracking/',
content_type='application/json', data=json.dumps({}))
return self.client.post(
"/anymail/sendinblue/tracking/",
content_type="application/json",
data=json.dumps({}),
)
# Actual tests are in WebhookBasicAuthTestCase
@tag('sendinblue')
@tag("sendinblue")
class SendinBlueDeliveryTestCase(WebhookTestCase):
# SendinBlue's webhook payload data is partially documented at
# https://help.sendinblue.com/hc/en-us/articles/360007666479,
@@ -29,16 +33,15 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
raw_event = {
"event": "request",
"email": "recipient@example.com",
"id": 9999999, # this appears to be a SendinBlue account id (not an event id)
"id": 9999999, # this seems to be SendinBlue account id (not an event id)
"message-id": "<201803062010.27287306012@smtp-relay.mailin.fr>",
"subject": "Test subject",
# From a message sent at 2018-03-06 11:10:23-08:00 (2018-03-06 19:10:23+00:00)...
"date": "2018-03-06 11:10:23", # uses time zone from SendinBlue account's preferences
# From a message sent at 2018-03-06 11:10:23-08:00
# (2018-03-06 19:10:23+00:00)...
"date": "2018-03-06 11:10:23", # tz from SendinBlue account's preferences
"ts": 1520331023, # 2018-03-06 10:10:23 -- what time zone is this?
"ts_event": 1520331023, # unclear if this ever differs from "ts"
"ts_epoch": 1520363423000, # 2018-03-06 19:10:23.000+00:00 -- UTC (milliseconds)
"ts_epoch": 1520363423000, # 2018-03-06 19:10:23.000+00:00 -- UTC (msec)
"X-Mailin-custom": '{"meta": "data"}',
# "tag" is JSON-serialized tags array if `tags` param set on send,
# else single tag string if `X-Mailin-Tag` header set on send,
@@ -50,18 +53,31 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
"template_id": 12,
"sending_ip": "333.33.33.33",
}
response = self.client.post('/anymail/sendinblue/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/sendinblue/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendinBlueTrackingWebhookView,
event=ANY, esp_name='SendinBlue')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendinBlueTrackingWebhookView,
event=ANY,
esp_name="SendinBlue",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "queued")
self.assertEqual(event.esp_event, raw_event)
self.assertEqual(event.timestamp, datetime(2018, 3, 6, 19, 10, 23, microsecond=0, tzinfo=timezone.utc))
self.assertEqual(event.message_id, "<201803062010.27287306012@smtp-relay.mailin.fr>")
self.assertIsNone(event.event_id) # SendinBlue does not provide a unique event id
self.assertEqual(
event.timestamp,
datetime(2018, 3, 6, 19, 10, 23, microsecond=0, tzinfo=timezone.utc),
)
self.assertEqual(
event.message_id, "<201803062010.27287306012@smtp-relay.mailin.fr>"
)
# SendinBlue does not provide a unique event id:
self.assertIsNone(event.event_id)
self.assertEqual(event.recipient, "recipient@example.com")
self.assertEqual(event.metadata, {"meta": "data"})
self.assertEqual(event.tags, ["tag1", "tag2"])
@@ -75,18 +91,28 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
"ts_epoch": 1520363423000,
"message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>",
}
response = self.client.post('/anymail/sendinblue/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/sendinblue/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendinBlueTrackingWebhookView,
event=ANY, esp_name='SendinBlue')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendinBlueTrackingWebhookView,
event=ANY,
esp_name="SendinBlue",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "delivered")
self.assertEqual(event.esp_event, raw_event)
self.assertEqual(event.message_id, "<201803011158.9876543210@smtp-relay.mailin.fr>")
self.assertEqual(
event.message_id, "<201803011158.9876543210@smtp-relay.mailin.fr>"
)
self.assertEqual(event.recipient, "recipient@example.com")
self.assertEqual(event.metadata, {}) # empty dict when no X-Mailin-custom header given
# empty dict when no X-Mailin-custom header given:
self.assertEqual(event.metadata, {})
self.assertEqual(event.tags, []) # empty list when no tags given
def test_hard_bounce(self):
@@ -96,19 +122,30 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
"ts_epoch": 1520363423000,
"message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>",
# the leading space in the reason is as received in actual testing:
"reason": " RecipientError: 550 5.5.0 Requested action not taken: mailbox unavailable.",
"reason": " RecipientError: 550 5.5.0"
" Requested action not taken: mailbox unavailable.",
"tag": "header-tag",
}
response = self.client.post('/anymail/sendinblue/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/sendinblue/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendinBlueTrackingWebhookView,
event=ANY, esp_name='SendinBlue')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendinBlueTrackingWebhookView,
event=ANY,
esp_name="SendinBlue",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "bounced")
self.assertEqual(event.reject_reason, "bounced")
self.assertEqual(event.mta_response,
" RecipientError: 550 5.5.0 Requested action not taken: mailbox unavailable.")
self.assertEqual(
event.mta_response,
" RecipientError: 550 5.5.0"
" Requested action not taken: mailbox unavailable.",
)
self.assertEqual(event.tags, ["header-tag"])
def test_soft_bounce_event(self):
@@ -119,16 +156,27 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
"message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>",
"reason": "undefined Unable to find MX of domain no-mx.example.com",
}
response = self.client.post('/anymail/sendinblue/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/sendinblue/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendinBlueTrackingWebhookView,
event=ANY, esp_name='SendinBlue')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendinBlueTrackingWebhookView,
event=ANY,
esp_name="SendinBlue",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "bounced")
self.assertEqual(event.reject_reason, "bounced")
self.assertIsNone(event.description) # no human-readable description consistently available
self.assertEqual(event.mta_response, "undefined Unable to find MX of domain no-mx.example.com")
# no human-readable description consistently available:
self.assertIsNone(event.description)
self.assertEqual(
event.mta_response,
"undefined Unable to find MX of domain no-mx.example.com",
)
def test_blocked(self):
raw_event = {
@@ -138,12 +186,19 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
"message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>",
"reason": "blocked : due to blacklist user",
}
response = self.client.post('/anymail/sendinblue/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/sendinblue/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendinBlueTrackingWebhookView,
event=ANY, esp_name='SendinBlue')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendinBlueTrackingWebhookView,
event=ANY,
esp_name="SendinBlue",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "rejected")
self.assertEqual(event.reject_reason, "blocked")
self.assertEqual(event.mta_response, "blocked : due to blacklist user")
@@ -157,18 +212,26 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
"ts_epoch": 1520363423000,
"message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>",
}
response = self.client.post('/anymail/sendinblue/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/sendinblue/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendinBlueTrackingWebhookView,
event=ANY, esp_name='SendinBlue')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendinBlueTrackingWebhookView,
event=ANY,
esp_name="SendinBlue",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "complained")
def test_invalid_email(self):
# "If a ISP again indicated us that the email is not valid or if we discovered that the email is not valid."
# (unclear whether this error originates with the receiving MTA or with SendinBlue pre-send)
# (haven't observed "invalid_email" event in actual testing; payload below is a guess)
# "If a ISP again indicated us that the email is not valid or if we discovered
# that the email is not valid." (unclear whether this error originates with the
# receiving MTA or with SendinBlue pre-send) (haven't observed "invalid_email"
# event in actual testing; payload below is a guess)
raw_event = {
"event": "invalid_email",
"email": "recipient@example.com",
@@ -176,73 +239,106 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
"message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>",
"reason": "(guessing invalid_email includes a reason)",
}
response = self.client.post('/anymail/sendinblue/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/sendinblue/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendinBlueTrackingWebhookView,
event=ANY, esp_name='SendinBlue')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendinBlueTrackingWebhookView,
event=ANY,
esp_name="SendinBlue",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "bounced")
self.assertEqual(event.reject_reason, "invalid")
self.assertEqual(event.mta_response, "(guessing invalid_email includes a reason)")
self.assertEqual(
event.mta_response, "(guessing invalid_email includes a reason)"
)
def test_deferred_event(self):
# Note: the example below is an actual event capture (with 'example.com' substituted
# for the real receiving domain). It's pretty clearly a bounce, not a deferral.
# It looks like SendinBlue mis-categorizes this SMTP response code.
# Note: the example below is an actual event capture (with 'example.com'
# substituted for the real receiving domain). It's pretty clearly a bounce, not
# a deferral. It looks like SendinBlue mis-categorizes this SMTP response code.
raw_event = {
"event": "deferred",
"email": "notauser@example.com",
"ts_epoch": 1520363423000,
"message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>",
"reason": "550 RecipientError: 550 5.1.1 <notauser@example.com>: Recipient address rejected: "
"User unknown in virtual alias table",
"reason": "550 RecipientError: 550 5.1.1 <notauser@example.com>: Recipient"
" address rejected: User unknown in virtual alias table",
}
response = self.client.post('/anymail/sendinblue/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/sendinblue/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendinBlueTrackingWebhookView,
event=ANY, esp_name='SendinBlue')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendinBlueTrackingWebhookView,
event=ANY,
esp_name="SendinBlue",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "deferred")
self.assertIsNone(event.description) # no human-readable description consistently available
self.assertEqual(event.mta_response,
"550 RecipientError: 550 5.1.1 <notauser@example.com>: Recipient address rejected: "
"User unknown in virtual alias table")
# no human-readable description consistently available:
self.assertIsNone(event.description)
self.assertEqual(
event.mta_response,
"550 RecipientError: 550 5.1.1 <notauser@example.com>:"
" Recipient address rejected: User unknown in virtual alias table",
)
def test_opened_event(self):
# SendinBlue delivers unique_opened *and* opened on the first open.
# To avoid double-counting, you should only enable one of the two events in SendinBlue.
# SendinBlue delivers unique_opened *and* opened on the first open. To avoid
# double-counting, you should only enable one of the two events in SendinBlue.
raw_event = {
"event": "opened",
"email": "recipient@example.com",
"ts_epoch": 1520363423000,
"message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>",
}
response = self.client.post('/anymail/sendinblue/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/sendinblue/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendinBlueTrackingWebhookView,
event=ANY, esp_name='SendinBlue')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendinBlueTrackingWebhookView,
event=ANY,
esp_name="SendinBlue",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "opened")
self.assertIsNone(event.user_agent) # SendinBlue doesn't report user agent
def test_unique_opened_event(self):
# SendinBlue delivers unique_opened *and* opened on the first open.
# To avoid double-counting, you should only enable one of the two events in SendinBlue.
# SendinBlue delivers unique_opened *and* opened on the first open. To avoid
# double-counting, you should only enable one of the two events in SendinBlue.
raw_event = {
"event": "unique_opened",
"email": "recipient@example.com",
"ts_epoch": 1520363423000,
"message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>",
}
response = self.client.post('/anymail/sendinblue/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/sendinblue/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendinBlueTrackingWebhookView,
event=ANY, esp_name='SendinBlue')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendinBlueTrackingWebhookView,
event=ANY,
esp_name="SendinBlue",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "opened")
def test_clicked_event(self):
@@ -253,29 +349,44 @@ class SendinBlueDeliveryTestCase(WebhookTestCase):
"message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>",
"link": "https://example.com/click/me",
}
response = self.client.post('/anymail/sendinblue/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/sendinblue/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendinBlueTrackingWebhookView,
event=ANY, esp_name='SendinBlue')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendinBlueTrackingWebhookView,
event=ANY,
esp_name="SendinBlue",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "clicked")
self.assertEqual(event.click_url, "https://example.com/click/me")
self.assertIsNone(event.user_agent) # SendinBlue doesn't report user agent
def test_unsubscribe(self):
# "When a person unsubscribes from the email received."
# (haven't observed "unsubscribe" event in actual testing; payload below is a guess)
# (haven't observed "unsubscribe" event in actual testing;
# payload below is a guess)
raw_event = {
"event": "unsubscribe",
"email": "recipient@example.com",
"ts_epoch": 1520363423000,
"message-id": "<201803011158.9876543210@smtp-relay.mailin.fr>",
}
response = self.client.post('/anymail/sendinblue/tracking/',
content_type='application/json', data=json.dumps(raw_event))
response = self.client.post(
"/anymail/sendinblue/tracking/",
content_type="application/json",
data=json.dumps(raw_event),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SendinBlueTrackingWebhookView,
event=ANY, esp_name='SendinBlue')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SendinBlueTrackingWebhookView,
event=ANY,
esp_name="SendinBlue",
)
event = kwargs["event"]
self.assertEqual(event.event_type, "unsubscribed")

View File

@@ -1,5 +1,5 @@
from django.urls import include, re_path
urlpatterns = [
re_path(r'^anymail/', include('anymail.urls')),
re_path(r"^anymail/", include("anymail.urls")),
]

View File

@@ -7,20 +7,34 @@ from email.mime.text import MIMEText
from django.core import mail
from django.test import override_settings, tag
from django.utils.timezone import get_fixed_timezone, override as override_current_timezone
from django.utils.timezone import (
get_fixed_timezone,
override as override_current_timezone,
)
from anymail.exceptions import (
AnymailAPIError, AnymailConfigurationError, AnymailRecipientsRefused,
AnymailSerializationError, AnymailUnsupportedFeature)
AnymailAPIError,
AnymailConfigurationError,
AnymailRecipientsRefused,
AnymailSerializationError,
AnymailUnsupportedFeature,
)
from anymail.message import attach_inline_image_file
from .mock_requests_backend import RequestsBackendMockAPITestCase
from .utils import SAMPLE_IMAGE_FILENAME, decode_att, sample_image_content, sample_image_path
from .utils import (
SAMPLE_IMAGE_FILENAME,
decode_att,
sample_image_content,
sample_image_path,
)
@tag('sparkpost')
@override_settings(EMAIL_BACKEND='anymail.backends.sparkpost.EmailBackend',
ANYMAIL={'SPARKPOST_API_KEY': 'test_api_key'})
@tag("sparkpost")
@override_settings(
EMAIL_BACKEND="anymail.backends.sparkpost.EmailBackend",
ANYMAIL={"SPARKPOST_API_KEY": "test_api_key"},
)
class SparkPostBackendMockAPITestCase(RequestsBackendMockAPITestCase):
"""TestCase that uses SparkPostEmailBackend with a mocked transmissions.send API"""
@@ -35,32 +49,40 @@ class SparkPostBackendMockAPITestCase(RequestsBackendMockAPITestCase):
def setUp(self):
super().setUp()
# Simple message useful for many tests
self.message = mail.EmailMultiAlternatives('Subject', 'Text Body',
'from@example.com', ['to@example.com'])
self.message = mail.EmailMultiAlternatives(
"Subject", "Text Body", "from@example.com", ["to@example.com"]
)
def set_mock_result(self, accepted=1, rejected=0, id="12345678901234567890"):
"""Set a mock response that reflects count of accepted/rejected recipients"""
raw = json.dumps({
"results": {
"id": id,
"total_accepted_recipients": accepted,
"total_rejected_recipients": rejected,
raw = json.dumps(
{
"results": {
"id": id,
"total_accepted_recipients": accepted,
"total_rejected_recipients": rejected,
}
}
}).encode("utf-8")
).encode("utf-8")
self.set_mock_response(raw=raw)
return raw
@tag('sparkpost')
@tag("sparkpost")
class SparkPostBackendStandardEmailTests(SparkPostBackendMockAPITestCase):
"""Test backend support for Django standard email features"""
def test_send_mail(self):
"""Test basic API for simple send"""
mail.send_mail('Subject here', 'Here is the message.',
'from@example.com', ['to@example.com'], fail_silently=False)
mail.send_mail(
"Subject here",
"Here is the message.",
"from@example.com",
["to@example.com"],
fail_silently=False,
)
self.assert_esp_called('/api/v1/transmissions/')
self.assert_esp_called("/api/v1/transmissions/")
headers = self.get_api_call_headers()
self.assertEqual("test_api_key", headers["Authorization"])
@@ -69,9 +91,10 @@ class SparkPostBackendStandardEmailTests(SparkPostBackendMockAPITestCase):
self.assertEqual(data["content"]["subject"], "Subject here")
self.assertEqual(data["content"]["text"], "Here is the message.")
self.assertEqual(data["content"]["from"], "from@example.com")
self.assertEqual(data['recipients'], [{
"address": {"email": "to@example.com", "header_to": "to@example.com"}
}])
self.assertEqual(
data["recipients"],
[{"address": {"email": "to@example.com", "header_to": "to@example.com"}}],
)
def test_name_addr(self):
"""Make sure RFC2822 name-addr format (with display-name) is allowed
@@ -80,73 +103,102 @@ class SparkPostBackendStandardEmailTests(SparkPostBackendMockAPITestCase):
"""
self.set_mock_result(accepted=6)
msg = mail.EmailMessage(
'Subject', 'Message', 'From Name <from@example.com>',
['Recipient #1 <to1@example.com>', 'to2@example.com'],
cc=['Carbon Copy <cc1@example.com>', 'cc2@example.com'],
bcc=['Blind Copy <bcc1@example.com>', 'bcc2@example.com'])
"Subject",
"Message",
"From Name <from@example.com>",
["Recipient #1 <to1@example.com>", "to2@example.com"],
cc=["Carbon Copy <cc1@example.com>", "cc2@example.com"],
bcc=["Blind Copy <bcc1@example.com>", "bcc2@example.com"],
)
msg.send()
data = self.get_api_call_json()
self.assertEqual(data["content"]["from"], "From Name <from@example.com>")
# This also checks recipient generation for cc and bcc. Because it's *not*
# a batch send, each recipient should see a To header reflecting all To addresses.
self.assertCountEqual(data["recipients"], [
{"address": {
"email": "to1@example.com",
"header_to": "Recipient #1 <to1@example.com>, to2@example.com",
}},
{"address": {
"email": "to2@example.com",
"header_to": "Recipient #1 <to1@example.com>, to2@example.com",
}},
# cc and bcc must be explicitly specified as recipients
{"address": {
"email": "cc1@example.com",
"header_to": "Recipient #1 <to1@example.com>, to2@example.com",
}},
{"address": {
"email": "cc2@example.com",
"header_to": "Recipient #1 <to1@example.com>, to2@example.com",
}},
{"address": {
"email": "bcc1@example.com",
"header_to": "Recipient #1 <to1@example.com>, to2@example.com",
}},
{"address": {
"email": "bcc2@example.com",
"header_to": "Recipient #1 <to1@example.com>, to2@example.com",
}},
])
# This also checks recipient generation for cc and bcc. Because it's *not* a
# batch send, each recipient should see a To header reflecting all To addresses.
self.assertCountEqual(
data["recipients"],
[
{
"address": {
"email": "to1@example.com",
"header_to": "Recipient #1 <to1@example.com>, to2@example.com",
}
},
{
"address": {
"email": "to2@example.com",
"header_to": "Recipient #1 <to1@example.com>, to2@example.com",
}
},
# cc and bcc must be explicitly specified as recipients
{
"address": {
"email": "cc1@example.com",
"header_to": "Recipient #1 <to1@example.com>, to2@example.com",
}
},
{
"address": {
"email": "cc2@example.com",
"header_to": "Recipient #1 <to1@example.com>, to2@example.com",
}
},
{
"address": {
"email": "bcc1@example.com",
"header_to": "Recipient #1 <to1@example.com>, to2@example.com",
}
},
{
"address": {
"email": "bcc2@example.com",
"header_to": "Recipient #1 <to1@example.com>, to2@example.com",
}
},
],
)
# Make sure we added a formatted Cc header visible to recipients
# (and not a Bcc header)
self.assertEqual(data["content"]["headers"], {
"Cc": "Carbon Copy <cc1@example.com>, cc2@example.com"
})
self.assertEqual(
data["content"]["headers"],
{"Cc": "Carbon Copy <cc1@example.com>, cc2@example.com"},
)
def test_custom_headers(self):
self.set_mock_result(accepted=6)
email = mail.EmailMessage(
'Subject', 'Body goes here', 'from@example.com', ['to1@example.com'],
cc=['cc1@example.com'],
headers={'Reply-To': 'another@example.com',
'X-MyHeader': 'my value',
'Message-ID': 'mycustommsgid@example.com'})
"Subject",
"Body goes here",
"from@example.com",
["to1@example.com"],
cc=["cc1@example.com"],
headers={
"Reply-To": "another@example.com",
"X-MyHeader": "my value",
"Message-ID": "mycustommsgid@example.com",
},
)
email.send()
data = self.get_api_call_json()
self.assertEqual(data["content"]["headers"], {
# Reply-To moved to separate param (below)
"X-MyHeader": "my value",
"Message-ID": "mycustommsgid@example.com",
"Cc": "cc1@example.com", # Cc header added
})
self.assertEqual(
data["content"]["headers"],
{
# Reply-To moved to separate param (below)
"X-MyHeader": "my value",
"Message-ID": "mycustommsgid@example.com",
"Cc": "cc1@example.com", # Cc header added
},
)
self.assertEqual(data["content"]["reply_to"], "another@example.com")
def test_html_message(self):
text_content = 'This is an important message.'
html_content = '<p>This is an <strong>important</strong> message.</p>'
email = mail.EmailMultiAlternatives('Subject', text_content,
'from@example.com', ['to@example.com'])
text_content = "This is an important message."
html_content = "<p>This is an <strong>important</strong> message.</p>"
email = mail.EmailMultiAlternatives(
"Subject", text_content, "from@example.com", ["to@example.com"]
)
email.attach_alternative(html_content, "text/html")
email.send()
@@ -157,8 +209,10 @@ class SparkPostBackendStandardEmailTests(SparkPostBackendMockAPITestCase):
self.assertNotIn("attachments", data["content"])
def test_html_only_message(self):
html_content = '<p>This is an <strong>important</strong> message.</p>'
email = mail.EmailMessage('Subject', html_content, 'from@example.com', ['to@example.com'])
html_content = "<p>This is an <strong>important</strong> message.</p>"
email = mail.EmailMessage(
"Subject", html_content, "from@example.com", ["to@example.com"]
)
email.content_subtype = "html" # Main content is now text/html
email.send()
@@ -167,19 +221,28 @@ class SparkPostBackendStandardEmailTests(SparkPostBackendMockAPITestCase):
self.assertEqual(data["content"]["html"], html_content)
def test_reply_to(self):
email = mail.EmailMessage('Subject', 'Body goes here', 'from@example.com', ['to1@example.com'],
reply_to=['reply@example.com', 'Other <reply2@example.com>'],
headers={'X-Other': 'Keep'})
email = mail.EmailMessage(
"Subject",
"Body goes here",
"from@example.com",
["to1@example.com"],
reply_to=["reply@example.com", "Other <reply2@example.com>"],
headers={"X-Other": "Keep"},
)
email.send()
data = self.get_api_call_json()
self.assertEqual(data["content"]["reply_to"],
"reply@example.com, Other <reply2@example.com>")
self.assertEqual(data["content"]["headers"], {"X-Other": "Keep"}) # don't lose other headers
self.assertEqual(
data["content"]["reply_to"], "reply@example.com, Other <reply2@example.com>"
)
# don't lose other headers:
self.assertEqual(data["content"]["headers"], {"X-Other": "Keep"})
def test_attachments(self):
text_content = "* Item one\n* Item two\n* Item three"
self.message.attach(filename="test.txt", content=text_content, mimetype="text/plain")
self.message.attach(
filename="test.txt", content=text_content, mimetype="text/plain"
)
# Should guess mimetype if not provided...
png_content = b"PNG\xb4 pretend this is the contents of a png file"
@@ -187,7 +250,7 @@ class SparkPostBackendStandardEmailTests(SparkPostBackendMockAPITestCase):
# Should work with a MIMEBase object (also tests no filename)...
pdf_content = b"PDF\xb4 pretend this is valid pdf params"
mimeattachment = MIMEBase('application', 'pdf')
mimeattachment = MIMEBase("application", "pdf")
mimeattachment.set_payload(pdf_content)
self.message.attach(mimeattachment)
@@ -197,7 +260,9 @@ class SparkPostBackendStandardEmailTests(SparkPostBackendMockAPITestCase):
self.assertEqual(len(attachments), 3)
self.assertEqual(attachments[0]["type"], "text/plain")
self.assertEqual(attachments[0]["name"], "test.txt")
self.assertEqual(decode_att(attachments[0]["data"]).decode("ascii"), text_content)
self.assertEqual(
decode_att(attachments[0]["data"]).decode("ascii"), text_content
)
self.assertEqual(attachments[1]["type"], "image/png") # inferred from filename
self.assertEqual(attachments[1]["name"], "test.png")
self.assertEqual(decode_att(attachments[1]["data"]), png_content)
@@ -210,7 +275,9 @@ class SparkPostBackendStandardEmailTests(SparkPostBackendMockAPITestCase):
def test_unicode_attachment_correctly_decoded(self):
# Slight modification from the Django unicode docs:
# http://django.readthedocs.org/en/latest/ref/unicode.html#email
self.message.attach("Une pièce jointe.html", '<p>\u2019</p>', mimetype='text/html')
self.message.attach(
"Une pièce jointe.html", "<p>\u2019</p>", mimetype="text/html"
)
self.message.send()
data = self.get_api_call_json()
attachments = data["content"]["attachments"]
@@ -223,7 +290,9 @@ class SparkPostBackendStandardEmailTests(SparkPostBackendMockAPITestCase):
data = self.get_api_call_json()
attachment = data["content"]["attachments"][0]
self.assertEqual(attachment["type"], 'text/plain; charset="iso8859-1"')
self.assertEqual(decode_att(attachment["data"]), "Une pièce jointe".encode("iso8859-1"))
self.assertEqual(
decode_att(attachment["data"]), "Une pièce jointe".encode("iso8859-1")
)
def test_embedded_images(self):
image_filename = SAMPLE_IMAGE_FILENAME
@@ -231,7 +300,9 @@ class SparkPostBackendStandardEmailTests(SparkPostBackendMockAPITestCase):
image_data = sample_image_content(image_filename)
cid = attach_inline_image_file(self.message, image_path)
html_content = '<p>This has an <img src="cid:%s" alt="inline" /> image.</p>' % cid
html_content = (
'<p>This has an <img src="cid:%s" alt="inline" /> image.</p>' % cid
)
self.message.attach_alternative(html_content, "text/html")
self.message.send()
@@ -241,7 +312,9 @@ class SparkPostBackendStandardEmailTests(SparkPostBackendMockAPITestCase):
self.assertEqual(len(data["content"]["inline_images"]), 1)
self.assertEqual(data["content"]["inline_images"][0]["type"], "image/png")
self.assertEqual(data["content"]["inline_images"][0]["name"], cid)
self.assertEqual(decode_att(data["content"]["inline_images"][0]["data"]), image_data)
self.assertEqual(
decode_att(data["content"]["inline_images"][0]["data"]), image_data
)
# Make sure neither the html nor the inline image is treated as an attachment:
self.assertNotIn("attachments", data["content"])
@@ -250,9 +323,11 @@ class SparkPostBackendStandardEmailTests(SparkPostBackendMockAPITestCase):
image_path = sample_image_path(image_filename)
image_data = sample_image_content(image_filename)
self.message.attach_file(image_path) # option 1: attach as a file
# option 1: attach as a file
self.message.attach_file(image_path)
image = MIMEImage(image_data) # option 2: construct the MIMEImage and attach it directly
# option 2: construct the MIMEImage and attach it directly
image = MIMEImage(image_data)
self.message.attach(image)
self.message.send()
@@ -305,18 +380,23 @@ class SparkPostBackendStandardEmailTests(SparkPostBackendMockAPITestCase):
self.assertNotIn("reply_to", data["content"])
def test_empty_to(self):
# Test empty `to` -- but send requires at least one recipient somewhere (like cc)
# Test empty `to`--but send requires at least one recipient somewhere (like cc)
self.message.to = []
self.message.cc = ["cc@example.com"]
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data["recipients"], [{
"address": {
"email": "cc@example.com",
# This results in a message with an empty To header, as desired:
"header_to": "",
},
}])
self.assertEqual(
data["recipients"],
[
{
"address": {
"email": "cc@example.com",
# This results in a message with an empty To header, as desired:
"header_to": "",
},
}
],
)
def test_api_failure(self):
self.set_mock_response(status_code=400)
@@ -337,11 +417,13 @@ class SparkPostBackendStandardEmailTests(SparkPostBackendMockAPITestCase):
}]
}"""
self.set_mock_response(status_code=400, raw=failure_response)
with self.assertRaisesMessage(AnymailAPIError, "Helpful explanation from your ESP"):
with self.assertRaisesMessage(
AnymailAPIError, "Helpful explanation from your ESP"
):
self.message.send()
@tag('sparkpost')
@tag("sparkpost")
class SparkPostBackendAnymailFeatureTests(SparkPostBackendMockAPITestCase):
"""Test backend support for Anymail added features"""
@@ -352,10 +434,10 @@ class SparkPostBackendAnymailFeatureTests(SparkPostBackendMockAPITestCase):
self.assertEqual(data["return_path"], "bounce-handler@bounces.example.com")
def test_metadata(self):
self.message.metadata = {'user_id': "12345", 'items': 'spark, post'}
self.message.metadata = {"user_id": "12345", "items": "spark, post"}
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data["metadata"], {'user_id': "12345", 'items': 'spark, post'})
self.assertEqual(data["metadata"], {"user_id": "12345", "items": "spark, post"})
def test_send_at(self):
utc_plus_6 = get_fixed_timezone(6 * 60)
@@ -407,7 +489,7 @@ class SparkPostBackendAnymailFeatureTests(SparkPostBackendMockAPITestCase):
self.assertEqual(data["campaign_id"], "receipt")
self.message.tags = ["receipt", "repeat-user"]
with self.assertRaisesMessage(AnymailUnsupportedFeature, 'multiple tags'):
with self.assertRaisesMessage(AnymailUnsupportedFeature, "multiple tags"):
self.message.send()
def test_tracking(self):
@@ -428,7 +510,9 @@ class SparkPostBackendAnymailFeatureTests(SparkPostBackendMockAPITestCase):
self.assertEqual(data["options"]["click_tracking"], True)
def test_template_id(self):
message = mail.EmailMultiAlternatives(from_email='from@example.com', to=['to@example.com'])
message = mail.EmailMultiAlternatives(
from_email="from@example.com", to=["to@example.com"]
)
message.template_id = "welcome_template"
message.send()
data = self.get_api_call_json()
@@ -440,51 +524,85 @@ class SparkPostBackendAnymailFeatureTests(SparkPostBackendMockAPITestCase):
def test_merge_data(self):
self.set_mock_result(accepted=4) # two 'to' plus one 'cc' for each 'to'
self.message.to = ['alice@example.com', 'Bob <bob@example.com>']
self.message.cc = ['cc@example.com']
self.message.to = ["alice@example.com", "Bob <bob@example.com>"]
self.message.cc = ["cc@example.com"]
self.message.body = "Hi {{address.name}}. Welcome to {{group}} at {{site}}."
self.message.merge_data = {
'alice@example.com': {'name': "Alice", 'group': "Developers"},
'bob@example.com': {'name': "Bob"}, # and leave group undefined
'nobody@example.com': {'name': "Not a recipient for this message"},
"alice@example.com": {"name": "Alice", "group": "Developers"},
"bob@example.com": {"name": "Bob"}, # and leave group undefined
"nobody@example.com": {"name": "Not a recipient for this message"},
}
self.message.merge_global_data = {'group': "Users", 'site': "ExampleCo"}
self.message.merge_global_data = {"group": "Users", "site": "ExampleCo"}
self.message.send()
data = self.get_api_call_json()
self.assertEqual({"group": "Users", "site": "ExampleCo"}, data["substitution_data"])
self.assertEqual([{
"address": {"email": "alice@example.com", "header_to": "alice@example.com"},
"substitution_data": {"name": "Alice", "group": "Developers"},
}, {
"address": {"email": "bob@example.com", "header_to": "Bob <bob@example.com>"},
"substitution_data": {"name": "Bob"},
}, { # duplicated for cc recipients...
"address": {"email": "cc@example.com", "header_to": "alice@example.com"},
"substitution_data": {"name": "Alice", "group": "Developers"},
}, {
"address": {"email": "cc@example.com", "header_to": "Bob <bob@example.com>"},
"substitution_data": {"name": "Bob"},
}], data["recipients"])
self.assertEqual(
{"group": "Users", "site": "ExampleCo"}, data["substitution_data"]
)
self.assertEqual(
[
{
"address": {
"email": "alice@example.com",
"header_to": "alice@example.com",
},
"substitution_data": {"name": "Alice", "group": "Developers"},
},
{
"address": {
"email": "bob@example.com",
"header_to": "Bob <bob@example.com>",
},
"substitution_data": {"name": "Bob"},
},
{ # duplicated for cc recipients...
"address": {
"email": "cc@example.com",
"header_to": "alice@example.com",
},
"substitution_data": {"name": "Alice", "group": "Developers"},
},
{
"address": {
"email": "cc@example.com",
"header_to": "Bob <bob@example.com>",
},
"substitution_data": {"name": "Bob"},
},
],
data["recipients"],
)
def test_merge_metadata(self):
self.set_mock_result(accepted=2)
self.message.to = ['alice@example.com', 'Bob <bob@example.com>']
self.message.to = ["alice@example.com", "Bob <bob@example.com>"]
self.message.merge_metadata = {
'alice@example.com': {'order_id': 123},
'bob@example.com': {'order_id': 678, 'tier': 'premium'},
"alice@example.com": {"order_id": 123},
"bob@example.com": {"order_id": 678, "tier": "premium"},
}
self.message.metadata = {'notification_batch': 'zx912'}
self.message.metadata = {"notification_batch": "zx912"}
self.message.send()
data = self.get_api_call_json()
self.assertEqual([{
"address": {"email": "alice@example.com", "header_to": "alice@example.com"},
"metadata": {"order_id": 123},
}, {
"address": {"email": "bob@example.com", "header_to": "Bob <bob@example.com>"},
"metadata": {"order_id": 678, "tier": "premium"}
}], data["recipients"])
self.assertEqual(
[
{
"address": {
"email": "alice@example.com",
"header_to": "alice@example.com",
},
"metadata": {"order_id": 123},
},
{
"address": {
"email": "bob@example.com",
"header_to": "Bob <bob@example.com>",
},
"metadata": {"order_id": 678, "tier": "premium"},
},
],
data["recipients"],
)
self.assertEqual(data["metadata"], {"notification_batch": "zx912"})
def test_default_omits_options(self):
@@ -498,7 +616,8 @@ class SparkPostBackendAnymailFeatureTests(SparkPostBackendMockAPITestCase):
data = self.get_api_call_json()
self.assertNotIn("campaign_id", data)
self.assertNotIn("metadata", data)
self.assertNotIn("options", data) # covers start_time, click_tracking, open_tracking
# covers start_time, click_tracking, open_tracking:
self.assertNotIn("options", data)
self.assertNotIn("substitution_data", data)
self.assertNotIn("template_id", data["content"])
@@ -517,64 +636,108 @@ class SparkPostBackendAnymailFeatureTests(SparkPostBackendMockAPITestCase):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data["description"], "The description")
self.assertEqual(data["options"], {
"transactional": True,
"click_tracking": True, # deep merge
})
self.assertDictMatches({
"use_draft_template": True,
"ab_test_id": "highlight_support_links",
"text": "Text Body", # deep merge
"subject": "Subject", # deep merge
}, data["content"])
self.assertEqual(
data["options"],
{
"transactional": True,
"click_tracking": True, # deep merge
},
)
self.assertDictMatches(
{
"use_draft_template": True,
"ab_test_id": "highlight_support_links",
"text": "Text Body", # deep merge
"subject": "Subject", # deep merge
},
data["content"],
)
def test_send_attaches_anymail_status(self):
"""The anymail_status should be attached to the message when it is sent """
"""The anymail_status should be attached to the message when it is sent"""
response_content = self.set_mock_result(accepted=1, rejected=0, id="9876543210")
msg = mail.EmailMessage('Subject', 'Message', 'from@example.com', ['to1@example.com'],)
msg = mail.EmailMessage(
"Subject",
"Message",
"from@example.com",
["to1@example.com"],
)
sent = msg.send()
self.assertEqual(sent, 1)
self.assertEqual(msg.anymail_status.status, {'queued'})
self.assertEqual(msg.anymail_status.message_id, '9876543210')
self.assertEqual(msg.anymail_status.recipients['to1@example.com'].status, 'queued')
self.assertEqual(msg.anymail_status.recipients['to1@example.com'].message_id, '9876543210')
self.assertEqual(msg.anymail_status.status, {"queued"})
self.assertEqual(msg.anymail_status.message_id, "9876543210")
self.assertEqual(
msg.anymail_status.recipients["to1@example.com"].status, "queued"
)
self.assertEqual(
msg.anymail_status.recipients["to1@example.com"].message_id, "9876543210"
)
self.assertEqual(msg.anymail_status.esp_response.content, response_content)
@override_settings(ANYMAIL_IGNORE_RECIPIENT_STATUS=True) # exception is tested later
@override_settings(
ANYMAIL_IGNORE_RECIPIENT_STATUS=True # exception is tested later
)
def test_send_all_rejected(self):
"""The anymail_status should be 'rejected' when all recipients rejected"""
self.set_mock_result(accepted=0, rejected=2)
msg = mail.EmailMessage('Subject', 'Message', 'from@example.com',
['to1@example.com', 'to2@example.com'],)
msg = mail.EmailMessage(
"Subject",
"Message",
"from@example.com",
["to1@example.com", "to2@example.com"],
)
msg.send()
self.assertEqual(msg.anymail_status.status, {'rejected'})
self.assertEqual(msg.anymail_status.recipients['to1@example.com'].status, 'rejected')
self.assertEqual(msg.anymail_status.recipients['to2@example.com'].status, 'rejected')
self.assertEqual(msg.anymail_status.status, {"rejected"})
self.assertEqual(
msg.anymail_status.recipients["to1@example.com"].status, "rejected"
)
self.assertEqual(
msg.anymail_status.recipients["to2@example.com"].status, "rejected"
)
def test_send_some_rejected(self):
"""The anymail_status should be 'unknown' when some recipients accepted and some rejected"""
"""
The anymail_status should be 'unknown'
when some recipients accepted and some rejected
"""
self.set_mock_result(accepted=1, rejected=1)
msg = mail.EmailMessage('Subject', 'Message', 'from@example.com',
['to1@example.com', 'to2@example.com'],)
msg = mail.EmailMessage(
"Subject",
"Message",
"from@example.com",
["to1@example.com", "to2@example.com"],
)
msg.send()
self.assertEqual(msg.anymail_status.status, {'unknown'})
self.assertEqual(msg.anymail_status.recipients['to1@example.com'].status, 'unknown')
self.assertEqual(msg.anymail_status.recipients['to2@example.com'].status, 'unknown')
self.assertEqual(msg.anymail_status.status, {"unknown"})
self.assertEqual(
msg.anymail_status.recipients["to1@example.com"].status, "unknown"
)
self.assertEqual(
msg.anymail_status.recipients["to2@example.com"].status, "unknown"
)
def test_send_unexpected_count(self):
"""The anymail_status should be 'unknown' when the total result count
doesn't match the number of recipients"""
doesn't match the number of recipients"""
self.set_mock_result(accepted=3, rejected=0) # but only 2 in the to-list
msg = mail.EmailMessage('Subject', 'Message', 'from@example.com',
['to1@example.com', 'to2@example.com'],)
msg = mail.EmailMessage(
"Subject",
"Message",
"from@example.com",
["to1@example.com", "to2@example.com"],
)
msg.send()
self.assertEqual(msg.anymail_status.status, {'unknown'})
self.assertEqual(msg.anymail_status.recipients['to1@example.com'].status, 'unknown')
self.assertEqual(msg.anymail_status.recipients['to2@example.com'].status, 'unknown')
self.assertEqual(msg.anymail_status.status, {"unknown"})
self.assertEqual(
msg.anymail_status.recipients["to1@example.com"].status, "unknown"
)
self.assertEqual(
msg.anymail_status.recipients["to2@example.com"].status, "unknown"
)
# noinspection PyUnresolvedReferences
def test_send_failed_anymail_status(self):
""" If the send fails, anymail_status should contain initial values"""
"""If the send fails, anymail_status should contain initial values"""
self.set_mock_response(status_code=400)
sent = self.message.send(fail_silently=True)
self.assertEqual(sent, 0)
@@ -585,7 +748,10 @@ class SparkPostBackendAnymailFeatureTests(SparkPostBackendMockAPITestCase):
# noinspection PyUnresolvedReferences
def test_send_unparsable_response(self):
"""If the send succeeds, but result is unexpected format, should raise an API exception"""
"""
If the send succeeds, but result is unexpected format,
should raise an API exception
"""
response_content = b"""{"wrong": "format"}"""
self.set_mock_response(raw=response_content)
with self.assertRaises(AnymailAPIError):
@@ -593,99 +759,142 @@ class SparkPostBackendAnymailFeatureTests(SparkPostBackendMockAPITestCase):
self.assertIsNone(self.message.anymail_status.status)
self.assertIsNone(self.message.anymail_status.message_id)
self.assertEqual(self.message.anymail_status.recipients, {})
self.assertEqual(self.message.anymail_status.esp_response.content, response_content)
self.assertEqual(
self.message.anymail_status.esp_response.content, response_content
)
def test_json_serialization_errors(self):
"""Try to provide more information about non-json-serializable data"""
self.message.tags = [Decimal('19.99')] # yeah, don't do this
self.message.tags = [Decimal("19.99")] # yeah, don't do this
with self.assertRaises(AnymailSerializationError) as cm:
self.message.send()
print(self.get_api_call_json())
err = cm.exception
self.assertIsInstance(err, TypeError) # compatibility with json.dumps
self.assertIn("Don't know how to send this data to SparkPost", str(err)) # our added context
self.assertRegex(str(err), r"Decimal.*is not JSON serializable") # original message
# our added context:
self.assertIn("Don't know how to send this data to SparkPost", str(err))
# original message:
self.assertRegex(str(err), r"Decimal.*is not JSON serializable")
@tag('sparkpost')
@tag("sparkpost")
class SparkPostBackendRecipientsRefusedTests(SparkPostBackendMockAPITestCase):
"""Should raise AnymailRecipientsRefused when *all* recipients are rejected or invalid"""
"""
Should raise AnymailRecipientsRefused when *all* recipients are rejected or invalid
"""
def test_recipients_refused(self):
self.set_mock_result(accepted=0, rejected=2)
msg = mail.EmailMessage('Subject', 'Body', 'from@example.com',
['invalid@localhost', 'reject@example.com'])
msg = mail.EmailMessage(
"Subject",
"Body",
"from@example.com",
["invalid@localhost", "reject@example.com"],
)
with self.assertRaises(AnymailRecipientsRefused):
msg.send()
def test_fail_silently(self):
self.set_mock_result(accepted=0, rejected=2)
sent = mail.send_mail('Subject', 'Body', 'from@example.com',
['invalid@localhost', 'reject@example.com'],
fail_silently=True)
sent = mail.send_mail(
"Subject",
"Body",
"from@example.com",
["invalid@localhost", "reject@example.com"],
fail_silently=True,
)
self.assertEqual(sent, 0)
def test_mixed_response(self):
"""If *any* recipients are valid or queued, no exception is raised"""
self.set_mock_result(accepted=2, rejected=2)
msg = mail.EmailMessage('Subject', 'Body', 'from@example.com',
['invalid@localhost', 'valid@example.com',
'reject@example.com', 'also.valid@example.com'])
msg = mail.EmailMessage(
"Subject",
"Body",
"from@example.com",
[
"invalid@localhost",
"valid@example.com",
"reject@example.com",
"also.valid@example.com",
],
)
sent = msg.send()
self.assertEqual(sent, 1) # one message sent, successfully, to 2 of 4 recipients
# one message sent, successfully, to 2 of 4 recipients:
self.assertEqual(sent, 1)
status = msg.anymail_status
# We don't know which recipients were rejected
self.assertEqual(status.recipients['invalid@localhost'].status, 'unknown')
self.assertEqual(status.recipients['valid@example.com'].status, 'unknown')
self.assertEqual(status.recipients['reject@example.com'].status, 'unknown')
self.assertEqual(status.recipients['also.valid@example.com'].status, 'unknown')
self.assertEqual(status.recipients["invalid@localhost"].status, "unknown")
self.assertEqual(status.recipients["valid@example.com"].status, "unknown")
self.assertEqual(status.recipients["reject@example.com"].status, "unknown")
self.assertEqual(status.recipients["also.valid@example.com"].status, "unknown")
@override_settings(ANYMAIL_IGNORE_RECIPIENT_STATUS=True)
def test_settings_override(self):
"""No exception with ignore setting"""
self.set_mock_result(accepted=0, rejected=2)
sent = mail.send_mail('Subject', 'Body', 'from@example.com',
['invalid@localhost', 'reject@example.com'])
sent = mail.send_mail(
"Subject",
"Body",
"from@example.com",
["invalid@localhost", "reject@example.com"],
)
self.assertEqual(sent, 1) # refused message is included in sent count
@tag('sparkpost')
@tag("sparkpost")
class SparkPostBackendConfigurationTests(SparkPostBackendMockAPITestCase):
"""Test various SparkPost client options"""
@override_settings(ANYMAIL={}) # clear SPARKPOST_API_KEY from SparkPostBackendMockAPITestCase
@override_settings(
# clear SPARKPOST_API_KEY from SparkPostBackendMockAPITestCase:
ANYMAIL={}
)
def test_missing_api_key(self):
with self.assertRaises(AnymailConfigurationError) as cm:
mail.send_mail('Subject', 'Message', 'from@example.com', ['to@example.com'])
mail.send_mail("Subject", "Message", "from@example.com", ["to@example.com"])
errmsg = str(cm.exception)
# Make sure the error mentions the different places to set the key
self.assertRegex(errmsg, r'\bSPARKPOST_API_KEY\b')
self.assertRegex(errmsg, r'\bANYMAIL_SPARKPOST_API_KEY\b')
self.assertRegex(errmsg, r"\bSPARKPOST_API_KEY\b")
self.assertRegex(errmsg, r"\bANYMAIL_SPARKPOST_API_KEY\b")
@override_settings(ANYMAIL={
"SPARKPOST_API_URL": "https://api.eu.sparkpost.com/api/v1",
"SPARKPOST_API_KEY": "test_api_key",
})
@override_settings(
ANYMAIL={
"SPARKPOST_API_URL": "https://api.eu.sparkpost.com/api/v1",
"SPARKPOST_API_KEY": "test_api_key",
}
)
def test_sparkpost_api_url(self):
mail.send_mail('Subject', 'Message', 'from@example.com', ['to@example.com'])
mail.send_mail("Subject", "Message", "from@example.com", ["to@example.com"])
self.assert_esp_called("https://api.eu.sparkpost.com/api/v1/transmissions/")
# can also override on individual connection (and even use non-versioned labs endpoint)
# can also override on individual connection
# (and even use non-versioned labs endpoint)
connection = mail.get_connection(api_url="https://api.sparkpost.com/api/labs")
mail.send_mail('Subject', 'Message', 'from@example.com', ['to@example.com'],
connection=connection)
mail.send_mail(
"Subject",
"Message",
"from@example.com",
["to@example.com"],
connection=connection,
)
self.assert_esp_called("https://api.sparkpost.com/api/labs/transmissions/")
def test_subaccount(self):
# A likely use case is supplying subaccount for a particular message.
# (For all messages, just set SPARKPOST_SUBACCOUNT in ANYMAIL settings.)
connection = mail.get_connection(subaccount=123)
mail.send_mail('Subject', 'Message', 'from@example.com', ['to@example.com'],
connection=connection)
mail.send_mail(
"Subject",
"Message",
"from@example.com",
["to@example.com"],
connection=connection,
)
headers = self.get_api_call_headers()
self.assertEqual(headers["X-MSYS-SUBACCOUNT"], 123)
# Make sure we're not setting the header on non-subaccount sends
mail.send_mail('Subject', 'Message', 'from@example.com', ['to@example.com'])
mail.send_mail("Subject", "Message", "from@example.com", ["to@example.com"])
headers = self.get_api_call_headers()
self.assertNotIn("X-MSYS-SUBACCOUNT", headers)

View File

@@ -9,21 +9,23 @@ from anymail.inbound import AnymailInboundMessage
from anymail.signals import AnymailInboundEvent
from anymail.webhooks.sparkpost import SparkPostInboundWebhookView
from .utils import sample_image_content, sample_email_content
from .utils import sample_email_content, sample_image_content
from .webhook_cases import WebhookTestCase
@tag('sparkpost')
@tag("sparkpost")
class SparkpostInboundTestCase(WebhookTestCase):
def test_inbound_basics(self):
event = {
'protocol': "smtp",
'rcpt_to': "test@inbound.example.com",
'msg_from': "envelope-from@example.org",
'content': {
# Anymail just parses the raw rfc822 email. SparkPost's other content fields are ignored.
'email_rfc822_is_base64': False,
'email_rfc822': dedent("""\
"protocol": "smtp",
"rcpt_to": "test@inbound.example.com",
"msg_from": "envelope-from@example.org",
"content": {
# Anymail just parses the raw rfc822 email.
# SparkPost's other content fields are ignored.
"email_rfc822_is_base64": False,
"email_rfc822": dedent(
"""\
Received: from mail.example.org by c.mta1vsmtp.cc.prd.sparkpost ...
Received: by mail.example.org for <test@inbound.example.com> ...
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.org; ...
@@ -50,20 +52,28 @@ class SparkpostInboundTestCase(WebhookTestCase):
<div dir=3D"ltr">It's a body=E2=80=A6</div>
--94eb2c05e174adb140055b6339c5--
"""),
""" # NOQA: E501
),
},
}
raw_event = {'msys': {'relay_message': event}}
raw_event = {"msys": {"relay_message": event}}
response = self.client.post('/anymail/sparkpost/inbound/',
content_type='application/json', data=json.dumps([raw_event]))
response = self.client.post(
"/anymail/sparkpost/inbound/",
content_type="application/json",
data=json.dumps([raw_event]),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.inbound_handler, sender=SparkPostInboundWebhookView,
event=ANY, esp_name='SparkPost')
kwargs = self.assert_handler_called_once_with(
self.inbound_handler,
sender=SparkPostInboundWebhookView,
event=ANY,
esp_name="SparkPost",
)
# AnymailInboundEvent
event = kwargs['event']
event = kwargs["event"]
self.assertIsInstance(event, AnymailInboundEvent)
self.assertEqual(event.event_type, 'inbound')
self.assertEqual(event.event_type, "inbound")
self.assertIsNone(event.timestamp)
self.assertIsNone(event.event_id)
self.assertIsInstance(event.message, AnymailInboundMessage)
@@ -72,36 +82,44 @@ class SparkpostInboundTestCase(WebhookTestCase):
# AnymailInboundMessage - convenience properties
message = event.message
self.assertEqual(message.from_email.display_name, 'Displayed From')
self.assertEqual(message.from_email.addr_spec, 'from+test@example.org')
self.assertEqual([str(e) for e in message.to],
['Test Inbound <test@inbound.example.com>', 'other@example.com'])
self.assertEqual([str(e) for e in message.cc],
['cc@example.com'])
self.assertEqual(message.subject, 'Test subject')
self.assertEqual(message.from_email.display_name, "Displayed From")
self.assertEqual(message.from_email.addr_spec, "from+test@example.org")
self.assertEqual(
[str(e) for e in message.to],
["Test Inbound <test@inbound.example.com>", "other@example.com"],
)
self.assertEqual([str(e) for e in message.cc], ["cc@example.com"])
self.assertEqual(message.subject, "Test subject")
self.assertEqual(message.date.isoformat(" "), "2017-10-11 18:31:04-07:00")
self.assertEqual(message.text, "It's a body\N{HORIZONTAL ELLIPSIS}\n")
self.assertEqual(message.html, """<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\n""")
self.assertEqual(
message.html,
"""<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\n""",
)
self.assertEqual(message.envelope_sender, 'envelope-from@example.org')
self.assertEqual(message.envelope_recipient, 'test@inbound.example.com')
self.assertEqual(message.envelope_sender, "envelope-from@example.org")
self.assertEqual(message.envelope_recipient, "test@inbound.example.com")
self.assertIsNone(message.stripped_text)
self.assertIsNone(message.stripped_html)
self.assertIsNone(message.spam_detected)
self.assertIsNone(message.spam_score)
# AnymailInboundMessage - other headers
self.assertEqual(message['Message-ID'], "<CAEPk3R+4Zr@mail.example.org>")
self.assertEqual(message.get_all('Received'), [
"from mail.example.org by c.mta1vsmtp.cc.prd.sparkpost ...",
"by mail.example.org for <test@inbound.example.com> ...",
"by 10.10.1.71 with HTTP; Wed, 11 Oct 2017 18:31:04 -0700 (PDT)",
])
self.assertEqual(message["Message-ID"], "<CAEPk3R+4Zr@mail.example.org>")
self.assertEqual(
message.get_all("Received"),
[
"from mail.example.org by c.mta1vsmtp.cc.prd.sparkpost ...",
"by mail.example.org for <test@inbound.example.com> ...",
"by 10.10.1.71 with HTTP; Wed, 11 Oct 2017 18:31:04 -0700 (PDT)",
],
)
def test_attachments(self):
image_content = sample_image_content()
email_content = sample_email_content()
raw_mime = dedent("""\
raw_mime = dedent(
"""\
MIME-Version: 1.0
From: from@example.org
Subject: Attachments
@@ -136,42 +154,60 @@ class SparkpostInboundTestCase(WebhookTestCase):
{email_content}
--boundary0--
""").format(image_content_base64=b64encode(image_content).decode('ascii'),
email_content=email_content.decode('ascii'))
""" # NOQA: E501
).format(
image_content_base64=b64encode(image_content).decode("ascii"),
email_content=email_content.decode("ascii"),
)
raw_event = {'msys': {'relay_message': {
'protocol': "smtp",
'content': {
'email_rfc822_is_base64': True,
'email_rfc822': b64encode(raw_mime.encode('utf-8')).decode('ascii'),
},
}}}
raw_event = {
"msys": {
"relay_message": {
"protocol": "smtp",
"content": {
"email_rfc822_is_base64": True,
"email_rfc822": b64encode(raw_mime.encode("utf-8")).decode(
"ascii"
),
},
}
}
}
response = self.client.post('/anymail/sparkpost/inbound/',
content_type='application/json', data=json.dumps([raw_event]))
response = self.client.post(
"/anymail/sparkpost/inbound/",
content_type="application/json",
data=json.dumps([raw_event]),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.inbound_handler, sender=SparkPostInboundWebhookView,
event=ANY, esp_name='SparkPost')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.inbound_handler,
sender=SparkPostInboundWebhookView,
event=ANY,
esp_name="SparkPost",
)
event = kwargs["event"]
message = event.message
attachments = message.attachments # AnymailInboundMessage convenience accessor
self.assertEqual(len(attachments), 2)
self.assertEqual(attachments[0].get_filename(), 'test.txt')
self.assertEqual(attachments[0].get_content_type(), 'text/plain')
self.assertEqual(attachments[0].get_content_text(), 'test attachment')
self.assertEqual(attachments[1].get_content_type(), 'message/rfc822')
self.assertEqualIgnoringHeaderFolding(attachments[1].get_content_bytes(), email_content)
self.assertEqual(attachments[0].get_filename(), "test.txt")
self.assertEqual(attachments[0].get_content_type(), "text/plain")
self.assertEqual(attachments[0].get_content_text(), "test attachment")
self.assertEqual(attachments[1].get_content_type(), "message/rfc822")
self.assertEqualIgnoringHeaderFolding(
attachments[1].get_content_bytes(), email_content
)
# the message attachment (its payload) is fully parsed
# (see the original in test_files/sample_email.txt)
att_message = attachments[1].get_payload(0)
self.assertEqual(att_message.get_content_type(), "multipart/alternative")
self.assertEqual(att_message['Subject'], "Test email")
self.assertEqual(att_message["Subject"], "Test email")
self.assertEqual(att_message.text, "Hi Bob, This is a message. Thanks!\n")
inlines = message.inline_attachments
self.assertEqual(len(inlines), 1)
inline = inlines['abc123']
self.assertEqual(inline.get_filename(), 'image.png')
self.assertEqual(inline.get_content_type(), 'image/png')
inline = inlines["abc123"]
self.assertEqual(inline.get_filename(), "image.png")
self.assertEqual(inline.get_content_type(), "image/png")
self.assertEqual(inline.get_content_bytes(), image_content)

View File

@@ -10,16 +10,20 @@ from anymail.message import AnymailMessage
from .utils import AnymailTestMixin, sample_image_path
ANYMAIL_TEST_SPARKPOST_API_KEY = os.getenv('ANYMAIL_TEST_SPARKPOST_API_KEY')
ANYMAIL_TEST_SPARKPOST_DOMAIN = os.getenv('ANYMAIL_TEST_SPARKPOST_DOMAIN')
ANYMAIL_TEST_SPARKPOST_API_KEY = os.getenv("ANYMAIL_TEST_SPARKPOST_API_KEY")
ANYMAIL_TEST_SPARKPOST_DOMAIN = os.getenv("ANYMAIL_TEST_SPARKPOST_DOMAIN")
@tag('sparkpost', 'live')
@unittest.skipUnless(ANYMAIL_TEST_SPARKPOST_API_KEY and ANYMAIL_TEST_SPARKPOST_DOMAIN,
"Set ANYMAIL_TEST_SPARKPOST_API_KEY and ANYMAIL_TEST_SPARKPOST_DOMAIN "
"environment variables to run SparkPost integration tests")
@override_settings(ANYMAIL_SPARKPOST_API_KEY=ANYMAIL_TEST_SPARKPOST_API_KEY,
EMAIL_BACKEND="anymail.backends.sparkpost.EmailBackend")
@tag("sparkpost", "live")
@unittest.skipUnless(
ANYMAIL_TEST_SPARKPOST_API_KEY and ANYMAIL_TEST_SPARKPOST_DOMAIN,
"Set ANYMAIL_TEST_SPARKPOST_API_KEY and ANYMAIL_TEST_SPARKPOST_DOMAIN "
"environment variables to run SparkPost integration tests",
)
@override_settings(
ANYMAIL_SPARKPOST_API_KEY=ANYMAIL_TEST_SPARKPOST_API_KEY,
EMAIL_BACKEND="anymail.backends.sparkpost.EmailBackend",
)
class SparkPostBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
"""SparkPost API integration tests
@@ -35,23 +39,32 @@ class SparkPostBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
def setUp(self):
super().setUp()
self.from_email = 'test@%s' % ANYMAIL_TEST_SPARKPOST_DOMAIN
self.message = AnymailMessage('Anymail SparkPost integration test', 'Text content',
self.from_email, ['to@test.sink.sparkpostmail.com'])
self.message.attach_alternative('<p>HTML content</p>', "text/html")
self.from_email = "test@%s" % ANYMAIL_TEST_SPARKPOST_DOMAIN
self.message = AnymailMessage(
"Anymail SparkPost integration test",
"Text content",
self.from_email,
["to@test.sink.sparkpostmail.com"],
)
self.message.attach_alternative("<p>HTML content</p>", "text/html")
def test_simple_send(self):
# Example of getting the SparkPost send status and transmission id from the message
# Example of getting the SparkPost send status
# and transmission id from the message
sent_count = self.message.send()
self.assertEqual(sent_count, 1)
anymail_status = self.message.anymail_status
sent_status = anymail_status.recipients['to@test.sink.sparkpostmail.com'].status
message_id = anymail_status.recipients['to@test.sink.sparkpostmail.com'].message_id
sent_status = anymail_status.recipients["to@test.sink.sparkpostmail.com"].status
message_id = anymail_status.recipients[
"to@test.sink.sparkpostmail.com"
].message_id
self.assertEqual(sent_status, 'queued') # SparkPost always queues
self.assertRegex(message_id, r'.+') # this is actually the transmission_id; should be non-blank
self.assertEqual(anymail_status.status, {sent_status}) # set of all recipient statuses
self.assertEqual(sent_status, "queued") # SparkPost always queues
# this is actually the transmission_id; should be non-blank:
self.assertRegex(message_id, r".+")
# set of all recipient statuses:
self.assertEqual(anymail_status.status, {sent_status})
self.assertEqual(anymail_status.message_id, message_id)
def test_all_options(self):
@@ -60,14 +73,15 @@ class SparkPostBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
subject="Anymail all-options integration test",
body="This is the text body",
from_email=formataddr(("Test From, with comma", self.from_email)),
to=["to1@test.sink.sparkpostmail.com", "Recipient 2 <to2@test.sink.sparkpostmail.com>"],
# Limit the live b/cc's to avoid running through our small monthly allowance:
# cc=["cc1@test.sink.sparkpostmail.com", "Copy 2 <cc2@test.sink.sparkpostmail.com>"],
# bcc=["bcc1@test.sink.sparkpostmail.com", "Blind Copy 2 <bcc2@test.sink.sparkpostmail.com>"],
to=[
"to1@test.sink.sparkpostmail.com",
"Recipient 2 <to2@test.sink.sparkpostmail.com>",
],
# Limit the live b/cc's to avoid running through our small monthly
# allowance:
cc=["Copy To <cc@test.sink.sparkpostmail.com>"],
reply_to=["reply1@example.com", "Reply 2 <reply2@example.com>"],
headers={"X-Anymail-Test": "value"},
metadata={"meta1": "simple string", "meta2": 2},
send_at=send_at,
tags=["tag 1"], # SparkPost only supports single tags
@@ -80,47 +94,57 @@ class SparkPostBackendIntegrationTests(AnymailTestMixin, SimpleTestCase):
message.attach_alternative(
"<p><b>HTML:</b> with <a href='http://example.com'>link</a>"
"and image: <img src='cid:%s'></div>" % cid,
"text/html")
"text/html",
)
message.send()
self.assertEqual(message.anymail_status.status, {'queued'}) # SparkPost always queues
# SparkPost always queues:
self.assertEqual(message.anymail_status.status, {"queued"})
def test_merge_data(self):
message = AnymailMessage(
subject="Anymail merge_data test: {{ value }}",
body="This body includes merge data: {{ value }}\n"
"And global merge data: {{ global }}",
"And global merge data: {{ global }}",
from_email=formataddr(("Test From", self.from_email)),
to=["to1@test.sink.sparkpostmail.com", "Recipient 2 <to2@test.sink.sparkpostmail.com>"],
to=[
"to1@test.sink.sparkpostmail.com",
"Recipient 2 <to2@test.sink.sparkpostmail.com>",
],
merge_data={
'to1@test.sink.sparkpostmail.com': {'value': 'one'},
'to2@test.sink.sparkpostmail.com': {'value': 'two'},
},
merge_global_data={
'global': 'global_value'
"to1@test.sink.sparkpostmail.com": {"value": "one"},
"to2@test.sink.sparkpostmail.com": {"value": "two"},
},
merge_global_data={"global": "global_value"},
)
message.send()
recipient_status = message.anymail_status.recipients
self.assertEqual(recipient_status['to1@test.sink.sparkpostmail.com'].status, 'queued')
self.assertEqual(recipient_status['to2@test.sink.sparkpostmail.com'].status, 'queued')
self.assertEqual(
recipient_status["to1@test.sink.sparkpostmail.com"].status, "queued"
)
self.assertEqual(
recipient_status["to2@test.sink.sparkpostmail.com"].status, "queued"
)
def test_stored_template(self):
message = AnymailMessage(
template_id='test-template', # a real template in our SparkPost test account
# a real template in our SparkPost test account:
template_id="test-template",
to=["to1@test.sink.sparkpostmail.com"],
merge_data={
'to1@test.sink.sparkpostmail.com': {
'name': "Test Recipient",
"to1@test.sink.sparkpostmail.com": {
"name": "Test Recipient",
}
},
merge_global_data={
'order': '12345',
"order": "12345",
},
)
message.send()
recipient_status = message.anymail_status.recipients
self.assertEqual(recipient_status['to1@test.sink.sparkpostmail.com'].status, 'queued')
self.assertEqual(
recipient_status["to1@test.sink.sparkpostmail.com"].status, "queued"
)
@override_settings(ANYMAIL_SPARKPOST_API_KEY="Hey, that's not an API key!")
def test_invalid_api_key(self):

View File

@@ -10,92 +10,129 @@ from anymail.webhooks.sparkpost import SparkPostTrackingWebhookView
from .webhook_cases import WebhookBasicAuthTestCase, WebhookTestCase
@tag('sparkpost')
@tag("sparkpost")
class SparkPostWebhookSecurityTestCase(WebhookBasicAuthTestCase):
def call_webhook(self):
return self.client.post('/anymail/sparkpost/tracking/',
content_type='application/json', data=json.dumps([]))
return self.client.post(
"/anymail/sparkpost/tracking/",
content_type="application/json",
data=json.dumps([]),
)
# Actual tests are in WebhookBasicAuthTestCase
@tag('sparkpost')
@tag("sparkpost")
class SparkPostDeliveryTestCase(WebhookTestCase):
def test_ping_event(self):
raw_events = [{'msys': {}}]
response = self.client.post('/anymail/sparkpost/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [{"msys": {}}]
response = self.client.post(
"/anymail/sparkpost/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
self.assertFalse(self.tracking_handler.called) # no real events
def test_injection_event(self):
# Full event from SparkPost sample events API. (Later tests omit unused event fields.)
raw_events = [{"msys": {"message_event": {
"type": "injection",
"campaign_id": "Example Campaign Name",
"customer_id": "1",
"event_id": "92356927693813856",
"friendly_from": "sender@example.com",
"ip_pool": "Example-Ip-Pool",
"message_id": "000443ee14578172be22",
"msg_from": "sender@example.com",
"msg_size": "1337",
"rcpt_meta": {"customKey": "customValue"},
"rcpt_tags": ["male", "US"],
"rcpt_to": "recipient@example.com",
"raw_rcpt_to": "recipient@example.com",
"rcpt_type": "cc",
"routing_domain": "example.com",
"sending_ip": "127.0.0.1",
"sms_coding": "ASCII",
"sms_dst": "7876712656",
"sms_dst_npi": "E164",
"sms_dst_ton": "International",
"sms_segments": 5,
"sms_src": "1234",
"sms_src_npi": "E164",
"sms_src_ton": "Unknown",
"sms_text": "lol",
"subaccount_id": "101",
"subject": "Summer deals are here!",
"template_id": "templ-1234",
"template_version": "1",
"timestamp": "1454442600",
"transmission_id": "65832150921904138"
}}}]
response = self.client.post('/anymail/sparkpost/tracking/',
content_type='application/json', data=json.dumps(raw_events))
# Full event from SparkPost sample events API.
# (Later tests omit unused event fields.)
raw_events = [
{
"msys": {
"message_event": {
"type": "injection",
"campaign_id": "Example Campaign Name",
"customer_id": "1",
"event_id": "92356927693813856",
"friendly_from": "sender@example.com",
"ip_pool": "Example-Ip-Pool",
"message_id": "000443ee14578172be22",
"msg_from": "sender@example.com",
"msg_size": "1337",
"rcpt_meta": {"customKey": "customValue"},
"rcpt_tags": ["male", "US"],
"rcpt_to": "recipient@example.com",
"raw_rcpt_to": "recipient@example.com",
"rcpt_type": "cc",
"routing_domain": "example.com",
"sending_ip": "127.0.0.1",
"sms_coding": "ASCII",
"sms_dst": "7876712656",
"sms_dst_npi": "E164",
"sms_dst_ton": "International",
"sms_segments": 5,
"sms_src": "1234",
"sms_src_npi": "E164",
"sms_src_ton": "Unknown",
"sms_text": "lol",
"subaccount_id": "101",
"subject": "Summer deals are here!",
"template_id": "templ-1234",
"template_version": "1",
"timestamp": "1454442600",
"transmission_id": "65832150921904138",
}
}
}
]
response = self.client.post(
"/anymail/sparkpost/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SparkPostTrackingWebhookView,
event=ANY, esp_name='SparkPost')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SparkPostTrackingWebhookView,
event=ANY,
esp_name="SparkPost",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "queued")
self.assertEqual(event.timestamp, datetime(2016, 2, 2, 19, 50, 00, tzinfo=timezone.utc))
self.assertEqual(
event.timestamp, datetime(2016, 2, 2, 19, 50, 00, tzinfo=timezone.utc)
)
self.assertEqual(event.esp_event, raw_events[0])
self.assertEqual(event.message_id, "65832150921904138") # actually transmission_id
# normalized "message_id" is actually transmission_id:
self.assertEqual(event.message_id, "65832150921904138")
self.assertEqual(event.event_id, "92356927693813856")
self.assertEqual(event.recipient, "recipient@example.com")
self.assertEqual(event.tags, ["Example Campaign Name"]) # campaign_id (rcpt_tags not available at send)
self.assertEqual(event.metadata, {"customKey": "customValue"}) # includes transmissions.send metadata
# campaign_id (rcpt_tags not available at send):
self.assertEqual(event.tags, ["Example Campaign Name"])
# includes transmissions.send metadata:
self.assertEqual(event.metadata, {"customKey": "customValue"})
def test_delivery_event(self):
raw_events = [{"msys": {"message_event": {
"type": "delivery",
"event_id": "92356927693813856",
"rcpt_to": "recipient@example.com",
"raw_rcpt_to": "Recipient@example.com",
"rcpt_meta": {},
"timestamp": "1454442600",
"transmission_id": "65832150921904138"
}}}]
response = self.client.post('/anymail/sparkpost/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"msys": {
"message_event": {
"type": "delivery",
"event_id": "92356927693813856",
"rcpt_to": "recipient@example.com",
"raw_rcpt_to": "Recipient@example.com",
"rcpt_meta": {},
"timestamp": "1454442600",
"transmission_id": "65832150921904138",
}
}
}
]
response = self.client.post(
"/anymail/sparkpost/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SparkPostTrackingWebhookView,
event=ANY, esp_name='SparkPost')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SparkPostTrackingWebhookView,
event=ANY,
esp_name="SparkPost",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "delivered")
self.assertEqual(event.recipient, "Recipient@example.com")
@@ -103,29 +140,41 @@ class SparkPostDeliveryTestCase(WebhookTestCase):
self.assertEqual(event.metadata, {})
def test_bounce_event(self):
raw_events = [{
"msys": {"message_event": {
"type": "bounce",
"bounce_class": "10",
"customer_id": "00000",
"error_code": "550",
"event_id": "84345317653491230",
"message_id": "0004e3724f57753a3561",
"raw_rcpt_to": "bounce@example.com",
"raw_reason": "550 5.1.1 <bounce@example.com>: Recipient address rejected: User unknown",
"rcpt_to": "bounce@example.com",
"reason": "550 5.1.1 ...@... Recipient address rejected: ...",
"timestamp": "1464824548",
"transmission_id": "84345317650824116",
}},
"cust": {"id": "00000"} # Included in real (non-example) event data
}]
response = self.client.post('/anymail/sparkpost/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"msys": {
"message_event": {
"type": "bounce",
"bounce_class": "10",
"customer_id": "00000",
"error_code": "550",
"event_id": "84345317653491230",
"message_id": "0004e3724f57753a3561",
"raw_rcpt_to": "bounce@example.com",
"raw_reason": "550 5.1.1 <bounce@example.com>:"
" Recipient address rejected: User unknown",
"rcpt_to": "bounce@example.com",
"reason": "550 5.1.1 ...@... Recipient address rejected: ...",
"timestamp": "1464824548",
"transmission_id": "84345317650824116",
}
},
"cust": {"id": "00000"}, # Included in real (non-example) event data
}
]
response = self.client.post(
"/anymail/sparkpost/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SparkPostTrackingWebhookView,
event=ANY, esp_name='SparkPost')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SparkPostTrackingWebhookView,
event=ANY,
esp_name="SparkPost",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "bounced")
self.assertEqual(event.esp_event, raw_events[0])
@@ -133,218 +182,389 @@ class SparkPostDeliveryTestCase(WebhookTestCase):
self.assertEqual(event.event_id, "84345317653491230")
self.assertEqual(event.recipient, "bounce@example.com")
self.assertEqual(event.reject_reason, "invalid")
self.assertEqual(event.mta_response,
"550 5.1.1 <bounce@example.com>: Recipient address rejected: User unknown")
self.assertEqual(
event.mta_response,
"550 5.1.1 <bounce@example.com>: Recipient address rejected: User unknown",
)
def test_delay_event(self):
raw_events = [{"msys": {"message_event": {
"type": "delay",
"bounce_class": "21",
"error_code": "454",
"event_id": "84345317653675522",
"message_id": "0004e3724f57753a3861",
"num_retries": "1",
"queue_time": "1200161",
"raw_rcpt_to": "recipient@nomx.example.com",
"raw_reason": "454 4.4.4 [internal] no MX or A for domain",
"rcpt_to": "recipient@nomx.example.com",
"reason": "454 4.4.4 [internal] no MX or A for domain",
"timestamp": "1464825748",
"transmission_id": "84345317650824116",
}}}]
response = self.client.post('/anymail/sparkpost/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"msys": {
"message_event": {
"type": "delay",
"bounce_class": "21",
"error_code": "454",
"event_id": "84345317653675522",
"message_id": "0004e3724f57753a3861",
"num_retries": "1",
"queue_time": "1200161",
"raw_rcpt_to": "recipient@nomx.example.com",
"raw_reason": "454 4.4.4 [internal] no MX or A for domain",
"rcpt_to": "recipient@nomx.example.com",
"reason": "454 4.4.4 [internal] no MX or A for domain",
"timestamp": "1464825748",
"transmission_id": "84345317650824116",
}
}
}
]
response = self.client.post(
"/anymail/sparkpost/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SparkPostTrackingWebhookView,
event=ANY, esp_name='SparkPost')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SparkPostTrackingWebhookView,
event=ANY,
esp_name="SparkPost",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "deferred")
self.assertEqual(event.esp_event, raw_events[0])
self.assertEqual(event.recipient, "recipient@nomx.example.com")
self.assertEqual(event.mta_response, "454 4.4.4 [internal] no MX or A for domain")
self.assertEqual(
event.mta_response, "454 4.4.4 [internal] no MX or A for domain"
)
def test_unsubscribe_event(self):
raw_events = [{"msys": {"unsubscribe_event": {
"type": "list_unsubscribe",
"event_id": "66331590532986193",
"message_id": "0004278150574660124d",
"raw_rcpt_to": "recipient@example.com",
"rcpt_to": "recipient@example.com",
"timestamp": "1464894280",
"transmission_id": "84345993965073285",
}}}]
response = self.client.post('/anymail/sparkpost/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"msys": {
"unsubscribe_event": {
"type": "list_unsubscribe",
"event_id": "66331590532986193",
"message_id": "0004278150574660124d",
"raw_rcpt_to": "recipient@example.com",
"rcpt_to": "recipient@example.com",
"timestamp": "1464894280",
"transmission_id": "84345993965073285",
}
}
}
]
response = self.client.post(
"/anymail/sparkpost/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SparkPostTrackingWebhookView,
event=ANY, esp_name='SparkPost')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SparkPostTrackingWebhookView,
event=ANY,
esp_name="SparkPost",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "unsubscribed")
self.assertEqual(event.recipient, "recipient@example.com")
def test_generation_rejection_event(self):
# This is what you get if you try to send to a suppressed address
raw_events = [{"msys": {"gen_event": {
"type": "generation_rejection",
"error_code": "554",
"event_id": "102360394390563734",
"message_id": "0005c29950577c61695d",
"raw_rcpt_to": "suppressed@example.com",
"raw_reason": "554 5.7.1 recipient address suppressed due to customer policy",
"rcpt_to": "suppressed@example.com",
"reason": "554 5.7.1 recipient address suppressed due to customer policy",
"timestamp": "1464900034",
"transmission_id": "102360394387646691",
}}}]
response = self.client.post('/anymail/sparkpost/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"msys": {
"gen_event": {
"type": "generation_rejection",
"error_code": "554",
"event_id": "102360394390563734",
"message_id": "0005c29950577c61695d",
"raw_rcpt_to": "suppressed@example.com",
"raw_reason": "554 5.7.1 recipient address suppressed"
" due to customer policy",
"rcpt_to": "suppressed@example.com",
"reason": "554 5.7.1 recipient address suppressed"
" due to customer policy",
"timestamp": "1464900034",
"transmission_id": "102360394387646691",
}
}
}
]
response = self.client.post(
"/anymail/sparkpost/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SparkPostTrackingWebhookView,
event=ANY, esp_name='SparkPost')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SparkPostTrackingWebhookView,
event=ANY,
esp_name="SparkPost",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "rejected")
self.assertEqual(event.recipient, "suppressed@example.com")
self.assertEqual(event.mta_response, "554 5.7.1 recipient address suppressed due to customer policy")
self.assertEqual(
event.mta_response,
"554 5.7.1 recipient address suppressed due to customer policy",
)
def test_generation_failure_event(self):
# This is what you get from a template rendering failure
raw_events = [{"msys": {"message_event": {
"type": "generation_failure",
"error_code": "554",
"event_id": "139013368081587254",
"raw_rcpt_to": "recipient@example.com",
"raw_reason": "554 5.3.3 [internal] Error while rendering part html: ...",
"rcpt_subs": {"name": "Alice", "order_no": "12345"},
"rcpt_to": "recipient@example.com",
"reason": "554 5.3.3 [internal] Error while rendering part html: ...",
"tdate": "2018-10-11T23:24:45.000Z",
"template_id": "test-template",
"template_version": "3",
"transmission_id": "139013368081177607",
"timestamp": "2018-10-11T23:24:45.000+00:00"
}}}]
response = self.client.post('/anymail/sparkpost/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"msys": {
"message_event": {
"type": "generation_failure",
"error_code": "554",
"event_id": "139013368081587254",
"raw_rcpt_to": "recipient@example.com",
"raw_reason": "554 5.3.3 [internal]"
" Error while rendering part html: ...",
"rcpt_subs": {"name": "Alice", "order_no": "12345"},
"rcpt_to": "recipient@example.com",
"reason": "554 5.3.3 [internal]"
" Error while rendering part html: ...",
"tdate": "2018-10-11T23:24:45.000Z",
"template_id": "test-template",
"template_version": "3",
"transmission_id": "139013368081177607",
"timestamp": "2018-10-11T23:24:45.000+00:00",
}
}
}
]
response = self.client.post(
"/anymail/sparkpost/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SparkPostTrackingWebhookView,
event=ANY, esp_name='SparkPost')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SparkPostTrackingWebhookView,
event=ANY,
esp_name="SparkPost",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "failed")
self.assertEqual(event.recipient, "recipient@example.com")
self.assertEqual(event.mta_response, "554 5.3.3 [internal] Error while rendering part html: ...")
self.assertEqual(
event.mta_response,
"554 5.3.3 [internal] Error while rendering part html: ...",
)
def test_bounce_challenge_response(self):
# Test for changing initial event_type based on bounce_class
raw_events = [{"msys": {"message_event": {
"type": "bounce",
"bounce_class": "60",
"raw_rcpt_to": "vacationing@example.com",
"rcpt_to": "vacationing@example.com",
}}}]
response = self.client.post('/anymail/sparkpost/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"msys": {
"message_event": {
"type": "bounce",
"bounce_class": "60",
"raw_rcpt_to": "vacationing@example.com",
"rcpt_to": "vacationing@example.com",
}
}
}
]
response = self.client.post(
"/anymail/sparkpost/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SparkPostTrackingWebhookView,
event=ANY, esp_name='SparkPost')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SparkPostTrackingWebhookView,
event=ANY,
esp_name="SparkPost",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "autoresponded")
self.assertEqual(event.reject_reason, "other")
self.assertEqual(event.recipient, "vacationing@example.com")
def test_open_event(self):
raw_events = [{"msys": {"track_event": {
"type": "open",
"raw_rcpt_to": "recipient@example.com",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36",
}}}]
response = self.client.post('/anymail/sparkpost/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"msys": {
"track_event": {
"type": "open",
"raw_rcpt_to": "recipient@example.com",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3)"
" AppleWebKit/537.36",
}
}
}
]
response = self.client.post(
"/anymail/sparkpost/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SparkPostTrackingWebhookView,
event=ANY, esp_name='SparkPost')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SparkPostTrackingWebhookView,
event=ANY,
esp_name="SparkPost",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "opened")
self.assertEqual(event.user_agent, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36")
self.assertEqual(
event.user_agent,
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36",
)
@override_settings(ANYMAIL_SPARKPOST_TRACK_INITIAL_OPEN_AS_OPENED=True)
def test_initial_open_event_as_opened(self):
# Mapping SparkPost "initial_open" to Anymail normalized "opened" is opt-in via a setting,
# for backwards compatibility and to avoid reporting duplicate "opened" events when all
# SparkPost event types are enabled.
raw_events = [{"msys": {"track_event": {
"type": "initial_open",
"raw_rcpt_to": "recipient@example.com",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36",
}}}]
response = self.client.post('/anymail/sparkpost/tracking/',
content_type='application/json', data=json.dumps(raw_events))
# Mapping SparkPost "initial_open" to Anymail normalized "opened" is opt-in
# via a setting, for backwards compatibility and to avoid reporting duplicate
# "opened" events when all SparkPost event types are enabled.
raw_events = [
{
"msys": {
"track_event": {
"type": "initial_open",
"raw_rcpt_to": "recipient@example.com",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3)"
" AppleWebKit/537.36",
}
}
}
]
response = self.client.post(
"/anymail/sparkpost/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SparkPostTrackingWebhookView,
event=ANY, esp_name='SparkPost')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SparkPostTrackingWebhookView,
event=ANY,
esp_name="SparkPost",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "opened")
self.assertEqual(event.user_agent, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36")
self.assertEqual(
event.user_agent,
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36",
)
def test_initial_open_event_as_unknown(self):
# By default, SparkPost "initial_open" is *not* mapped to Anymail "opened".
raw_events = [{"msys": {"track_event": {
"type": "initial_open",
"raw_rcpt_to": "recipient@example.com",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36",
}}}]
response = self.client.post('/anymail/sparkpost/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"msys": {
"track_event": {
"type": "initial_open",
"raw_rcpt_to": "recipient@example.com",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3)"
" AppleWebKit/537.36",
}
}
}
]
response = self.client.post(
"/anymail/sparkpost/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SparkPostTrackingWebhookView,
event=ANY, esp_name='SparkPost')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SparkPostTrackingWebhookView,
event=ANY,
esp_name="SparkPost",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "unknown")
# Here's how to get the raw SparkPost event type:
self.assertEqual(event.esp_event["msys"].get("track_event", {}).get("type"), "initial_open")
self.assertEqual(
event.esp_event["msys"].get("track_event", {}).get("type"), "initial_open"
)
# Note that other Anymail normalized event properties are still available:
self.assertEqual(event.user_agent, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36")
self.assertEqual(
event.user_agent,
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36",
)
def test_click_event(self):
raw_events = [{"msys": {"track_event": {
"type": "amp_click",
"raw_rcpt_to": "recipient@example.com",
"target_link_name": "Example Link Name",
"target_link_url": "http://example.com",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36",
}}}]
response = self.client.post('/anymail/sparkpost/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"msys": {
"track_event": {
"type": "amp_click",
"raw_rcpt_to": "recipient@example.com",
"target_link_name": "Example Link Name",
"target_link_url": "http://example.com",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3)"
" AppleWebKit/537.36",
}
}
}
]
response = self.client.post(
"/anymail/sparkpost/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=SparkPostTrackingWebhookView,
event=ANY, esp_name='SparkPost')
event = kwargs['event']
kwargs = self.assert_handler_called_once_with(
self.tracking_handler,
sender=SparkPostTrackingWebhookView,
event=ANY,
esp_name="SparkPost",
)
event = kwargs["event"]
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "clicked")
self.assertEqual(event.recipient, "recipient@example.com")
self.assertEqual(event.user_agent, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36")
self.assertEqual(
event.user_agent,
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36",
)
self.assertEqual(event.click_url, "http://example.com")
def test_amp_events(self):
raw_events = [{"msys": {"track_event": {
"type": "amp_open",
}}}, {"msys": {"track_event": {
"type": "amp_initial_open",
}}}, {"msys": {"track_event": {
"type": "amp_click",
}}}]
response = self.client.post('/anymail/sparkpost/tracking/',
content_type='application/json', data=json.dumps(raw_events))
raw_events = [
{
"msys": {
"track_event": {
"type": "amp_open",
}
}
},
{
"msys": {
"track_event": {
"type": "amp_initial_open",
}
}
},
{
"msys": {
"track_event": {
"type": "amp_click",
}
}
},
]
response = self.client.post(
"/anymail/sparkpost/tracking/",
content_type="application/json",
data=json.dumps(raw_events),
)
self.assertEqual(response.status_code, 200)
self.assertEqual(self.tracking_handler.call_count, 3)
events = [kwargs["event"] for (args, kwargs) in self.tracking_handler.call_args_list]
events = [
kwargs["event"] for (args, kwargs) in self.tracking_handler.call_args_list
]
self.assertEqual(events[0].event_type, "opened")
self.assertEqual(events[1].event_type, "unknown") # amp_initial_open is mapped to "unknown" by default
# amp_initial_open is mapped to "unknown" by default:
self.assertEqual(events[1].event_type, "unknown")
self.assertEqual(events[2].event_type, "clicked")

View File

@@ -7,18 +7,28 @@ from email.mime.image import MIMEImage
from email.mime.text import MIMEText
from django.http import QueryDict
from django.test import SimpleTestCase, RequestFactory, override_settings
from django.test import RequestFactory, SimpleTestCase, override_settings
from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy
from anymail.exceptions import AnymailInvalidAddress, _LazyError
from anymail.utils import (
parse_address_list, parse_single_address, EmailAddress,
UNSET,
Attachment,
is_lazy, force_non_lazy, force_non_lazy_dict, force_non_lazy_list,
update_deep, UNSET,
get_request_uri, get_request_basic_auth, parse_rfc2822date, querydict_getfirst,
CaseInsensitiveCasePreservingDict)
CaseInsensitiveCasePreservingDict,
EmailAddress,
force_non_lazy,
force_non_lazy_dict,
force_non_lazy_list,
get_request_basic_auth,
get_request_uri,
is_lazy,
parse_address_list,
parse_rfc2822date,
parse_single_address,
querydict_getfirst,
update_deep,
)
class ParseAddressListTests(SimpleTestCase):
@@ -48,36 +58,46 @@ class ParseAddressListTests(SimpleTestCase):
def test_obsolete_display_name(self):
# you can get away without the quotes if there are no commas or parens
# (but it's not recommended)
parsed_list = parse_address_list(['Display Name <test@example.com>'])
parsed_list = parse_address_list(["Display Name <test@example.com>"])
self.assertEqual(len(parsed_list), 1)
parsed = parsed_list[0]
self.assertEqual(parsed.addr_spec, "test@example.com")
self.assertEqual(parsed.display_name, "Display Name")
self.assertEqual(parsed.address, 'Display Name <test@example.com>')
self.assertEqual(parsed.address, "Display Name <test@example.com>")
def test_unicode_display_name(self):
parsed_list = parse_address_list(['"Unicode \N{HEAVY BLACK HEART}" <test@example.com>'])
parsed_list = parse_address_list(
['"Unicode \N{HEAVY BLACK HEART}" <test@example.com>']
)
self.assertEqual(len(parsed_list), 1)
parsed = parsed_list[0]
self.assertEqual(parsed.addr_spec, "test@example.com")
self.assertEqual(parsed.display_name, "Unicode \N{HEAVY BLACK HEART}")
# formatted display-name automatically shifts to quoted-printable/base64 for non-ascii chars:
self.assertEqual(parsed.address, '=?utf-8?b?VW5pY29kZSDinaQ=?= <test@example.com>')
# formatted display-name automatically shifts
# to quoted-printable/base64 for non-ascii chars:
self.assertEqual(
parsed.address, "=?utf-8?b?VW5pY29kZSDinaQ=?= <test@example.com>"
)
def test_invalid_display_name(self):
with self.assertRaisesMessage(AnymailInvalidAddress, "Invalid email address 'webmaster'"):
parse_address_list(['webmaster'])
with self.assertRaisesMessage(
AnymailInvalidAddress, "Invalid email address 'webmaster'"
):
parse_address_list(["webmaster"])
with self.assertRaisesMessage(AnymailInvalidAddress, "Maybe missing quotes around a display-name?"):
with self.assertRaisesMessage(
AnymailInvalidAddress, "Maybe missing quotes around a display-name?"
):
# this parses as multiple email addresses, because of the comma:
parse_address_list(['Display Name, Inc. <test@example.com>'])
parse_address_list(["Display Name, Inc. <test@example.com>"])
def test_idn(self):
parsed_list = parse_address_list(["idn@\N{ENVELOPE}.example.com"])
self.assertEqual(len(parsed_list), 1)
parsed = parsed_list[0]
self.assertEqual(parsed.addr_spec, "idn@\N{ENVELOPE}.example.com")
self.assertEqual(parsed.address, "idn@xn--4bi.example.com") # punycode-encoded domain
# punycode-encoded domain:
self.assertEqual(parsed.address, "idn@xn--4bi.example.com")
self.assertEqual(parsed.username, "idn")
self.assertEqual(parsed.domain, "\N{ENVELOPE}.example.com")
@@ -88,23 +108,23 @@ class ParseAddressListTests(SimpleTestCase):
def test_empty_address(self):
with self.assertRaises(AnymailInvalidAddress):
parse_address_list([''])
parse_address_list([""])
def test_whitespace_only_address(self):
with self.assertRaises(AnymailInvalidAddress):
parse_address_list([' '])
parse_address_list([" "])
def test_invalid_address(self):
with self.assertRaises(AnymailInvalidAddress):
parse_address_list(['localonly'])
parse_address_list(["localonly"])
with self.assertRaises(AnymailInvalidAddress):
parse_address_list(['localonly@'])
parse_address_list(["localonly@"])
with self.assertRaises(AnymailInvalidAddress):
parse_address_list(['@domainonly'])
parse_address_list(["@domainonly"])
with self.assertRaises(AnymailInvalidAddress):
parse_address_list(['<localonly@>'])
parse_address_list(["<localonly@>"])
with self.assertRaises(AnymailInvalidAddress):
parse_address_list(['<@domainonly>'])
parse_address_list(["<@domainonly>"])
def test_email_list(self):
parsed_list = parse_address_list(["first@example.com", "second@example.com"])
@@ -126,11 +146,12 @@ class ParseAddressListTests(SimpleTestCase):
# the bare "Display Name" below should *not* get merged with
# the email in the second item
with self.assertRaisesMessage(AnymailInvalidAddress, "Display Name"):
parse_address_list(['"Display Name"', '<valid@example.com>'])
parse_address_list(['"Display Name"', "<valid@example.com>"])
def test_invalid_with_unicode(self):
with self.assertRaisesMessage(AnymailInvalidAddress,
"Invalid email address '\N{ENVELOPE}'"):
with self.assertRaisesMessage(
AnymailInvalidAddress, "Invalid email address '\N{ENVELOPE}'"
):
parse_address_list(["\N{ENVELOPE}"])
def test_single_string(self):
@@ -140,7 +161,9 @@ class ParseAddressListTests(SimpleTestCase):
self.assertEqual(parsed_list[0].addr_spec, "one@example.com")
def test_lazy_strings(self):
parsed_list = parse_address_list([gettext_lazy('"Example, Inc." <one@example.com>')])
parsed_list = parse_address_list(
[gettext_lazy('"Example, Inc." <one@example.com>')]
)
self.assertEqual(len(parsed_list), 1)
self.assertEqual(parsed_list[0].display_name, "Example, Inc.")
self.assertEqual(parsed_list[0].addr_spec, "one@example.com")
@@ -154,7 +177,9 @@ class ParseAddressListTests(SimpleTestCase):
parsed = parse_single_address("one@example.com")
self.assertEqual(parsed.address, "one@example.com")
with self.assertRaisesMessage(AnymailInvalidAddress, "Only one email address is allowed; found 2"):
with self.assertRaisesMessage(
AnymailInvalidAddress, "Only one email address is allowed; found 2"
):
parse_single_address("one@example.com, two@example.com")
with self.assertRaisesMessage(AnymailInvalidAddress, "Invalid email address"):
@@ -174,8 +199,10 @@ class ParseAddressListTests(SimpleTestCase):
_ = EmailAddress(name, addr)
def test_email_address_repr(self):
self.assertEqual("EmailAddress('Name', 'addr@example.com')",
repr(EmailAddress('Name', 'addr@example.com')))
self.assertEqual(
"EmailAddress('Name', 'addr@example.com')",
repr(EmailAddress("Name", "addr@example.com")),
)
class NormalizedAttachmentTests(SimpleTestCase):
@@ -192,25 +219,32 @@ class NormalizedAttachmentTests(SimpleTestCase):
self.assertFalse(att.inline)
self.assertIsNone(att.content_id)
self.assertEqual(att.cid, "")
self.assertEqual(repr(att), "Attachment<image/x-emoticon, len=3, name='emoticon.txt'>")
self.assertEqual(
repr(att), "Attachment<image/x-emoticon, len=3, name='emoticon.txt'>"
)
def test_content_disposition_inline(self):
image = MIMEImage(b";-)", "x-emoticon")
image["Content-Disposition"] = 'inline'
image["Content-Disposition"] = "inline"
att = Attachment(image, "ascii")
self.assertIsNone(att.name)
self.assertEqual(att.content, b";-)")
self.assertTrue(att.inline) # even without the Content-ID
self.assertIsNone(att.content_id)
self.assertEqual(att.cid, "")
self.assertEqual(repr(att), "Attachment<inline, image/x-emoticon, len=3, content_id=None>")
self.assertEqual(
repr(att), "Attachment<inline, image/x-emoticon, len=3, content_id=None>"
)
image["Content-ID"] = "<abc123@example.net>"
att = Attachment(image, "ascii")
self.assertEqual(att.content_id, "<abc123@example.net>")
self.assertEqual(att.cid, "abc123@example.net")
self.assertEqual(repr(att),
"Attachment<inline, image/x-emoticon, len=3, content_id='<abc123@example.net>'>")
self.assertEqual(
repr(att),
"Attachment<inline, image/x-emoticon, len=3,"
" content_id='<abc123@example.net>'>",
)
def test_content_id_implies_inline(self):
"""A MIME object with a Content-ID should be assumed to be inline"""
@@ -219,8 +253,11 @@ class NormalizedAttachmentTests(SimpleTestCase):
att = Attachment(image, "ascii")
self.assertTrue(att.inline)
self.assertEqual(att.content_id, "<abc123@example.net>")
self.assertEqual(repr(att),
"Attachment<inline, image/x-emoticon, len=3, content_id='<abc123@example.net>'>")
self.assertEqual(
repr(att),
"Attachment<inline, image/x-emoticon, len=3,"
" content_id='<abc123@example.net>'>",
)
# ... but not if explicit Content-Disposition says otherwise
image["Content-Disposition"] = "attachment"
@@ -246,7 +283,7 @@ class LazyCoercionTests(SimpleTestCase):
self.assertFalse(is_lazy("text not lazy"))
self.assertFalse(is_lazy(b"bytes not lazy"))
self.assertFalse(is_lazy(None))
self.assertFalse(is_lazy({'dict': "not lazy"}))
self.assertFalse(is_lazy({"dict": "not lazy"}))
self.assertFalse(is_lazy(["list", "not lazy"]))
self.assertFalse(is_lazy(object()))
self.assertFalse(is_lazy([gettext_lazy("doesn't recurse")]))
@@ -257,10 +294,20 @@ class LazyCoercionTests(SimpleTestCase):
self.assertEqual(result, "text")
def test_format_lazy(self):
self.assertTrue(is_lazy(format_lazy("{0}{1}",
gettext_lazy("concatenation"), gettext_lazy("is lazy"))))
result = force_non_lazy(format_lazy("{first}/{second}",
first=gettext_lazy("text"), second=gettext_lazy("format")))
self.assertTrue(
is_lazy(
format_lazy(
"{0}{1}", gettext_lazy("concatenation"), gettext_lazy("is lazy")
)
)
)
result = force_non_lazy(
format_lazy(
"{first}/{second}",
first=gettext_lazy("text"),
second=gettext_lazy("format"),
)
)
self.assertIsInstance(result, str)
self.assertEqual(result, "text/format")
@@ -279,11 +326,12 @@ class LazyCoercionTests(SimpleTestCase):
self.assertIsNone(result)
def test_force_dict(self):
result = force_non_lazy_dict({'a': 1, 'b': gettext_lazy("b"),
'c': {'c1': gettext_lazy("c1")}})
self.assertEqual(result, {'a': 1, 'b': "b", 'c': {'c1': "c1"}})
self.assertIsInstance(result['b'], str)
self.assertIsInstance(result['c']['c1'], str)
result = force_non_lazy_dict(
{"a": 1, "b": gettext_lazy("b"), "c": {"c1": gettext_lazy("c1")}}
)
self.assertEqual(result, {"a": 1, "b": "b", "c": {"c1": "c1"}})
self.assertIsInstance(result["b"], str)
self.assertIsInstance(result["c"]["c1"], str)
def test_force_list(self):
result = force_non_lazy_list([0, gettext_lazy("b"), "c"])
@@ -295,26 +343,28 @@ class UpdateDeepTests(SimpleTestCase):
"""Test utils.update_deep"""
def test_updates_recursively(self):
first = {'a': {'a1': 1, 'aa': {}}, 'b': "B"}
second = {'a': {'a2': 2, 'aa': {'aa1': 11}}}
first = {"a": {"a1": 1, "aa": {}}, "b": "B"}
second = {"a": {"a2": 2, "aa": {"aa1": 11}}}
result = update_deep(first, second)
self.assertEqual(first, {'a': {'a1': 1, 'a2': 2, 'aa': {'aa1': 11}}, 'b': "B"})
self.assertIsNone(result) # modifies first in place; doesn't return it (same as dict.update())
self.assertEqual(first, {"a": {"a1": 1, "a2": 2, "aa": {"aa1": 11}}, "b": "B"})
# modifies first in place; doesn't return it (same as dict.update()):
self.assertIsNone(result)
def test_overwrites_sequences(self):
"""Only mappings are handled recursively; sequences are considered atomic"""
first = {'a': [1, 2]}
second = {'a': [3]}
first = {"a": [1, 2]}
second = {"a": [3]}
update_deep(first, second)
self.assertEqual(first, {'a': [3]})
self.assertEqual(first, {"a": [3]})
def test_handles_non_dict_mappings(self):
"""Mapping types in general are supported"""
from collections import OrderedDict, defaultdict
first = OrderedDict(a=OrderedDict(a1=1), c={'c1': 1})
first = OrderedDict(a=OrderedDict(a1=1), c={"c1": 1})
second = defaultdict(None, a=dict(a2=2))
update_deep(first, second)
self.assertEqual(first, {'a': {'a1': 1, 'a2': 2}, 'c': {'c1': 1}})
self.assertEqual(first, {"a": {"a1": 1, "a2": 2}, "c": {"c1": 1}})
@override_settings(ALLOWED_HOSTS=[".example.com"])
@@ -327,68 +377,89 @@ class RequestUtilsTests(SimpleTestCase):
@staticmethod
def basic_auth(username, password):
"""Return HTTP_AUTHORIZATION header value for basic auth with username, password"""
credentials = base64.b64encode("{}:{}".format(username, password).encode('utf-8')).decode('utf-8')
"""
Return HTTP_AUTHORIZATION header value for basic auth with username, password
"""
credentials = base64.b64encode(
"{}:{}".format(username, password).encode("utf-8")
).decode("utf-8")
return "Basic {}".format(credentials)
def test_get_request_basic_auth(self):
# without auth:
request = self.request_factory.post('/path/to/?query',
HTTP_HOST='www.example.com',
HTTP_SCHEME='https')
request = self.request_factory.post(
"/path/to/?query", HTTP_HOST="www.example.com", HTTP_SCHEME="https"
)
self.assertIsNone(get_request_basic_auth(request))
# with basic auth:
request = self.request_factory.post('/path/to/?query',
HTTP_HOST='www.example.com',
HTTP_AUTHORIZATION=self.basic_auth('user', 'pass'))
request = self.request_factory.post(
"/path/to/?query",
HTTP_HOST="www.example.com",
HTTP_AUTHORIZATION=self.basic_auth("user", "pass"),
)
self.assertEqual(get_request_basic_auth(request), "user:pass")
# with some other auth
request = self.request_factory.post('/path/to/?query',
HTTP_HOST='www.example.com',
HTTP_AUTHORIZATION="Bearer abcde12345")
request = self.request_factory.post(
"/path/to/?query",
HTTP_HOST="www.example.com",
HTTP_AUTHORIZATION="Bearer abcde12345",
)
self.assertIsNone(get_request_basic_auth(request))
def test_get_request_uri(self):
# without auth:
request = self.request_factory.post('/path/to/?query', secure=True,
HTTP_HOST='www.example.com')
self.assertEqual(get_request_uri(request),
"https://www.example.com/path/to/?query")
request = self.request_factory.post(
"/path/to/?query", secure=True, HTTP_HOST="www.example.com"
)
self.assertEqual(
get_request_uri(request), "https://www.example.com/path/to/?query"
)
# with basic auth:
request = self.request_factory.post('/path/to/?query', secure=True,
HTTP_HOST='www.example.com',
HTTP_AUTHORIZATION=self.basic_auth('user', 'pass'))
self.assertEqual(get_request_uri(request),
"https://user:pass@www.example.com/path/to/?query")
request = self.request_factory.post(
"/path/to/?query",
secure=True,
HTTP_HOST="www.example.com",
HTTP_AUTHORIZATION=self.basic_auth("user", "pass"),
)
self.assertEqual(
get_request_uri(request), "https://user:pass@www.example.com/path/to/?query"
)
@override_settings(SECURE_PROXY_SSL_HEADER=('HTTP_X_FORWARDED_PROTO', 'https'),
USE_X_FORWARDED_HOST=True)
@override_settings(
SECURE_PROXY_SSL_HEADER=("HTTP_X_FORWARDED_PROTO", "https"),
USE_X_FORWARDED_HOST=True,
)
def test_get_request_uri_with_proxy(self):
request = self.request_factory.post('/path/to/?query', secure=False,
HTTP_HOST='web1.internal',
HTTP_X_FORWARDED_PROTO='https',
HTTP_X_FORWARDED_HOST='secret.example.com:8989',
HTTP_AUTHORIZATION=self.basic_auth('user', 'pass'))
self.assertEqual(get_request_uri(request),
"https://user:pass@secret.example.com:8989/path/to/?query")
request = self.request_factory.post(
"/path/to/?query",
secure=False,
HTTP_HOST="web1.internal",
HTTP_X_FORWARDED_PROTO="https",
HTTP_X_FORWARDED_HOST="secret.example.com:8989",
HTTP_AUTHORIZATION=self.basic_auth("user", "pass"),
)
self.assertEqual(
get_request_uri(request),
"https://user:pass@secret.example.com:8989/path/to/?query",
)
class QueryDictUtilsTests(SimpleTestCase):
def test_querydict_getfirst(self):
q = QueryDict("a=one&a=two&a=three")
q.getfirst = querydict_getfirst.__get__(q)
self.assertEqual(q.getfirst('a'), "one")
self.assertEqual(q.getfirst("a"), "one")
# missing key exception:
with self.assertRaisesMessage(KeyError, "not a key"):
q.getfirst("not a key")
# defaults:
self.assertEqual(q.getfirst('not a key', "beta"), "beta")
self.assertIsNone(q.getfirst('not a key', None))
self.assertEqual(q.getfirst("not a key", "beta"), "beta")
self.assertIsNone(q.getfirst("not a key", None))
class ParseRFC2822DateTests(SimpleTestCase):
@@ -406,9 +477,11 @@ class ParseRFC2822DateTests(SimpleTestCase):
self.assertIsNotNone(dt.tzinfo) # aware
def test_without_timezones(self):
dt = parse_rfc2822date("Tue, 24 Oct 2017 10:11:35 -0000") # "no timezone information"
# "no timezone information":
dt = parse_rfc2822date("Tue, 24 Oct 2017 10:11:35 -0000")
self.assertEqual(dt.isoformat(), "2017-10-24T10:11:35")
self.assertIsNone(dt.tzinfo) # naive (compare with +0000 version in previous test)
# naive (compare with +0000 version in previous test):
self.assertIsNone(dt.tzinfo)
dt = parse_rfc2822date("Tue, 24 Oct 2017 10:11:35")
self.assertEqual(dt.isoformat(), "2017-10-24T10:11:35")

View File

@@ -15,13 +15,14 @@ import django.test.client
def decode_att(att):
"""Returns the original data from base64-encoded attachment content"""
return b64decode(att.encode('ascii'))
return b64decode(att.encode("ascii"))
def rfc822_unfold(text):
# "Unfolding is accomplished by simply removing any CRLF that is immediately followed by WSP"
# (WSP is space or tab, and per email.parser semantics, we allow CRLF, CR, or LF endings)
return re.sub(r'(\r\n|\r|\n)(?=[ \t])', "", text)
# "Unfolding is accomplished by simply removing any CRLF that is immediately
# followed by WSP" (WSP is space or tab, and per email.parser semantics, we allow
# CRLF, CR, or LF endings)
return re.sub(r"(\r\n|\r|\n)(?=[ \t])", "", text)
#
@@ -60,7 +61,9 @@ def sample_email_path(filename=SAMPLE_EMAIL_FILENAME):
def sample_email_content(filename=SAMPLE_EMAIL_FILENAME):
"""Returns bytes contents of an email file (e.g., for forwarding as an attachment)"""
"""
Returns bytes contents of an email file (e.g., for forwarding as an attachment)
"""
return test_file_content(filename)
@@ -68,6 +71,7 @@ def sample_email_content(filename=SAMPLE_EMAIL_FILENAME):
# TestCase helpers
#
class AnymailTestMixin(TestCase):
"""Helpful additional methods for Anymail tests"""
@@ -86,20 +90,21 @@ class AnymailTestMixin(TestCase):
if key not in actual:
missing.append(key)
elif value != actual[key]:
mismatched.append('%s, expected: %s, actual: %s' %
(safe_repr(key), safe_repr(value),
safe_repr(actual[key])))
mismatched.append(
"%s, expected: %s, actual: %s"
% (safe_repr(key), safe_repr(value), safe_repr(actual[key]))
)
if not (missing or mismatched):
return
standardMsg = ''
standardMsg = ""
if missing:
standardMsg = 'Missing: %s' % ','.join(safe_repr(m) for m in missing)
standardMsg = "Missing: %s" % ",".join(safe_repr(m) for m in missing)
if mismatched:
if standardMsg:
standardMsg += '; '
standardMsg += 'Mismatched values: %s' % ','.join(mismatched)
standardMsg += "; "
standardMsg += "Mismatched values: %s" % ",".join(mismatched)
self.fail(self._formatMessage(msg, standardMsg))
@@ -124,8 +129,8 @@ class AnymailTestMixin(TestCase):
# (Technically, this is unfolding both headers and (incorrectly) bodies,
# but that doesn't really affect the tests.)
if isinstance(first, bytes) and isinstance(second, bytes):
first = first.decode('utf-8')
second = second.decode('utf-8')
first = first.decode("utf-8")
second = second.decode("utf-8")
first = rfc822_unfold(first)
second = rfc822_unfold(second)
self.assertEqual(first, second, msg)
@@ -135,8 +140,7 @@ class AnymailTestMixin(TestCase):
try:
uuid.UUID(uuid_str, version=version)
except (ValueError, AttributeError, TypeError):
raise self.failureException(
msg or "%r is not a valid UUID" % uuid_str)
raise self.failureException(msg or "%r is not a valid UUID" % uuid_str)
@contextmanager
def assertPrints(self, expected, match="contain", msg=None):
@@ -168,8 +172,11 @@ class AnymailTestMixin(TestCase):
bound_matchfn = getattr(actual, matchfn)
if not bound_matchfn(expected):
raise self.failureException(
msg or "Stdout {actual!r} does not {match} {expected!r}".format(
actual=actual, match=match, expected=expected))
msg
or "Stdout {actual!r} does not {match} {expected!r}".format(
actual=actual, match=match, expected=expected
)
)
finally:
sys.stdout = old_stdout
@@ -186,8 +193,8 @@ class ClientWithCsrfChecks(django.test.Client):
# dedent for bytestrs
# https://stackoverflow.com/a/39841195/647002
_whitespace_only_re = re.compile(b'^[ \t]+$', re.MULTILINE)
_leading_whitespace_re = re.compile(b'(^[ \t]*)(?:[^ \t\n])', re.MULTILINE)
_whitespace_only_re = re.compile(b"^[ \t]+$", re.MULTILINE)
_leading_whitespace_re = re.compile(b"(^[ \t]*)(?:[^ \t\n])", re.MULTILINE)
def dedent_bytes(text):
@@ -195,7 +202,7 @@ def dedent_bytes(text):
# Look for the longest leading string of spaces and tabs common to
# all lines.
margin = None
text = _whitespace_only_re.sub(b'', text)
text = _whitespace_only_re.sub(b"", text)
indents = _leading_whitespace_re.findall(text)
for indent in indents:
if margin is None:
@@ -219,10 +226,10 @@ def dedent_bytes(text):
margin = margin[:i]
break
else:
margin = margin[:len(indent)]
margin = margin[: len(indent)]
if margin:
text = re.sub(b'(?m)^' + margin, b'', text)
text = re.sub(b"(?m)^" + margin, b"", text)
return text
@@ -233,7 +240,7 @@ def make_fileobj(content, filename=None, content_type=None, encoding=None):
"""
# The logic that unpacks this is in django.test.client.encode_file.
if isinstance(content, str):
content = content.encode(encoding or 'utf-8')
content = content.encode(encoding or "utf-8")
fileobj = BytesIO(content)
if filename is not None:
fileobj.name = filename
@@ -254,5 +261,5 @@ def encode_multipart(boundary, data):
encoded = django.test.client.encode_multipart(boundary, data)
re_keys = r"|".join(re.escape(key) for key in data.keys())
return re.sub(
rb'filename="(%s)"' % re_keys.encode("ascii"),
b'filename=""', encoded)
rb'filename="(%s)"' % re_keys.encode("ascii"), b'filename=""', encoded
)

View File

@@ -6,10 +6,8 @@ from tests.utils import ClientWithCsrfChecks
HAS_CRYPTOGRAPHY = True
try:
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa
except ImportError:
HAS_CRYPTOGRAPHY = False
@@ -24,14 +22,14 @@ def make_key():
def derive_public_webhook_key(private_key):
"""Derive public """
"""Derive public"""
public_key = private_key.public_key()
public_bytes = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
public_bytes = b'\n'.join(public_bytes.splitlines()[1:-1])
return public_bytes.decode('utf-8')
public_bytes = b"\n".join(public_bytes.splitlines()[1:-1])
return public_bytes.decode("utf-8")
def sign(private_key, message):
@@ -47,11 +45,11 @@ class _ClientWithPostalSignature(ClientWithCsrfChecks):
self.private_key = private_key
def post(self, *args, **kwargs):
signature = b64encode(sign(self.private_key, kwargs['data'].encode('utf-8')))
kwargs.setdefault('HTTP_X_POSTAL_SIGNATURE', signature)
signature = b64encode(sign(self.private_key, kwargs["data"].encode("utf-8")))
kwargs.setdefault("HTTP_X_POSTAL_SIGNATURE", signature)
webhook_key = derive_public_webhook_key(self.private_key)
with override_settings(ANYMAIL={'POSTAL_WEBHOOK_KEY': webhook_key}):
with override_settings(ANYMAIL={"POSTAL_WEBHOOK_KEY": webhook_key}):
return super().post(*args, **kwargs)

View File

@@ -1,10 +1,10 @@
import base64
from unittest.mock import create_autospec, ANY
from unittest.mock import ANY, create_autospec
from django.test import override_settings, SimpleTestCase
from django.test import SimpleTestCase, override_settings
from anymail.exceptions import AnymailInsecureWebhookWarning
from anymail.signals import tracking, inbound
from anymail.signals import inbound, tracking
from .utils import AnymailTestMixin, ClientWithCsrfChecks
@@ -14,7 +14,7 @@ def event_handler(sender, event, esp_name, **kwargs):
pass
@override_settings(ANYMAIL={'WEBHOOK_SECRET': 'username:password'})
@override_settings(ANYMAIL={"WEBHOOK_SECRET": "username:password"})
class WebhookTestCase(AnymailTestMixin, SimpleTestCase):
"""Base for testing webhooks
@@ -38,18 +38,23 @@ class WebhookTestCase(AnymailTestMixin, SimpleTestCase):
inbound.connect(self.inbound_handler)
self.addCleanup(inbound.disconnect, self.inbound_handler)
def set_basic_auth(self, username='username', password='password'):
def set_basic_auth(self, username="username", password="password"):
"""Set basic auth for all subsequent test client requests"""
credentials = base64.b64encode("{}:{}".format(username, password).encode('utf-8')).decode('utf-8')
self.client.defaults['HTTP_AUTHORIZATION'] = "Basic {}".format(credentials)
credentials = base64.b64encode(
"{}:{}".format(username, password).encode("utf-8")
).decode("utf-8")
self.client.defaults["HTTP_AUTHORIZATION"] = "Basic {}".format(credentials)
def clear_basic_auth(self):
self.client.defaults.pop('HTTP_AUTHORIZATION', None)
self.client.defaults.pop("HTTP_AUTHORIZATION", None)
def assert_handler_called_once_with(self, mockfn, *expected_args, **expected_kwargs):
def assert_handler_called_once_with(
self, mockfn, *expected_args, **expected_kwargs
):
"""Verifies mockfn was called with expected_args and at least expected_kwargs.
Ignores *additional* actual kwargs (which might be added by Django signal dispatch).
Ignores *additional* actual kwargs
(which might be added by Django signal dispatch).
(This differs from mock.assert_called_once_with.)
Returns the actual kwargs.
@@ -80,16 +85,17 @@ class WebhookBasicAuthTestCase(WebhookTestCase):
- adding or overriding any tests as appropriate
"""
def __init__(self, methodName='runTest'):
def __init__(self, methodName="runTest"):
if self.__class__ is WebhookBasicAuthTestCase:
# don't run these tests on the abstract base implementation
methodName = 'runNoTestsInBaseClass'
methodName = "runNoTestsInBaseClass"
super().__init__(methodName)
def runNoTestsInBaseClass(self):
pass
should_warn_if_no_auth = True # subclass set False if other webhook verification used
#: subclass set False if other webhook verification used
should_warn_if_no_auth = True
def call_webhook(self):
# Concrete test cases should call a webhook via self.client.post,
@@ -111,7 +117,7 @@ class WebhookBasicAuthTestCase(WebhookTestCase):
self.assertEqual(response.status_code, 200)
def test_verifies_bad_auth(self):
self.set_basic_auth('baduser', 'wrongpassword')
self.set_basic_auth("baduser", "wrongpassword")
response = self.call_webhook()
self.assertEqual(response.status_code, 400)
@@ -120,17 +126,17 @@ class WebhookBasicAuthTestCase(WebhookTestCase):
response = self.call_webhook()
self.assertEqual(response.status_code, 400)
@override_settings(ANYMAIL={'WEBHOOK_SECRET': ['cred1:pass1', 'cred2:pass2']})
@override_settings(ANYMAIL={"WEBHOOK_SECRET": ["cred1:pass1", "cred2:pass2"]})
def test_supports_credential_rotation(self):
"""You can supply a list of basic auth credentials, and any is allowed"""
self.set_basic_auth('cred1', 'pass1')
self.set_basic_auth("cred1", "pass1")
response = self.call_webhook()
self.assertEqual(response.status_code, 200)
self.set_basic_auth('cred2', 'pass2')
self.set_basic_auth("cred2", "pass2")
response = self.call_webhook()
self.assertEqual(response.status_code, 200)
self.set_basic_auth('baduser', 'wrongpassword')
self.set_basic_auth("baduser", "wrongpassword")
response = self.call_webhook()
self.assertEqual(response.status_code, 400)