mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 11:51:05 -05:00
Mailgun: better errors for misconfigured webhooks
Detect cases where inbound or tracking webhook url has been configured in wrong Mailgun webhook. See #129.
This commit is contained in:
@@ -7,7 +7,7 @@ from django.utils.crypto import constant_time_compare
|
|||||||
from django.utils.timezone import utc
|
from django.utils.timezone import utc
|
||||||
|
|
||||||
from .base import AnymailBaseWebhookView
|
from .base import AnymailBaseWebhookView
|
||||||
from ..exceptions import AnymailWebhookValidationFailure, AnymailInvalidAddress
|
from ..exceptions import AnymailConfigurationError, AnymailWebhookValidationFailure, AnymailInvalidAddress
|
||||||
from ..inbound import AnymailInboundMessage
|
from ..inbound import AnymailInboundMessage
|
||||||
from ..signals import inbound, tracking, AnymailInboundEvent, AnymailTrackingEvent, EventType, RejectReason
|
from ..signals import inbound, tracking, AnymailInboundEvent, AnymailTrackingEvent, EventType, RejectReason
|
||||||
from ..utils import get_anymail_setting, combine, querydict_getfirst, parse_single_address
|
from ..utils import get_anymail_setting, combine, querydict_getfirst, parse_single_address
|
||||||
@@ -200,6 +200,12 @@ class MailgunTrackingWebhookView(MailgunBaseWebhookView):
|
|||||||
# to avoid potential conflicting user-data.
|
# to avoid potential conflicting user-data.
|
||||||
esp_event.getfirst = querydict_getfirst.__get__(esp_event)
|
esp_event.getfirst = querydict_getfirst.__get__(esp_event)
|
||||||
|
|
||||||
|
if 'event' not in esp_event and 'sender' in esp_event:
|
||||||
|
# Inbound events don't (currently) have an event field
|
||||||
|
raise AnymailConfigurationError(
|
||||||
|
"You seem to have set Mailgun's *inbound* route "
|
||||||
|
"to Anymail's Mailgun *tracking* webhook URL.")
|
||||||
|
|
||||||
event_type = self.legacy_event_types.get(esp_event.getfirst('event'), EventType.UNKNOWN)
|
event_type = self.legacy_event_types.get(esp_event.getfirst('event'), EventType.UNKNOWN)
|
||||||
timestamp = datetime.fromtimestamp(int(esp_event['timestamp']), tz=utc) # use *last* value of timestamp
|
timestamp = datetime.fromtimestamp(int(esp_event['timestamp']), tz=utc) # use *last* value of timestamp
|
||||||
# Message-Id is not documented for every event, but seems to always be included.
|
# Message-Id is not documented for every event, but seems to always be included.
|
||||||
@@ -319,12 +325,27 @@ class MailgunInboundWebhookView(MailgunBaseWebhookView):
|
|||||||
signal = inbound
|
signal = inbound
|
||||||
|
|
||||||
def parse_events(self, request):
|
def parse_events(self, request):
|
||||||
|
if request.content_type == "application/json":
|
||||||
|
esp_event = json.loads(request.body.decode('utf-8'))
|
||||||
|
event_type = esp_event.get('event-data', {}).get('event', '')
|
||||||
|
raise AnymailConfigurationError(
|
||||||
|
"You seem to have set Mailgun's *%s tracking* webhook "
|
||||||
|
"to Anymail's Mailgun *inbound* webhook URL. "
|
||||||
|
"(Or Mailgun has changed inbound events to use json.)"
|
||||||
|
% event_type)
|
||||||
return [self.esp_to_anymail_event(request)]
|
return [self.esp_to_anymail_event(request)]
|
||||||
|
|
||||||
def esp_to_anymail_event(self, request):
|
def esp_to_anymail_event(self, request):
|
||||||
# Inbound uses the entire Django request as esp_event, because we need POST and FILES.
|
# Inbound uses the entire Django request as esp_event, because we need POST and FILES.
|
||||||
# Note that request.POST is case-sensitive (unlike email.message.Message headers).
|
# Note that request.POST is case-sensitive (unlike email.message.Message headers).
|
||||||
esp_event = request
|
esp_event = request
|
||||||
|
|
||||||
|
if request.POST.get('event', 'inbound') != 'inbound':
|
||||||
|
# (Legacy) tracking event
|
||||||
|
raise AnymailConfigurationError(
|
||||||
|
"You seem to have set Mailgun's *%s tracking* webhook "
|
||||||
|
"to Anymail's Mailgun *inbound* webhook URL." % request.POST['event'])
|
||||||
|
|
||||||
if 'body-mime' in request.POST:
|
if 'body-mime' in request.POST:
|
||||||
# Raw-MIME
|
# Raw-MIME
|
||||||
message = AnymailInboundMessage.parse_raw_mime(request.POST['body-mime'])
|
message = AnymailInboundMessage.parse_raw_mime(request.POST['body-mime'])
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ from django.test import override_settings
|
|||||||
from django.utils.timezone import utc
|
from django.utils.timezone import utc
|
||||||
from mock import ANY
|
from mock import ANY
|
||||||
|
|
||||||
|
from anymail.exceptions import AnymailConfigurationError
|
||||||
from anymail.inbound import AnymailInboundMessage
|
from anymail.inbound import AnymailInboundMessage
|
||||||
from anymail.signals import AnymailInboundEvent
|
from anymail.signals import AnymailInboundEvent
|
||||||
from anymail.webhooks.mailgun import MailgunInboundWebhookView
|
from anymail.webhooks.mailgun import MailgunInboundWebhookView
|
||||||
|
|
||||||
from .test_mailgun_webhooks import TEST_API_KEY, mailgun_sign_legacy_payload, querydict_to_postdict
|
from .test_mailgun_webhooks import (
|
||||||
|
TEST_API_KEY, mailgun_sign_payload,
|
||||||
|
mailgun_sign_legacy_payload, querydict_to_postdict)
|
||||||
from .utils import sample_image_content, sample_email_content
|
from .utils import sample_image_content, sample_email_content
|
||||||
from .webhook_cases import WebhookTestCase
|
from .webhook_cases import WebhookTestCase
|
||||||
|
|
||||||
@@ -174,3 +177,34 @@ class MailgunInboundTestCase(WebhookTestCase):
|
|||||||
self.assertEqual(message.subject, 'Raw MIME test')
|
self.assertEqual(message.subject, 'Raw MIME test')
|
||||||
self.assertEqual(message.text, u"It's a body\N{HORIZONTAL ELLIPSIS}\n")
|
self.assertEqual(message.text, u"It's a body\N{HORIZONTAL ELLIPSIS}\n")
|
||||||
self.assertEqual(message.html, u"""<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\n""")
|
self.assertEqual(message.html, u"""<div dir="ltr">It's a body\N{HORIZONTAL ELLIPSIS}</div>\n""")
|
||||||
|
|
||||||
|
def test_misconfigured_tracking(self):
|
||||||
|
raw_event = mailgun_sign_payload({
|
||||||
|
"event-data": {
|
||||||
|
"event": "clicked",
|
||||||
|
"timestamp": 1534109600.089676,
|
||||||
|
"recipient": "recipient@example.com",
|
||||||
|
"url": "https://example.com/test"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
with self.assertRaisesMessage(
|
||||||
|
AnymailConfigurationError,
|
||||||
|
"You seem to have set Mailgun's *clicked tracking* webhook"
|
||||||
|
" to Anymail's Mailgun *inbound* webhook URL."
|
||||||
|
):
|
||||||
|
self.client.post('/anymail/mailgun/inbound/',
|
||||||
|
data=json.dumps(raw_event), content_type='application/json')
|
||||||
|
|
||||||
|
def test_misconfigured_tracking_legacy(self):
|
||||||
|
raw_event = mailgun_sign_legacy_payload({
|
||||||
|
'domain': 'example.com',
|
||||||
|
'message-headers': '[]',
|
||||||
|
'recipient': 'recipient@example.com',
|
||||||
|
'event': 'delivered',
|
||||||
|
})
|
||||||
|
with self.assertRaisesMessage(
|
||||||
|
AnymailConfigurationError,
|
||||||
|
"You seem to have set Mailgun's *delivered tracking* webhook"
|
||||||
|
" to Anymail's Mailgun *inbound* webhook URL."
|
||||||
|
):
|
||||||
|
self.client.post('/anymail/mailgun/inbound/', data=raw_event)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from django.test import override_settings
|
|||||||
from django.utils.timezone import utc
|
from django.utils.timezone import utc
|
||||||
from mock import ANY
|
from mock import ANY
|
||||||
|
|
||||||
|
from anymail.exceptions import AnymailConfigurationError
|
||||||
from anymail.signals import AnymailTrackingEvent
|
from anymail.signals import AnymailTrackingEvent
|
||||||
from anymail.webhooks.mailgun import MailgunTrackingWebhookView
|
from anymail.webhooks.mailgun import MailgunTrackingWebhookView
|
||||||
|
|
||||||
@@ -698,3 +699,16 @@ class MailgunLegacyTestCase(WebhookTestCase):
|
|||||||
kwargs = self.assert_handler_called_once_with(self.tracking_handler)
|
kwargs = self.assert_handler_called_once_with(self.tracking_handler)
|
||||||
event = kwargs['event']
|
event = kwargs['event']
|
||||||
self.assertEqual(event.tags, ["tag1", "tag2"])
|
self.assertEqual(event.tags, ["tag1", "tag2"])
|
||||||
|
|
||||||
|
def test_misconfigured_inbound(self):
|
||||||
|
raw_event = mailgun_sign_legacy_payload({
|
||||||
|
'recipient': 'test@inbound.example.com',
|
||||||
|
'sender': 'envelope-from@example.org',
|
||||||
|
'message-headers': '[]',
|
||||||
|
'body-plain': 'Test body plain',
|
||||||
|
'body-html': '<div>Test body html</div>',
|
||||||
|
})
|
||||||
|
|
||||||
|
errmsg = "You seem to have set Mailgun's *inbound* route to Anymail's Mailgun *tracking* webhook URL."
|
||||||
|
with self.assertRaisesMessage(AnymailConfigurationError, errmsg):
|
||||||
|
self.client.post('/anymail/mailgun/tracking/', data=raw_event)
|
||||||
|
|||||||
Reference in New Issue
Block a user