diff --git a/anymail/webhooks/mandrill.py b/anymail/webhooks/mandrill.py index b1d0c63..f3d13ce 100644 --- a/anymail/webhooks/mandrill.py +++ b/anymail/webhooks/mandrill.py @@ -23,15 +23,23 @@ class MandrillSignatureMixin(object): def __init__(self, **kwargs): # noinspection PyUnresolvedReferences esp_name = self.esp_name - webhook_key = get_anymail_setting('webhook_key', esp_name=esp_name, + # webhook_key is required for POST, but not for HEAD when Mandrill validates webhook url. + # Defer "missing setting" error until we actually try to use it in the POST... + webhook_key = get_anymail_setting('webhook_key', esp_name=esp_name, default=None, kwargs=kwargs, allow_bare=True) - self.webhook_key = webhook_key.encode('ascii') # hmac.new requires bytes key in python 3 + if webhook_key is not None: + 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): + if self.webhook_key is None: + # issue deferred "missing setting" error (re-call get-setting without a default) + # noinspection PyUnresolvedReferences + get_anymail_setting('webhook_key', esp_name=self.esp_name, allow_bare=True) + try: signature = request.META["HTTP_X_MANDRILL_SIGNATURE"] except KeyError: diff --git a/docs/esps/mandrill.rst b/docs/esps/mandrill.rst index 3d579c1..39a1e31 100644 --- a/docs/esps/mandrill.rst +++ b/docs/esps/mandrill.rst @@ -191,20 +191,32 @@ Status tracking webhooks ------------------------ If you are using Anymail's normalized :ref:`status tracking `, -follow `Mandrill's instructions`_ to add Anymail's webhook URL: +setting up Anymail's webhook URL requires deploying your Django project twice: - :samp:`https://{random}:{random}@{yoursite.example.com}/anymail/mandrill/tracking/` +1. First, follow the instructions to + :ref:`configure Anymail's webhooks `. You *must* + deploy before adding the webhook URL to Mandrill, because it will attempt + to verify the URL against your production server. - * *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret - * *yoursite.example.com* is your Django site + Follow `Mandrill's instructions`_ to add Anymail's webhook URL in their settings: -Be sure to check the boxes in the Mandrill settings for the event types you want to receive. -The same Anymail tracking URL can handle all Mandrill "message" and "sync" events. + :samp:`https://{random}:{random}@{yoursite.example.com}/anymail/mandrill/tracking/` + + * *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret + * *yoursite.example.com* is your Django site + + Be sure to check the boxes in the Mandrill settings for the event types you want to receive. + The same Anymail tracking URL can handle all Mandrill "message" and "sync" events. + +2. Mandrill will provide you a "webhook authentication key" once it verifies the URL + is working. Add this to your Django project's Anymail settings under + :setting:`MANDRILL_WEBHOOK_KEY `. + (You may also need to set :setting:`MANDRILL_WEBHOOK_URL ` + depending on your server config.) Then deploy your project again. Mandrill implements webhook signing on the entire event payload, and Anymail will -verify the signature. You must set :setting:`ANYMAIL_MANDRILL_WEBHOOK_KEY` to the -webhook key authentication key issued by Mandrill. You may also need to set -:setting:`ANYMAIL_MANDRILL_WEBHOOK_URL` depending on your server config. +verify the signature. Until the correct webhook key is set, Anymail will raise +an exception for any webhook calls from Mandrill (other than the initial validation request). Mandrill will report these Anymail :attr:`~anymail.signals.AnymailTrackingEvent.event_type`\s: sent, rejected, deferred, bounced, opened, clicked, complained, unsubscribed. Mandrill does diff --git a/tests/test_mandrill_webhooks.py b/tests/test_mandrill_webhooks.py index d5150df..0a72112 100644 --- a/tests/test_mandrill_webhooks.py +++ b/tests/test_mandrill_webhooks.py @@ -41,10 +41,17 @@ def mandrill_args(events=None, url='/anymail/mandrill/tracking/', key=TEST_WEBHO class MandrillWebhookSettingsTestCase(WebhookTestCase): def test_requires_webhook_key(self): - with self.assertRaises(ImproperlyConfigured): + with self.assertRaisesRegex(ImproperlyConfigured, r'MANDRILL_WEBHOOK_KEY'): self.client.post('/anymail/mandrill/tracking/', data={'mandrill_events': '[]'}) + def test_head_does_not_require_webhook_key(self): + # Mandrill issues an unsigned HEAD request to verify the wehbook url. + # Only *after* that succeeds will Mandrill will tell you the webhook key. + # So make sure that HEAD request will go through without any key set: + response = self.client.head('/anymail/mandrill/tracking/') + self.assertEqual(response.status_code, 200) + @override_settings(ANYMAIL_MANDRILL_WEBHOOK_KEY=TEST_WEBHOOK_KEY) class MandrillWebhookSecurityTestCase(WebhookTestCase, WebhookBasicAuthTestsMixin):