From 0ded9f752990105e936e88e212caf0ab44df1dd8 Mon Sep 17 00:00:00 2001 From: medmunds Date: Fri, 6 Apr 2018 15:03:36 -0700 Subject: [PATCH] Postmark: Use new RecordType field to identify event types Simplify Postmark tracking webhook code by using new "RecordType" field introduced with Postmark "modular webhooks". (Rather than looking for fields that are probably only in certain events.) Also issue configuration error on inbound url installed as tracking webhook (and vice versa). --- anymail/webhooks/postmark.py | 50 ++++++++++++++++++++++----------- tests/test_postmark_inbound.py | 7 +++++ tests/test_postmark_webhooks.py | 11 ++++++++ 3 files changed, 51 insertions(+), 17 deletions(-) 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"}))