Skip to content

Commit 146afba

Browse files
committed
Simplify Mandrill webhook validation handshake.
Anymail was requiring Mandrill's webhook authentication key for the initial webhook url validation request from Mandrill, but Mandrill doesn't issue the key until that validation request succeeds. * Defer complaining about missing Mandrill webhook key until actual event post. * Document the double-deploy process required to set up Mandrill webhooks. Fixes #46.
1 parent 5259639 commit 146afba

File tree

3 files changed

+39
-12
lines changed

3 files changed

+39
-12
lines changed

anymail/webhooks/mandrill.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,23 @@ class MandrillSignatureMixin(object):
2323
def __init__(self, **kwargs):
2424
# noinspection PyUnresolvedReferences
2525
esp_name = self.esp_name
26-
webhook_key = get_anymail_setting('webhook_key', esp_name=esp_name,
26+
# webhook_key is required for POST, but not for HEAD when Mandrill validates webhook url.
27+
# Defer "missing setting" error until we actually try to use it in the POST...
28+
webhook_key = get_anymail_setting('webhook_key', esp_name=esp_name, default=None,
2729
kwargs=kwargs, allow_bare=True)
28-
self.webhook_key = webhook_key.encode('ascii') # hmac.new requires bytes key in python 3
30+
if webhook_key is not None:
31+
self.webhook_key = webhook_key.encode('ascii') # hmac.new requires bytes key in python 3
2932
self.webhook_url = get_anymail_setting('webhook_url', esp_name=esp_name, default=None,
3033
kwargs=kwargs, allow_bare=True)
3134
# noinspection PyArgumentList
3235
super(MandrillSignatureMixin, self).__init__(**kwargs)
3336

3437
def validate_request(self, request):
38+
if self.webhook_key is None:
39+
# issue deferred "missing setting" error (re-call get-setting without a default)
40+
# noinspection PyUnresolvedReferences
41+
get_anymail_setting('webhook_key', esp_name=self.esp_name, allow_bare=True)
42+
3543
try:
3644
signature = request.META["HTTP_X_MANDRILL_SIGNATURE"]
3745
except KeyError:

docs/esps/mandrill.rst

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -191,20 +191,32 @@ Status tracking webhooks
191191
------------------------
192192

193193
If you are using Anymail's normalized :ref:`status tracking <event-tracking>`,
194-
follow `Mandrill's instructions`_ to add Anymail's webhook URL:
194+
setting up Anymail's webhook URL requires deploying your Django project twice:
195195

196-
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/mandrill/tracking/`
196+
1. First, follow the instructions to
197+
:ref:`configure Anymail's webhooks <webhooks-configuration>`. You *must*
198+
deploy before adding the webhook URL to Mandrill, because it will attempt
199+
to verify the URL against your production server.
197200

198-
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
199-
* *yoursite.example.com* is your Django site
201+
Follow `Mandrill's instructions`_ to add Anymail's webhook URL in their settings:
200202

201-
Be sure to check the boxes in the Mandrill settings for the event types you want to receive.
202-
The same Anymail tracking URL can handle all Mandrill "message" and "sync" events.
203+
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/mandrill/tracking/`
204+
205+
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
206+
* *yoursite.example.com* is your Django site
207+
208+
Be sure to check the boxes in the Mandrill settings for the event types you want to receive.
209+
The same Anymail tracking URL can handle all Mandrill "message" and "sync" events.
210+
211+
2. Mandrill will provide you a "webhook authentication key" once it verifies the URL
212+
is working. Add this to your Django project's Anymail settings under
213+
:setting:`MANDRILL_WEBHOOK_KEY <ANYMAIL_MANDRILL_WEBHOOK_KEY>`.
214+
(You may also need to set :setting:`MANDRILL_WEBHOOK_URL <ANYMAIL_MANDRILL_WEBHOOK_URL>`
215+
depending on your server config.) Then deploy your project again.
203216

204217
Mandrill implements webhook signing on the entire event payload, and Anymail will
205-
verify the signature. You must set :setting:`ANYMAIL_MANDRILL_WEBHOOK_KEY` to the
206-
webhook key authentication key issued by Mandrill. You may also need to set
207-
:setting:`ANYMAIL_MANDRILL_WEBHOOK_URL` depending on your server config.
218+
verify the signature. Until the correct webhook key is set, Anymail will raise
219+
an exception for any webhook calls from Mandrill (other than the initial validation request).
208220

209221
Mandrill will report these Anymail :attr:`~anymail.signals.AnymailTrackingEvent.event_type`\s:
210222
sent, rejected, deferred, bounced, opened, clicked, complained, unsubscribed. Mandrill does

tests/test_mandrill_webhooks.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,17 @@ def mandrill_args(events=None, url='/anymail/mandrill/tracking/', key=TEST_WEBHO
4141

4242
class MandrillWebhookSettingsTestCase(WebhookTestCase):
4343
def test_requires_webhook_key(self):
44-
with self.assertRaises(ImproperlyConfigured):
44+
with self.assertRaisesRegex(ImproperlyConfigured, r'MANDRILL_WEBHOOK_KEY'):
4545
self.client.post('/anymail/mandrill/tracking/',
4646
data={'mandrill_events': '[]'})
4747

48+
def test_head_does_not_require_webhook_key(self):
49+
# Mandrill issues an unsigned HEAD request to verify the wehbook url.
50+
# Only *after* that succeeds will Mandrill will tell you the webhook key.
51+
# So make sure that HEAD request will go through without any key set:
52+
response = self.client.head('/anymail/mandrill/tracking/')
53+
self.assertEqual(response.status_code, 200)
54+
4855

4956
@override_settings(ANYMAIL_MANDRILL_WEBHOOK_KEY=TEST_WEBHOOK_KEY)
5057
class MandrillWebhookSecurityTestCase(WebhookTestCase, WebhookBasicAuthTestsMixin):

0 commit comments

Comments
 (0)