mirror of
https://github.com/pacnpal/django-anymail.git
synced 2026-02-05 03:55:20 -05:00
Reformat code with automated tools
Apply standardized code style
This commit is contained in:
@@ -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
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"}),
|
||||
)
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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
@@ -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"}),
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")),
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user