From ab47bbca4f78b4a7f86b0a4da9e9fd68b5aaaa52 Mon Sep 17 00:00:00 2001 From: Conor Heine Date: Sun, 17 Nov 2024 22:10:45 +0000 Subject: [PATCH] Mailtrap: add webhook tests and update test configs --- .github/workflows/integration-test.yml | 1 + README.rst | 1 + anymail/backends/mailtrap.py | 2 + anymail/webhooks/mailtrap.py | 5 +- pyproject.toml | 1 + tests/test_mailtrap_webhooks.py | 375 +++++++++++++++++++++++++ tox.ini | 1 + 7 files changed, 384 insertions(+), 2 deletions(-) create mode 100644 tests/test_mailtrap_webhooks.py diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index e0fd8fc9..4bd67366 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -44,6 +44,7 @@ jobs: - { tox: django41-py310-mailersend, python: "3.12" } - { tox: django41-py310-mailgun, python: "3.12" } - { tox: django41-py310-mailjet, python: "3.12" } + - { tox: django41-py310-mailtrap, python: "3.12" } - { tox: django41-py310-mandrill, python: "3.12" } - { tox: django41-py310-postal, python: "3.12" } - { tox: django41-py310-postmark, python: "3.12" } diff --git a/README.rst b/README.rst index 8daf0ee0..452bf4bb 100644 --- a/README.rst +++ b/README.rst @@ -31,6 +31,7 @@ Anymail currently supports these ESPs: * **MailerSend** * **Mailgun** (Sinch transactional email) * **Mailjet** (Sinch transactional email) +* **Mailtrap** * **Mandrill** (MailChimp transactional email) * **Postal** (self-hosted ESP) * **Postmark** (ActiveCampaign transactional email) diff --git a/anymail/backends/mailtrap.py b/anymail/backends/mailtrap.py index f381b6b7..a6b36e81 100644 --- a/anymail/backends/mailtrap.py +++ b/anymail/backends/mailtrap.py @@ -58,7 +58,9 @@ def __init__( "Api-Token": backend.api_token, "Content-Type": "application/json", "Accept": "application/json", + "User-Agent": "django-anymail (https://github.com/anymail/django-anymail)", } + # Yes, the parent sets this, but setting it here, too, gives type hints self.backend = backend self.metadata = None super().__init__( diff --git a/anymail/webhooks/mailtrap.py b/anymail/webhooks/mailtrap.py index d554cbe4..10414d1b 100644 --- a/anymail/webhooks/mailtrap.py +++ b/anymail/webhooks/mailtrap.py @@ -59,7 +59,6 @@ def parse_events(self, request): "click": EventType.CLICKED, "bounce": EventType.BOUNCED, "soft bounce": EventType.DEFERRED, - "blocked": EventType.REJECTED, "spam": EventType.COMPLAINED, "unsubscribe": EventType.UNSUBSCRIBED, "reject": EventType.REJECTED, @@ -73,6 +72,8 @@ def parse_events(self, request): "spam": RejectReason.SPAM, "unsubscribe": RejectReason.UNSUBSCRIBED, "reject": RejectReason.BLOCKED, + "suspension": RejectReason.OTHER, + "soft bounce": RejectReason.OTHER, } def esp_to_anymail_event(self, esp_event: MailtrapEvent): @@ -91,7 +92,7 @@ def esp_to_anymail_event(self, esp_event: MailtrapEvent): event_id=esp_event.get("event_id", None), recipient=esp_event.get("email", None), reject_reason=reject_reason, - mta_response=esp_event.get("response_code", None), + mta_response=esp_event.get("response", None), tags=tags, metadata=custom_variables, click_url=esp_event.get("url", None), diff --git a/pyproject.toml b/pyproject.toml index 76523c5d..9b987ac4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ keywords = [ "Brevo", "SendinBlue", "MailerSend", "Mailgun", "Mailjet", "Sinch", + "Mailtrap", "Mandrill", "MailChimp", "Postal", "Postmark", "ActiveCampaign", diff --git a/tests/test_mailtrap_webhooks.py b/tests/test_mailtrap_webhooks.py new file mode 100644 index 00000000..b209523d --- /dev/null +++ b/tests/test_mailtrap_webhooks.py @@ -0,0 +1,375 @@ +import json +from datetime import datetime, timezone +from unittest.mock import ANY + +from django.test import tag + +from anymail.signals import AnymailTrackingEvent +from anymail.webhooks.mailtrap import MailtrapTrackingWebhookView + +from .webhook_cases import WebhookBasicAuthTestCase, WebhookTestCase + + +@tag("mailtrap") +class MailtrapWebhookSecurityTestCase(WebhookBasicAuthTestCase): + def call_webhook(self): + return self.client.post( + "/anymail/mailtrap/tracking/", + content_type="application/json", + data=json.dumps({}), + ) + + # Actual tests are in WebhookBasicAuthTestCase + + +@tag("mailtrap") +class MailtrapDeliveryTestCase(WebhookTestCase): + def test_sent_event(self): + payload = { + "events": [ + { + "event": "delivery", + "timestamp": 1498093527, + "sending_stream": "transactional", + "category": "password-reset", + "custom_variables": {"variable_a": "value", "variable_b": "value2"}, + "message_id": "1df37d17-0286-4d8b-8edf-bc4ec5be86e6", + "email": "receiver@example.com", + "event_id": "bede7236-2284-43d6-a953-1fdcafd0fdbc", + }, + ] + } + response = self.client.post( + "/anymail/mailtrap/tracking/", + content_type="application/json", + data=json.dumps(payload), + ) + self.assertEqual(response.status_code, 200) + kwargs = self.assert_handler_called_once_with( + self.tracking_handler, + sender=MailtrapTrackingWebhookView, + event=ANY, + esp_name="Mailtrap", + ) + 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.esp_event, payload["events"][0]) + self.assertEqual( + event.mta_response, + None, + ) + self.assertEqual(event.message_id, "1df37d17-0286-4d8b-8edf-bc4ec5be86e6") + self.assertEqual(event.recipient, "receiver@example.com") + self.assertEqual(event.tags, ["password-reset"]) + self.assertEqual( + event.metadata, {"variable_a": "value", "variable_b": "value2"} + ) + + def test_open_event(self): + payload = { + "events": [ + { + "event": "open", + "timestamp": 1498093527, + "sending_stream": "transactional", + "message_id": "1df37d17-0286-4d8b-8edf-bc4ec5be86e6", + "email": "receiver@example.com", + "event_id": "bede7236-2284-43d6-a953-1fdcafd0fdbc", + "ip": "192.168.1.42", + "user_agent": "Mozilla/5.0 (via ggpht.com GoogleImageProxy)", + }, + ] + } + response = self.client.post( + "/anymail/mailtrap/tracking/", + content_type="application/json", + data=json.dumps(payload), + ) + self.assertEqual(response.status_code, 200) + kwargs = self.assert_handler_called_once_with( + self.tracking_handler, + sender=MailtrapTrackingWebhookView, + event=ANY, + esp_name="Mailtrap", + ) + event = kwargs["event"] + self.assertEqual(event.event_type, "opened") + self.assertEqual(event.message_id, "1df37d17-0286-4d8b-8edf-bc4ec5be86e6") + self.assertEqual(event.recipient, "receiver@example.com") + 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): + payload = { + "events": [ + { + "event": "click", + "timestamp": 1498093527, + "sending_stream": "transactional", + "message_id": "1df37d17-0286-4d8b-8edf-bc4ec5be86e6", + "email": "receiver@example.com", + "event_id": "bede7236-2284-43d6-a953-1fdcafd0fdbc", + "category": "custom-value", + "custom_variables": {"testing": True}, + "ip": "192.168.1.42", + "user_agent": ( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) Chrome/58.0.3029.110)" + ), + "url": "http://example.com/anymail", + }, + ] + } + response = self.client.post( + "/anymail/mailtrap/tracking/", + content_type="application/json", + data=json.dumps(payload), + ) + self.assertEqual(response.status_code, 200) + kwargs = self.assert_handler_called_once_with( + self.tracking_handler, + sender=MailtrapTrackingWebhookView, + event=ANY, + esp_name="Mailtrap", + ) + event = kwargs["event"] + self.assertEqual(event.event_type, "clicked") + self.assertEqual(event.message_id, "1df37d17-0286-4d8b-8edf-bc4ec5be86e6") + self.assertEqual(event.recipient, "receiver@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.click_url, "http://example.com/anymail") + self.assertEqual(event.tags, ["custom-value"]) + self.assertEqual(event.metadata, {"testing": True}) + + def test_bounce_event(self): + payload = { + "events": [ + { + "event": "bounce", + "timestamp": 1498093527, + "sending_stream": "transactional", + "message_id": "1df37d17-0286-4d8b-8edf-bc4ec5be86e6", + "email": "invalid@example.com", + "event_id": "bede7236-2284-43d6-a953-1fdcafd0fdbc", + "category": "custom-value", + "custom_variables": {"testing": True}, + "response": ( + "bounced (550 5.1.1 The email account that you tried to reach " + "does not exist. a67bc12345def.22 - gsmtp)" + ), + "response_code": 550, + "bounce_category": "hard", + }, + ] + } + response = self.client.post( + "/anymail/mailtrap/tracking/", + content_type="application/json", + data=json.dumps(payload), + ) + self.assertEqual(response.status_code, 200) + kwargs = self.assert_handler_called_once_with( + self.tracking_handler, + sender=MailtrapTrackingWebhookView, + event=ANY, + esp_name="Mailtrap", + ) + event = kwargs["event"] + self.assertEqual(event.event_type, "bounced") + self.assertEqual(event.message_id, "1df37d17-0286-4d8b-8edf-bc4ec5be86e6") + self.assertEqual(event.recipient, "invalid@example.com") + self.assertEqual(event.reject_reason, "bounced") + self.assertEqual( + event.mta_response, + ( + "bounced (550 5.1.1 The email account that you tried to reach does not exist. " + "a67bc12345def.22 - gsmtp)" + ), + ) + + def test_soft_bounce_event(self): + payload = { + "events": [ + { + "event": "soft bounce", + "timestamp": 1498093527, + "sending_stream": "transactional", + "message_id": "1df37d17-0286-4d8b-8edf-bc4ec5be86e6", + "email": "receiver@example.com", + "event_id": "bede7236-2284-43d6-a953-1fdcafd0fdbc", + "response": ( + "soft bounce (450 4.2.0 The email account that you tried to reach is " + "temporarily unavailable. a67bc12345def.22 - gsmtp)" + ), + "response_code": 450, + "bounce_category": "unavailable", + }, + ] + } + response = self.client.post( + "/anymail/mailtrap/tracking/", + content_type="application/json", + data=json.dumps(payload), + ) + self.assertEqual(response.status_code, 200) + kwargs = self.assert_handler_called_once_with( + self.tracking_handler, + sender=MailtrapTrackingWebhookView, + event=ANY, + esp_name="Mailtrap", + ) + event = kwargs["event"] + self.assertEqual(event.event_type, "deferred") + self.assertEqual(event.message_id, "1df37d17-0286-4d8b-8edf-bc4ec5be86e6") + self.assertEqual(event.recipient, "receiver@example.com") + self.assertEqual(event.reject_reason, "other") + self.assertEqual( + event.mta_response, + ( + "soft bounce (450 4.2.0 The email account that you tried to reach is " + "temporarily unavailable. a67bc12345def.22 - gsmtp)" + ), + ) + + def test_spam_event(self): + payload = { + "events": [ + { + "event": "spam", + "timestamp": 1498093527, + "sending_stream": "transactional", + "message_id": "1df37d17-0286-4d8b-8edf-bc4ec5be86e6", + "email": "receiver@example.com", + "event_id": "bede7236-2284-43d6-a953-1fdcafd0fdbc", + }, + ] + } + response = self.client.post( + "/anymail/mailtrap/tracking/", + content_type="application/json", + data=json.dumps(payload), + ) + self.assertEqual(response.status_code, 200) + kwargs = self.assert_handler_called_once_with( + self.tracking_handler, + sender=MailtrapTrackingWebhookView, + event=ANY, + esp_name="Mailtrap", + ) + event = kwargs["event"] + self.assertEqual(event.event_type, "complained") + self.assertEqual(event.message_id, "1df37d17-0286-4d8b-8edf-bc4ec5be86e6") + self.assertEqual(event.recipient, "receiver@example.com") + self.assertEqual(event.reject_reason, "spam") + + def test_unsubscribe_event(self): + payload = { + "events": [ + { + "event": "unsubscribe", + "timestamp": 1498093527, + "sending_stream": "transactional", + "message_id": "1df37d17-0286-4d8b-8edf-bc4ec5be86e6", + "email": "receiver@example.com", + "event_id": "bede7236-2284-43d6-a953-1fdcafd0fdbc", + "ip": "192.168.1.42", + "user_agent": ( + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) Chrome/58.0.3029.110)" + ), + }, + ] + } + response = self.client.post( + "/anymail/mailtrap/tracking/", + content_type="application/json", + data=json.dumps(payload), + ) + self.assertEqual(response.status_code, 200) + kwargs = self.assert_handler_called_once_with( + self.tracking_handler, + sender=MailtrapTrackingWebhookView, + event=ANY, + esp_name="Mailtrap", + ) + event = kwargs["event"] + self.assertEqual(event.event_type, "unsubscribed") + self.assertEqual(event.message_id, "1df37d17-0286-4d8b-8edf-bc4ec5be86e6") + self.assertEqual(event.recipient, "receiver@example.com") + self.assertEqual(event.reject_reason, "unsubscribed") + self.assertEqual( + event.user_agent, + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) Chrome/58.0.3029.110)", + ) + + def test_suspension_event(self): + payload = { + "events": [ + { + "event": "suspension", + "timestamp": 1498093527, + "sending_stream": "transactional", + "message_id": "1df37d17-0286-4d8b-8edf-bc4ec5be86e6", + "email": "receiver@example.com", + "event_id": "bede7236-2284-43d6-a953-1fdcafd0fdbc", + "reason": "other", + }, + ] + } + response = self.client.post( + "/anymail/mailtrap/tracking/", + content_type="application/json", + data=json.dumps(payload), + ) + self.assertEqual(response.status_code, 200) + kwargs = self.assert_handler_called_once_with( + self.tracking_handler, + sender=MailtrapTrackingWebhookView, + event=ANY, + esp_name="Mailtrap", + ) + event = kwargs["event"] + self.assertEqual(event.event_type, "deferred") + self.assertEqual(event.message_id, "1df37d17-0286-4d8b-8edf-bc4ec5be86e6") + self.assertEqual(event.recipient, "receiver@example.com") + self.assertEqual(event.reject_reason, "other") + + def test_reject_event(self): + payload = { + "events": [ + { + "event": "reject", + "timestamp": 1498093527, + "sending_stream": "transactional", + "message_id": "1df37d17-0286-4d8b-8edf-bc4ec5be86e6", + "email": "receiver@example.com", + "event_id": "bede7236-2284-43d6-a953-1fdcafd0fdbc", + "reason": "unknown", + }, + ] + } + response = self.client.post( + "/anymail/mailtrap/tracking/", + content_type="application/json", + data=json.dumps(payload), + ) + self.assertEqual(response.status_code, 200) + kwargs = self.assert_handler_called_once_with( + self.tracking_handler, + sender=MailtrapTrackingWebhookView, + event=ANY, + esp_name="Mailtrap", + ) + event = kwargs["event"] + self.assertEqual(event.event_type, "rejected") + self.assertEqual(event.message_id, "1df37d17-0286-4d8b-8edf-bc4ec5be86e6") + self.assertEqual(event.recipient, "receiver@example.com") + self.assertEqual(event.reject_reason, "blocked") diff --git a/tox.ini b/tox.ini index f37f70f9..4c352b24 100644 --- a/tox.ini +++ b/tox.ini @@ -58,6 +58,7 @@ setenv = mailersend: ANYMAIL_ONLY_TEST=mailersend mailgun: ANYMAIL_ONLY_TEST=mailgun mailjet: ANYMAIL_ONLY_TEST=mailjet + mailtrap: ANYMAIL_ONLY_TEST=mailtrap mandrill: ANYMAIL_ONLY_TEST=mandrill postal: ANYMAIL_ONLY_TEST=postal postmark: ANYMAIL_ONLY_TEST=postmark