Skip to content

Commit 1d252ca

Browse files
committed
Mailgun: better errors for misconfigured webhooks
Detect cases where inbound or tracking webhook url has been configured in wrong Mailgun webhook. See #129.
1 parent bb25715 commit 1d252ca

File tree

3 files changed

+71
-2
lines changed

3 files changed

+71
-2
lines changed

anymail/webhooks/mailgun.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from django.utils.timezone import utc
88

99
from .base import AnymailBaseWebhookView
10-
from ..exceptions import AnymailWebhookValidationFailure, AnymailInvalidAddress
10+
from ..exceptions import AnymailConfigurationError, AnymailWebhookValidationFailure, AnymailInvalidAddress
1111
from ..inbound import AnymailInboundMessage
1212
from ..signals import inbound, tracking, AnymailInboundEvent, AnymailTrackingEvent, EventType, RejectReason
1313
from ..utils import get_anymail_setting, combine, querydict_getfirst, parse_single_address
@@ -200,6 +200,12 @@ def mailgun_legacy_to_anymail_event(self, esp_event):
200200
# to avoid potential conflicting user-data.
201201
esp_event.getfirst = querydict_getfirst.__get__(esp_event)
202202

203+
if 'event' not in esp_event and 'sender' in esp_event:
204+
# Inbound events don't (currently) have an event field
205+
raise AnymailConfigurationError(
206+
"You seem to have set Mailgun's *inbound* route "
207+
"to Anymail's Mailgun *tracking* webhook URL.")
208+
203209
event_type = self.legacy_event_types.get(esp_event.getfirst('event'), EventType.UNKNOWN)
204210
timestamp = datetime.fromtimestamp(int(esp_event['timestamp']), tz=utc) # use *last* value of timestamp
205211
# Message-Id is not documented for every event, but seems to always be included.
@@ -319,12 +325,27 @@ class MailgunInboundWebhookView(MailgunBaseWebhookView):
319325
signal = inbound
320326

321327
def parse_events(self, request):
328+
if request.content_type == "application/json":
329+
esp_event = json.loads(request.body.decode('utf-8'))
330+
event_type = esp_event.get('event-data', {}).get('event', '')
331+
raise AnymailConfigurationError(
332+
"You seem to have set Mailgun's *%s tracking* webhook "
333+
"to Anymail's Mailgun *inbound* webhook URL. "
334+
"(Or Mailgun has changed inbound events to use json.)"
335+
% event_type)
322336
return [self.esp_to_anymail_event(request)]
323337

324338
def esp_to_anymail_event(self, request):
325339
# Inbound uses the entire Django request as esp_event, because we need POST and FILES.
326340
# Note that request.POST is case-sensitive (unlike email.message.Message headers).
327341
esp_event = request
342+
343+
if request.POST.get('event', 'inbound') != 'inbound':
344+
# (Legacy) tracking event
345+
raise AnymailConfigurationError(
346+
"You seem to have set Mailgun's *%s tracking* webhook "
347+
"to Anymail's Mailgun *inbound* webhook URL." % request.POST['event'])
348+
328349
if 'body-mime' in request.POST:
329350
# Raw-MIME
330351
message = AnymailInboundMessage.parse_raw_mime(request.POST['body-mime'])

tests/test_mailgun_inbound.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@
77
from django.utils.timezone import utc
88
from mock import ANY
99

10+
from anymail.exceptions import AnymailConfigurationError
1011
from anymail.inbound import AnymailInboundMessage
1112
from anymail.signals import AnymailInboundEvent
1213
from anymail.webhooks.mailgun import MailgunInboundWebhookView
1314

14-
from .test_mailgun_webhooks import TEST_API_KEY, mailgun_sign_legacy_payload, querydict_to_postdict
15+
from .test_mailgun_webhooks import (
16+
TEST_API_KEY, mailgun_sign_payload,
17+
mailgun_sign_legacy_payload, querydict_to_postdict)
1518
from .utils import sample_image_content, sample_email_content
1619
from .webhook_cases import WebhookTestCase
1720

@@ -174,3 +177,34 @@ def test_inbound_mime(self):
174177
self.assertEqual(message.subject, 'Raw MIME test')
175178
self.assertEqual(message.text, u"It's a body\N{HORIZONTAL ELLIPSIS}\n")
176179
self.assertEqual(message.html, u"""<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\n""")
180+
181+
def test_misconfigured_tracking(self):
182+
raw_event = mailgun_sign_payload({
183+
"event-data": {
184+
"event": "clicked",
185+
"timestamp": 1534109600.089676,
186+
"recipient": "[email protected]",
187+
"url": "https://example.com/test"
188+
}
189+
})
190+
with self.assertRaisesMessage(
191+
AnymailConfigurationError,
192+
"You seem to have set Mailgun's *clicked tracking* webhook"
193+
" to Anymail's Mailgun *inbound* webhook URL."
194+
):
195+
self.client.post('/anymail/mailgun/inbound/',
196+
data=json.dumps(raw_event), content_type='application/json')
197+
198+
def test_misconfigured_tracking_legacy(self):
199+
raw_event = mailgun_sign_legacy_payload({
200+
'domain': 'example.com',
201+
'message-headers': '[]',
202+
'recipient': '[email protected]',
203+
'event': 'delivered',
204+
})
205+
with self.assertRaisesMessage(
206+
AnymailConfigurationError,
207+
"You seem to have set Mailgun's *delivered tracking* webhook"
208+
" to Anymail's Mailgun *inbound* webhook URL."
209+
):
210+
self.client.post('/anymail/mailgun/inbound/', data=raw_event)

tests/test_mailgun_webhooks.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from django.utils.timezone import utc
99
from mock import ANY
1010

11+
from anymail.exceptions import AnymailConfigurationError
1112
from anymail.signals import AnymailTrackingEvent
1213
from anymail.webhooks.mailgun import MailgunTrackingWebhookView
1314

@@ -698,3 +699,16 @@ def test_x_tags(self):
698699
kwargs = self.assert_handler_called_once_with(self.tracking_handler)
699700
event = kwargs['event']
700701
self.assertEqual(event.tags, ["tag1", "tag2"])
702+
703+
def test_misconfigured_inbound(self):
704+
raw_event = mailgun_sign_legacy_payload({
705+
'recipient': '[email protected]',
706+
'sender': '[email protected]',
707+
'message-headers': '[]',
708+
'body-plain': 'Test body plain',
709+
'body-html': '<div>Test body html</div>',
710+
})
711+
712+
errmsg = "You seem to have set Mailgun's *inbound* route to Anymail's Mailgun *tracking* webhook URL."
713+
with self.assertRaisesMessage(AnymailConfigurationError, errmsg):
714+
self.client.post('/anymail/mailgun/tracking/', data=raw_event)

0 commit comments

Comments
 (0)