mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
353
tests/test_mailersend_inbound.py
Normal file
353
tests/test_mailersend_inbound.py
Normal file
@@ -0,0 +1,353 @@
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from textwrap import dedent
|
||||
from unittest.mock import ANY
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.test import override_settings, tag
|
||||
|
||||
from anymail.exceptions import AnymailConfigurationError
|
||||
from anymail.inbound import AnymailInboundMessage
|
||||
from anymail.signals import AnymailInboundEvent
|
||||
from anymail.webhooks.mailersend import MailerSendInboundWebhookView
|
||||
|
||||
from .test_mailersend_webhooks import (
|
||||
TEST_WEBHOOK_SIGNING_SECRET,
|
||||
MailerSendWebhookTestCase,
|
||||
)
|
||||
from .utils import sample_image_content
|
||||
from .webhook_cases import WebhookBasicAuthTestCase
|
||||
|
||||
|
||||
@tag("mailersend")
|
||||
@override_settings(ANYMAIL_MAILERSEND_INBOUND_SECRET=TEST_WEBHOOK_SIGNING_SECRET)
|
||||
class MailerSendInboundSecurityTestCase(
|
||||
MailerSendWebhookTestCase, WebhookBasicAuthTestCase
|
||||
):
|
||||
should_warn_if_no_auth = False # because we check webhook signature
|
||||
|
||||
def call_webhook(self):
|
||||
return self.client_post_signed(
|
||||
"/anymail/mailersend/inbound/",
|
||||
{"type": "inbound.message", "data": {"raw": "..."}},
|
||||
secret=TEST_WEBHOOK_SIGNING_SECRET,
|
||||
)
|
||||
|
||||
# Additional tests are in WebhookBasicAuthTestCase
|
||||
|
||||
def test_verifies_correct_signature(self):
|
||||
response = self.client_post_signed(
|
||||
"/anymail/mailersend/inbound/",
|
||||
{"type": "inbound.message", "data": {"raw": "..."}},
|
||||
secret=TEST_WEBHOOK_SIGNING_SECRET,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_verifies_missing_signature(self):
|
||||
response = self.client.post(
|
||||
"/anymail/mailersend/inbound/",
|
||||
content_type="application/json",
|
||||
data=json.dumps({"type": "inbound.message", "data": {"raw": "..."}}),
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_verifies_bad_signature(self):
|
||||
# This also verifies that the error log references the correct setting to check.
|
||||
with self.assertLogs() as logs:
|
||||
response = self.client_post_signed(
|
||||
"/anymail/mailersend/inbound/",
|
||||
{"type": "inbound.message", "data": {"raw": "..."}},
|
||||
secret="wrong signing key",
|
||||
)
|
||||
# SuspiciousOperation causes 400 response (even in test client):
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIn("check Anymail MAILERSEND_INBOUND_SECRET", logs.output[0])
|
||||
|
||||
|
||||
@tag("mailersend")
|
||||
class MailerSendInboundSettingsTestCase(MailerSendWebhookTestCase):
|
||||
def test_requires_inbound_secret(self):
|
||||
with self.assertRaisesMessage(
|
||||
ImproperlyConfigured, "MAILERSEND_INBOUND_SECRET"
|
||||
):
|
||||
self.client_post_signed(
|
||||
"/anymail/mailersend/inbound/",
|
||||
{
|
||||
"type": "inbound.message",
|
||||
"data": {"object": "message", "raw": "..."},
|
||||
},
|
||||
)
|
||||
|
||||
@override_settings(
|
||||
ANYMAIL={
|
||||
"MAILERSEND_INBOUND_SECRET": "inbound secret",
|
||||
"MAILERSEND_WEBHOOK_SIGNING_SECRET": "webhook secret",
|
||||
}
|
||||
)
|
||||
def test_webhook_signing_secret_is_different(self):
|
||||
response = self.client_post_signed(
|
||||
"/anymail/mailersend/inbound/",
|
||||
{
|
||||
"type": "inbound.message",
|
||||
"data": {"object": "message", "raw": "..."},
|
||||
},
|
||||
secret="inbound secret",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@override_settings(ANYMAIL_MAILERSEND_INBOUND_SECRET="settings secret")
|
||||
def test_inbound_secret_view_params(self):
|
||||
"""Webhook signing secret can be provided as a view param"""
|
||||
view = MailerSendInboundWebhookView.as_view(inbound_secret="view-level secret")
|
||||
view_instance = view.view_class(**view.view_initkwargs)
|
||||
self.assertEqual(view_instance.signing_secret, b"view-level secret")
|
||||
|
||||
|
||||
@tag("mailersend")
|
||||
@override_settings(ANYMAIL_MAILERSEND_INBOUND_SECRET=TEST_WEBHOOK_SIGNING_SECRET)
|
||||
class MailerSendInboundTestCase(MailerSendWebhookTestCase):
|
||||
# Since Anymail just parses the raw MIME message through the Python email
|
||||
# package, there aren't really a lot of different cases to test here.
|
||||
# (We don't need to re-test the whole email.parser.)
|
||||
|
||||
def test_inbound(self):
|
||||
# This is an actual (sanitized) inbound payload received from MailerSend:
|
||||
raw_event = {
|
||||
"type": "inbound.message",
|
||||
"inbound_id": "[inbound-route-id-redacted]",
|
||||
"url": "https://test.anymail.dev/anymail/mailersend/inbound/",
|
||||
"created_at": "2023-03-04T02:22:16.417935Z",
|
||||
"data": {
|
||||
"object": "message",
|
||||
"id": "6402ab57f79d39d7e10f2523",
|
||||
"recipients": {
|
||||
"rcptTo": [{"email": "envelope-recipient@example.com"}],
|
||||
"to": {
|
||||
"raw": "Recipient <to@example.com>",
|
||||
"data": [{"email": "to@example.com", "name": "Recipient"}],
|
||||
},
|
||||
},
|
||||
"from": {
|
||||
"email": "sender@example.org",
|
||||
"name": "Sender Name",
|
||||
"raw": "Sender Name <sender@example.org>",
|
||||
},
|
||||
"sender": {"email": "envelope-sender@example.org"},
|
||||
"subject": "Testing inbound \ud83c\udf0e",
|
||||
"date": "Fri, 3 Mar 2023 18:22:03 -0800",
|
||||
"headers": {
|
||||
"X-Envelope-From": "<envelope-sender@example.org>",
|
||||
# Multiple-instance headers appear as arrays:
|
||||
"Received": [
|
||||
"from example.org (mail.example.org [10.10.10.10])\r\n"
|
||||
" by inbound.mailersend.net with ESMTPS id ...\r\n"
|
||||
" Sat, 04 Mar 2023 02:22:15 +0000 (UTC)",
|
||||
"by mail.example.org with SMTP id ...\r\n"
|
||||
" for <envelope-recipient@example.com>;\r\n"
|
||||
" Fri, 03 Mar 2023 18:22:15 -0800 (PST)",
|
||||
],
|
||||
"DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed; ...",
|
||||
"MIME-Version": "1.0",
|
||||
"From": "Sender Name <sender@example.org>",
|
||||
"Date": "Fri, 3 Mar 2023 18:22:03 -0800",
|
||||
"Message-ID": "<AzjSdSHsmvXUeZGTPQ@mail.example.org>",
|
||||
"Subject": "=?UTF-8?Q?Testing_inbound_=F0=9F=8C=8E?=",
|
||||
"To": "Recipient <to@example.com>",
|
||||
"Content-Type": 'multipart/mixed; boundary="000000000000e5575c05f609bab6"',
|
||||
},
|
||||
"text": "This is a *test*!\r\n\r\n[image: sample_image.png]\r\n",
|
||||
"html": (
|
||||
"<p>This is a <b>test</b>!</p>"
|
||||
'<img src="cid:ii_letc8ro50" alt="sample_image.png">'
|
||||
),
|
||||
"raw": dedent(
|
||||
"""\
|
||||
X-Envelope-From: <envelope-sender@example.org>
|
||||
Received: from example.org (mail.example.org [10.10.10.10])
|
||||
by inbound.mailersend.net with ESMTPS id ...
|
||||
Sat, 04 Mar 2023 02:22:15 +0000 (UTC)
|
||||
Received: by mail.example.org with SMTP id ...
|
||||
for <envelope-recipient@example.com>;
|
||||
Fri, 03 Mar 2023 18:22:15 -0800 (PST)
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; ...
|
||||
MIME-Version: 1.0
|
||||
From: Sender Name <sender@example.org>
|
||||
Date: Fri, 3 Mar 2023 18:22:03 -0800
|
||||
Message-ID: <AzjSdSHsmvXUeZGTPQ@mail.example.org>
|
||||
Subject: =?UTF-8?Q?Testing_inbound_=F0=9F=8C=8E?=
|
||||
To: Recipient <to@example.com>
|
||||
Content-Type: multipart/mixed; boundary="000000000000e5575c05f609bab6"
|
||||
|
||||
--000000000000e5575c05f609bab6
|
||||
Content-Type: multipart/related; boundary="000000000000e5575b05f609bab5"
|
||||
|
||||
--000000000000e5575b05f609bab5
|
||||
Content-Type: multipart/alternative; boundary="000000000000e5575a05f609bab4"
|
||||
|
||||
--000000000000e5575a05f609bab4
|
||||
Content-Type: text/plain; charset="UTF-8"
|
||||
|
||||
This is a *test*!
|
||||
|
||||
[image: sample_image.png]
|
||||
|
||||
--000000000000e5575a05f609bab4
|
||||
Content-Type: text/html; charset="UTF-8"
|
||||
|
||||
<p>This is a <b>test</b>!</p>
|
||||
<img src="cid:ii_letc8ro50" alt="sample_image.png">
|
||||
|
||||
--000000000000e5575a05f609bab4--
|
||||
--000000000000e5575b05f609bab5
|
||||
Content-Type: image/png; name="sample_image.png"
|
||||
Content-Disposition: inline; filename="sample_image.png"
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-ID: <ii_letc8ro50>
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz
|
||||
AAALEgAACxIB0t1+/AAAABR0RVh0Q3JlYXRpb24gVGltZQAzLzEvMTNoZNRjAAAAHHRFWHRTb2Z0
|
||||
d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M1cbXjNgAAAZ1JREFUWIXtl7FKA0EQhr+TgIFgo5BXyBUp
|
||||
fIGksLawUNAXWFFfwCJgBAtfIJFMLXgQn8BSwdpCiPcKAdOIoI2x2Dmyd7kYwXhp9odluX/uZv6d
|
||||
nZu7DXowxiKZi0IAUHKCvxcsoAIEpST4IawVGb0Hb0BlpcigefACvAAvwAsoTTGGlwwzBAyivLUP
|
||||
EZrOM10AhGOH2wWugVVlHoAdhJHrPC8DNR0JGsAAQ9mxNzBOMNjS4Qrq69U5EKmf12ywWVsQI4QI
|
||||
IbCn3Gnmnk7uk1bokfooI7QRDlQIGCdzPwiYh0idtXNs2zq3UqwVEiDcu/R0DVjUnFpItuPSscfA
|
||||
FXCGSfEAdZ2fVeQ68OjYWwi3ycVvMhABGwgfKXZScHeZ+4c6VzN8FbuYukvOykCs+z8PJ0xqIXYE
|
||||
d4ALoKlVH2IIgUHWwd/6gNAFPjPcCPvKNTDcYAj1lXzKc7GIRrSZI6yJzcQ+dtV9bD+IkHThBj34
|
||||
4j9/yYxupaQbXPJLNqsGFgeZ6qwpLP1b4AV4AV5AoKfjpR5OwR6VKwULCAC+AQV4W9Ps4uZQAAAA
|
||||
AElFTkSuQmCC
|
||||
--000000000000e5575b05f609bab5--
|
||||
--000000000000e5575c05f609bab6
|
||||
Content-Type: text/csv; charset="US-ASCII"; name="sample_data.csv"
|
||||
Content-Disposition: attachment; filename="sample_data.csv"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
Product,Price
|
||||
Widget,33.20
|
||||
--000000000000e5575c05f609bab6--"""
|
||||
).replace("\n", "\r\n"),
|
||||
"attachments": [
|
||||
{
|
||||
"file_name": "sample_image.png",
|
||||
"content_type": "image/png",
|
||||
"content_disposition": "inline",
|
||||
"content_id": "ii_letc8ro50",
|
||||
"size": 579,
|
||||
"content": (
|
||||
"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABHNCSVQICAgIfAhki"
|
||||
"AAAAAlwSFlzAAALEgAACxIB0t1+/AAAABR0RVh0Q3JlYXRpb24gVGltZQAzLzEvMT"
|
||||
"NoZNRjAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M1cbXjNgAAAZ1"
|
||||
"JREFUWIXtl7FKA0EQhr+TgIFgo5BXyBUpfIGksLawUNAXWFFfwCJgBAtfIJFMLXgQ"
|
||||
"n8BSwdpCiPcKAdOIoI2x2Dmyd7kYwXhp9odluX/uZv6dnZu7DXowxiKZi0IAUHKCv"
|
||||
"xcsoAIEpST4IawVGb0Hb0BlpcigefACvAAvwAsoTTGGlwwzBAyivLUPEZrOM10AhG"
|
||||
"OH2wWugVVlHoAdhJHrPC8DNR0JGsAAQ9mxNzBOMNjS4Qrq69U5EKmf12ywWVsQI4Q"
|
||||
"IIbCn3Gnmnk7uk1bokfooI7QRDlQIGCdzPwiYh0idtXNs2zq3UqwVEiDcu/R0DVjU"
|
||||
"nFpItuPSscfAFXCGSfEAdZ2fVeQ68OjYWwi3ycVvMhABGwgfKXZScHeZ+4c6VzN8F"
|
||||
"buYukvOykCs+z8PJ0xqIXYEd4ALoKlVH2IIgUHWwd/6gNAFPjPcCPvKNTDcYAj1lX"
|
||||
"zKc7GIRrSZI6yJzcQ+dtV9bD+IkHThBj344j9/yYxupaQbXPJLNqsGFgeZ6qwpLP1"
|
||||
"b4AV4AV5AoKfjpR5OwR6VKwULCAC+AQV4W9Ps4uZQAAAAAElFTkSuQmCC"
|
||||
),
|
||||
},
|
||||
{
|
||||
"file_name": "sample_data.csv",
|
||||
"content_type": "text/csv",
|
||||
"content_disposition": "attachment",
|
||||
"size": 26,
|
||||
"content": "UHJvZHVjdCxQcmljZQpXaWRnZXQsMzMuMjA=",
|
||||
},
|
||||
],
|
||||
"spf_check": {"code": "+", "value": None},
|
||||
"dkim_check": False,
|
||||
"created_at": "2023-03-04T02:22:15.525000Z",
|
||||
},
|
||||
}
|
||||
response = self.client_post_signed("/anymail/mailersend/inbound/", raw_event)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
kwargs = self.assert_handler_called_once_with(
|
||||
self.inbound_handler,
|
||||
sender=MailerSendInboundWebhookView,
|
||||
event=ANY,
|
||||
esp_name="MailerSend",
|
||||
)
|
||||
# AnymailInboundEvent
|
||||
event = kwargs["event"]
|
||||
self.assertIsInstance(event, AnymailInboundEvent)
|
||||
self.assertEqual(event.event_type, "inbound")
|
||||
self.assertEqual(
|
||||
event.timestamp,
|
||||
# "2023-03-04T02:22:15.525000Z"
|
||||
datetime(2023, 3, 4, 2, 22, 15, microsecond=525000, tzinfo=timezone.utc),
|
||||
)
|
||||
self.assertEqual(event.event_id, "6402ab57f79d39d7e10f2523")
|
||||
self.assertIsInstance(event.message, AnymailInboundMessage)
|
||||
|
||||
# (The raw_event subject contains a "\N{EARTH GLOBE AMERICAS}" (🌎)
|
||||
# character in the escaped form "\ud83c\udf0e", which won't compare equal
|
||||
# until unescaped. Passing through json dumps/loads resolves the escapes.)
|
||||
self.assertEqual(event.esp_event, json.loads(json.dumps(raw_event)))
|
||||
|
||||
# AnymailInboundMessage - convenience properties
|
||||
message = event.message
|
||||
|
||||
self.assertEqual(message.from_email.display_name, "Sender Name")
|
||||
self.assertEqual(message.from_email.addr_spec, "sender@example.org")
|
||||
self.assertEqual(str(message.to[0]), "Recipient <to@example.com>")
|
||||
self.assertEqual(message.subject, "Testing inbound 🌎")
|
||||
self.assertEqual(message.date.isoformat(" "), "2023-03-03 18:22:03-08:00")
|
||||
self.assertEqual(
|
||||
message.text, "This is a *test*!\r\n\r\n[image: sample_image.png]\r\n"
|
||||
)
|
||||
self.assertHTMLEqual(
|
||||
message.html,
|
||||
"<p>This is a <b>test</b>!</p>"
|
||||
'<img src="cid:ii_letc8ro50" alt="sample_image.png">',
|
||||
)
|
||||
|
||||
self.assertEqual(message.envelope_sender, "envelope-sender@example.org")
|
||||
self.assertEqual(message.envelope_recipient, "envelope-recipient@example.com")
|
||||
|
||||
# MailerSend inbound doesn't provide these:
|
||||
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"], "<AzjSdSHsmvXUeZGTPQ@mail.example.org>")
|
||||
self.assertEqual(
|
||||
message.get_all("Received"),
|
||||
[
|
||||
"from example.org (mail.example.org [10.10.10.10]) by inbound.mailersend.net"
|
||||
" with ESMTPS id ... Sat, 04 Mar 2023 02:22:15 +0000 (UTC)",
|
||||
"by mail.example.org with SMTP id ... for <envelope-recipient@example.com>;"
|
||||
" Fri, 03 Mar 2023 18:22:15 -0800 (PST)",
|
||||
],
|
||||
)
|
||||
|
||||
inlines = message.inline_attachments
|
||||
self.assertEqual(len(inlines), 1)
|
||||
inline = inlines["ii_letc8ro50"]
|
||||
self.assertEqual(inline.get_filename(), "sample_image.png")
|
||||
self.assertEqual(inline.get_content_type(), "image/png")
|
||||
self.assertEqual(inline.get_content_bytes(), sample_image_content())
|
||||
|
||||
attachments = message.attachments
|
||||
self.assertEqual(len(attachments), 1)
|
||||
self.assertEqual(attachments[0].get_filename(), "sample_data.csv")
|
||||
self.assertEqual(attachments[0].get_content_type(), "text/csv")
|
||||
self.assertEqual(
|
||||
attachments[0].get_content_text(), "Product,Price\r\nWidget,33.20"
|
||||
)
|
||||
|
||||
def test_misconfigured_inbound(self):
|
||||
errmsg = (
|
||||
"You seem to have set MailerSend's *activity.sent* webhook"
|
||||
" to Anymail's MailerSend *inbound* webhook URL."
|
||||
)
|
||||
with self.assertRaisesMessage(AnymailConfigurationError, errmsg):
|
||||
self.client_post_signed(
|
||||
"/anymail/mailersend/inbound/",
|
||||
{
|
||||
"type": "activity.sent",
|
||||
"data": {"object": "activity", "type": "sent"},
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user