diff --git a/anymail/webhooks/postmark.py b/anymail/webhooks/postmark.py index ec1c8e7..f387514 100644 --- a/anymail/webhooks/postmark.py +++ b/anymail/webhooks/postmark.py @@ -27,8 +27,18 @@ class PostmarkTrackingWebhookView(PostmarkBaseWebhookView): signal = tracking + event_record_types = { + # Map Postmark event RecordType --> Anymail normalized event type + 'Bounce': EventType.BOUNCED, # but check Type field for further info (below) + 'Click': EventType.CLICKED, + 'Delivery': EventType.DELIVERED, + 'Open': EventType.OPENED, + 'SpamComplaint': EventType.COMPLAINED, + 'Inbound': EventType.INBOUND, # future, probably + } + event_types = { - # Map Postmark event type: Anymail normalized (event type, reject reason) + # Map Postmark bounce/spam event Type --> Anymail normalized (event type, reject reason) 'HardBounce': (EventType.BOUNCED, RejectReason.BOUNCED), 'Transient': (EventType.DEFERRED, None), 'Unsubscribe': (EventType.UNSUBSCRIBED, RejectReason.UNSUBSCRIBED), @@ -51,31 +61,32 @@ class PostmarkTrackingWebhookView(PostmarkBaseWebhookView): 'InboundError': (EventType.INBOUND_FAILED, None), 'DMARCPolicy': (EventType.REJECTED, RejectReason.BLOCKED), 'TemplateRenderingFailed': (EventType.FAILED, None), - # DELIVERED doesn't have a Type field; detected separately below - # CLICKED doesn't have a Type field; detected separately below - # OPENED doesn't have a Type field; detected separately below - # INBOUND doesn't have a Type field; should come in through different webhook } def esp_to_anymail_event(self, esp_event): reject_reason = None try: - esp_type = esp_event['Type'] - event_type, reject_reason = self.event_types.get(esp_type, (EventType.UNKNOWN, None)) + esp_record_type = esp_event["RecordType"] except KeyError: - if 'FirstOpen' in esp_event: - event_type = EventType.OPENED - elif 'OriginalLink' in esp_event: - event_type = EventType.CLICKED - elif 'DeliveredAt' in esp_event: - event_type = EventType.DELIVERED - elif 'From' in esp_event: + if 'FromFull' in esp_event: # This is an inbound event - raise AnymailConfigurationError( - "You seem to have set Postmark's *inbound* webhook URL " - "to Anymail's Postmark *tracking* webhook URL.") + event_type = EventType.INBOUND else: event_type = EventType.UNKNOWN + else: + event_type = self.event_record_types.get(esp_record_type, EventType.UNKNOWN) + + if event_type == EventType.INBOUND: + raise AnymailConfigurationError( + "You seem to have set Postmark's *inbound* webhook " + "to Anymail's Postmark *tracking* webhook URL.") + + if event_type in (EventType.BOUNCED, EventType.COMPLAINED): + # additional info is in the Type field + try: + event_type, reject_reason = self.event_types[esp_event['Type']] + except KeyError: + pass recipient = getfirst(esp_event, ['Email', 'Recipient'], None) # Email for bounce; Recipient for open @@ -118,6 +129,11 @@ class PostmarkInboundWebhookView(PostmarkBaseWebhookView): signal = inbound def esp_to_anymail_event(self, esp_event): + if esp_event.get("RecordType", "Inbound") != "Inbound": + raise AnymailConfigurationError( + "You seem to have set Postmark's *%s* webhook " + "to Anymail's Postmark *inbound* webhook URL." % esp_event["RecordType"]) + attachments = [ AnymailInboundMessage.construct_attachment( content_type=attachment["ContentType"], diff --git a/tests/test_postmark_inbound.py b/tests/test_postmark_inbound.py index af77615..ed4fba0 100644 --- a/tests/test_postmark_inbound.py +++ b/tests/test_postmark_inbound.py @@ -3,6 +3,7 @@ from base64 import b64encode from mock import ANY +from anymail.exceptions import AnymailConfigurationError from anymail.inbound import AnymailInboundMessage from anymail.signals import AnymailInboundEvent from anymail.webhooks.postmark import PostmarkInboundWebhookView @@ -222,3 +223,9 @@ class PostmarkInboundTestCase(WebhookTestCase): "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." + with self.assertRaisesMessage(AnymailConfigurationError, errmsg): + self.client.post('/anymail/postmark/inbound/', content_type='application/json', + data=json.dumps({"RecordType": "Delivery"})) diff --git a/tests/test_postmark_webhooks.py b/tests/test_postmark_webhooks.py index 3937eba..500bf7b 100644 --- a/tests/test_postmark_webhooks.py +++ b/tests/test_postmark_webhooks.py @@ -4,6 +4,7 @@ from datetime import datetime from django.utils.timezone import get_fixed_timezone, utc from mock import ANY +from anymail.exceptions import AnymailConfigurationError from anymail.signals import AnymailTrackingEvent from anymail.webhooks.postmark import PostmarkTrackingWebhookView from .webhook_cases import WebhookBasicAuthTestsMixin, WebhookTestCase @@ -202,3 +203,13 @@ class PostmarkDeliveryTestCase(WebhookTestCase): self.assertEqual(event.reject_reason, "spam") self.assertEqual(event.description, "") 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." + with self.assertRaisesMessage(AnymailConfigurationError, errmsg): + 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"}))