import json from django.utils.dateparse import parse_datetime from .base import AnymailBaseWebhookView from ..exceptions import AnymailConfigurationError from ..signals import tracking, AnymailTrackingEvent, EventType, RejectReason from ..utils import getfirst class PostmarkBaseWebhookView(AnymailBaseWebhookView): """Base view class for Postmark webhooks""" def parse_events(self, request): esp_event = json.loads(request.body.decode('utf-8')) return [self.esp_to_anymail_event(esp_event)] def esp_to_anymail_event(self, esp_event): raise NotImplementedError() class PostmarkTrackingWebhookView(PostmarkBaseWebhookView): """Handler for Postmark delivery and engagement tracking webhooks""" signal = tracking event_types = { # Map Postmark event type: Anymail normalized (event type, reject reason) 'HardBounce': (EventType.BOUNCED, RejectReason.BOUNCED), 'Transient': (EventType.DEFERRED, None), 'Unsubscribe': (EventType.UNSUBSCRIBED, RejectReason.UNSUBSCRIBED), 'Subscribe': (EventType.SUBSCRIBED, None), 'AutoResponder': (EventType.AUTORESPONDED, None), 'AddressChange': (EventType.AUTORESPONDED, None), 'DnsError': (EventType.DEFERRED, None), # "temporary DNS error" 'SpamNotification': (EventType.COMPLAINED, RejectReason.SPAM), 'OpenRelayTest': (EventType.DEFERRED, None), # Receiving MTA is testing Postmark 'Unknown': (EventType.UNKNOWN, None), 'SoftBounce': (EventType.BOUNCED, RejectReason.BOUNCED), # might also receive HardBounce later 'VirusNotification': (EventType.BOUNCED, RejectReason.OTHER), 'ChallengeVerification': (EventType.AUTORESPONDED, None), 'BadEmailAddress': (EventType.REJECTED, RejectReason.INVALID), 'SpamComplaint': (EventType.COMPLAINED, RejectReason.SPAM), 'ManuallyDeactivated': (EventType.REJECTED, RejectReason.BLOCKED), 'Unconfirmed': (EventType.REJECTED, None), 'Blocked': (EventType.REJECTED, RejectReason.BLOCKED), 'SMTPApiError': (EventType.FAILED, None), # could occur if user also using Postmark SMTP directly '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 Postmark webhook (yet?) # 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)) except KeyError: if 'FirstOpen' in esp_event: event_type = EventType.OPENED elif 'DeliveredAt' in esp_event: event_type = EventType.DELIVERED elif 'From' 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.") else: event_type = EventType.UNKNOWN recipient = getfirst(esp_event, ['Email', 'Recipient'], None) # Email for bounce; Recipient for open try: timestr = getfirst(esp_event, ['DeliveredAt', 'BouncedAt', 'ReceivedAt']) except KeyError: timestamp = None else: timestamp = parse_datetime(timestr) try: event_id = str(esp_event['ID']) # only in bounce events except KeyError: event_id = None try: tags = [esp_event['Tag']] except KeyError: tags = None return AnymailTrackingEvent( description=esp_event.get('Description', None), esp_event=esp_event, event_id=event_id, event_type=event_type, message_id=esp_event.get('MessageID', None), mta_response=esp_event.get('Details', None), recipient=recipient, reject_reason=reject_reason, tags=tags, timestamp=timestamp, user_agent=esp_event.get('UserAgent', None), )