mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
140
anymail/webhooks/mandrill.py
Normal file
140
anymail/webhooks/mandrill.py
Normal file
@@ -0,0 +1,140 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
from base64 import b64encode
|
||||
from django.utils.timezone import utc
|
||||
|
||||
from .base import AnymailBaseWebhookView
|
||||
from ..exceptions import AnymailWebhookValidationFailure, AnymailConfigurationError
|
||||
from ..signals import tracking, AnymailTrackingEvent, EventType
|
||||
from ..utils import get_anymail_setting, getfirst
|
||||
|
||||
|
||||
class MandrillSignatureMixin(object):
|
||||
"""Validates Mandrill webhook signature"""
|
||||
|
||||
# These can be set from kwargs in View.as_view, or pulled from settings in init:
|
||||
webhook_key = None # required
|
||||
webhook_url = None # optional; defaults to actual url used
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# noinspection PyUnresolvedReferences
|
||||
esp_name = self.esp_name
|
||||
webhook_key = get_anymail_setting('webhook_key', esp_name=esp_name,
|
||||
kwargs=kwargs, allow_bare=True)
|
||||
self.webhook_key = webhook_key.encode('ascii') # hmac.new requires bytes key in python 3
|
||||
self.webhook_url = get_anymail_setting('webhook_url', esp_name=esp_name, default=None,
|
||||
kwargs=kwargs, allow_bare=True)
|
||||
# noinspection PyArgumentList
|
||||
super(MandrillSignatureMixin, self).__init__(**kwargs)
|
||||
|
||||
def validate_request(self, request):
|
||||
try:
|
||||
signature = request.META["HTTP_X_MANDRILL_SIGNATURE"]
|
||||
except KeyError:
|
||||
raise AnymailWebhookValidationFailure("X-Mandrill-Signature header missing from webhook POST")
|
||||
|
||||
# Mandrill signs the exact URL plus the sorted POST params:
|
||||
signed_data = self.webhook_url or request.build_absolute_uri()
|
||||
params = request.POST.dict()
|
||||
for key in sorted(params.keys()):
|
||||
signed_data += key + params[key]
|
||||
|
||||
expected_signature = b64encode(hmac.new(key=self.webhook_key, msg=signed_data.encode('utf-8'),
|
||||
digestmod=hashlib.sha1).digest())
|
||||
if not hmac.compare_digest(signature, expected_signature):
|
||||
raise AnymailWebhookValidationFailure("Mandrill webhook called with incorrect signature")
|
||||
|
||||
|
||||
class MandrillBaseWebhookView(MandrillSignatureMixin, AnymailBaseWebhookView):
|
||||
"""Base view class for Mandrill webhooks"""
|
||||
|
||||
warn_if_no_basic_auth = False # because we validate against signature
|
||||
|
||||
def parse_events(self, request):
|
||||
esp_events = json.loads(request.POST['mandrill_events'])
|
||||
return [self.esp_to_anymail_event(esp_event) for esp_event in esp_events]
|
||||
|
||||
def esp_to_anymail_event(self, esp_event):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class MandrillTrackingWebhookView(MandrillBaseWebhookView):
|
||||
|
||||
signal = tracking
|
||||
|
||||
event_types = {
|
||||
# Message events:
|
||||
'send': EventType.SENT,
|
||||
'deferral': EventType.DEFERRED,
|
||||
'hard_bounce': EventType.BOUNCED,
|
||||
'soft_bounce': EventType.DEFERRED,
|
||||
'open': EventType.OPENED,
|
||||
'click': EventType.CLICKED,
|
||||
'spam': EventType.COMPLAINED,
|
||||
'unsub': EventType.UNSUBSCRIBED,
|
||||
'reject': EventType.REJECTED,
|
||||
# Sync events (we don't really normalize these well):
|
||||
'whitelist': EventType.UNKNOWN,
|
||||
'blacklist': EventType.UNKNOWN,
|
||||
# Inbound events:
|
||||
'inbound': EventType.INBOUND,
|
||||
}
|
||||
|
||||
def esp_to_anymail_event(self, esp_event):
|
||||
esp_type = getfirst(esp_event, ['event', 'type'], None)
|
||||
event_type = self.event_types.get(esp_type, EventType.UNKNOWN)
|
||||
if event_type == EventType.INBOUND:
|
||||
raise AnymailConfigurationError(
|
||||
"You seem to have set Mandrill's *inbound* webhook URL "
|
||||
"to Anymail's Mandrill *tracking* webhook URL.")
|
||||
|
||||
try:
|
||||
timestamp = datetime.fromtimestamp(esp_event['ts'], tz=utc)
|
||||
except (KeyError, ValueError):
|
||||
timestamp = None
|
||||
|
||||
try:
|
||||
recipient = esp_event['msg']['email']
|
||||
except KeyError:
|
||||
try:
|
||||
recipient = esp_event['reject']['email'] # sync events
|
||||
except KeyError:
|
||||
recipient = None
|
||||
|
||||
try:
|
||||
mta_response = esp_event['msg']['diag']
|
||||
except KeyError:
|
||||
mta_response = None
|
||||
|
||||
try:
|
||||
description = getfirst(esp_event['reject'], ['detail', 'reason'])
|
||||
except KeyError:
|
||||
description = None
|
||||
|
||||
try:
|
||||
metadata = esp_event['msg']['metadata']
|
||||
except KeyError:
|
||||
metadata = None
|
||||
|
||||
try:
|
||||
tags = esp_event['msg']['tags']
|
||||
except KeyError:
|
||||
tags = None
|
||||
|
||||
return AnymailTrackingEvent(
|
||||
click_url=esp_event.get('url', None),
|
||||
description=description,
|
||||
esp_event=esp_event,
|
||||
event_type=event_type,
|
||||
message_id=esp_event.get('_id', None),
|
||||
metadata=metadata,
|
||||
mta_response=mta_response,
|
||||
recipient=recipient,
|
||||
reject_reason=None, # probably map esp_event['msg']['bounce_description'], but insufficient docs
|
||||
tags=tags,
|
||||
timestamp=timestamp,
|
||||
user_agent=esp_event.get('user_agent', None),
|
||||
)
|
||||
Reference in New Issue
Block a user