Add Postal support

Thanks to @tiltec for researching, implementing, testing and documenting it.
This commit is contained in:
Tilmann Becker
2021-06-08 02:11:35 +02:00
committed by GitHub
parent f831fe814a
commit e90c10b546
14 changed files with 1674 additions and 25 deletions

View File

@@ -0,0 +1,324 @@
import json
import unittest
from base64 import b64encode
from datetime import datetime
from django.test import tag
from django.utils.timezone import utc
from mock import ANY
from anymail.exceptions import AnymailConfigurationError
from anymail.signals import AnymailTrackingEvent
from anymail.webhooks.postal import PostalTrackingWebhookView
from .utils_postal import ClientWithPostalSignature, make_key
from .webhook_cases import WebhookTestCase
@tag('postal')
@unittest.skipUnless(ClientWithPostalSignature, "Install 'cryptography' to run postal webhook tests")
class PostalWebhookSecurityTestCase(WebhookTestCase):
client_class = ClientWithPostalSignature
def setUp(self):
super().setUp()
self.clear_basic_auth()
self.client.set_private_key(make_key())
def test_failed_signature_check(self):
response = self.client.post('/anymail/postal/tracking/',
content_type='application/json', data=json.dumps({'some': 'data'}),
HTTP_X_POSTAL_SIGNATURE=b64encode('invalid'.encode('utf-8')))
self.assertEqual(response.status_code, 400)
response = self.client.post('/anymail/postal/tracking/',
content_type='application/json', data=json.dumps({'some': 'data'}),
HTTP_X_POSTAL_SIGNATURE='garbage')
self.assertEqual(response.status_code, 400)
response = self.client.post('/anymail/postal/tracking/',
content_type='application/json', data=json.dumps({'some': 'data'}),
HTTP_X_POSTAL_SIGNATURE='')
self.assertEqual(response.status_code, 400)
@tag('postal')
@unittest.skipUnless(ClientWithPostalSignature, "Install 'cryptography' to run postal webhook tests")
class PostalDeliveryTestCase(WebhookTestCase):
client_class = ClientWithPostalSignature
def setUp(self):
super().setUp()
self.clear_basic_auth()
self.client.set_private_key(make_key())
def test_bounce_event(self):
raw_event = {
"event": "MessageDelayed",
"timestamp": 1606753101.961181,
"payload": {
"original_message": {
"id": 233843,
"token": "McC2tuqg7mhx",
"direction": "outgoing",
"message_id": "7b82aac4-5d63-41b8-8e35-9faa31a892dc@rp.postal.example.com",
"to": "bounce@example.com",
"from": "sender@example.com",
"subject": "...",
"timestamp": 1606436187.8883688,
"spam_status": "NotChecked",
"tag": None
},
"bounce": {
"id": 233864,
"token": "nII5p0Cp8onV",
"direction": "incoming",
"message_id": "E1kiRR8-0001ay-Iq@example.com",
"to": "bk87jw@psrp.postal.example.com",
"from": None,
"subject": "Mail delivery failed: returning message to sender",
"timestamp": 1606436523.6060522,
"spam_status": "NotChecked",
"tag": None
},
"details": "details",
"output": "server output",
"sent_with_ssl": None,
"timestamp": 1606753101.9110143,
"time": None
},
"uuid": "0fcc831f-92b9-4e2b-97f2-d873abc77fab"
}
response = self.client.post('/anymail/postal/tracking/',
content_type='application/json', data=json.dumps(raw_event))
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=PostalTrackingWebhookView,
event=ANY, esp_name='Postal')
event = kwargs['event']
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "bounced")
self.assertEqual(event.esp_event, raw_event)
self.assertEqual(event.timestamp, datetime.fromtimestamp(1606753101, tz=utc))
self.assertEqual(event.message_id, 233843)
self.assertEqual(event.event_id, "0fcc831f-92b9-4e2b-97f2-d873abc77fab")
self.assertEqual(event.recipient, "bounce@example.com")
self.assertEqual(event.reject_reason, "bounced")
self.assertEqual(event.description,
"details")
self.assertEqual(event.mta_response,
"server output")
def test_deferred_event(self):
raw_event = {
"event": "MessageDelayed",
"timestamp": 1606753101.961181,
"payload": {
"message": {
"id": 1564,
"token": "Kmo8CRdjuM7B",
"direction": "outgoing",
"message_id": "7b095c0e-2c98-4e68-a41f-7bd217a83925@rp.postal.example.com",
"to": "deferred@example.com",
"from": "test@postal.example.com",
"subject": "Test Message at November 30, 2020 16:03",
"timestamp": 1606752235.195664,
"spam_status": "NotChecked",
"tag": None
},
"status": "SoftFail",
"details": "details",
"output": "server output",
"sent_with_ssl": None,
"timestamp": 1606753101.9110143,
"time": None
},
"uuid": "0fcc831f-92b9-4e2b-97f2-d873abc77fab"
}
response = self.client.post('/anymail/postal/tracking/',
content_type='application/json', data=json.dumps(raw_event))
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=PostalTrackingWebhookView,
event=ANY, esp_name='Postal')
event = kwargs['event']
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "deferred")
self.assertEqual(event.esp_event, raw_event)
self.assertEqual(event.timestamp, datetime.fromtimestamp(1606753101, tz=utc))
self.assertEqual(event.message_id, 1564)
self.assertEqual(event.event_id, "0fcc831f-92b9-4e2b-97f2-d873abc77fab")
self.assertEqual(event.recipient, "deferred@example.com")
self.assertEqual(event.reject_reason, None)
self.assertEqual(event.description,
"details")
self.assertEqual(event.mta_response,
"server output")
def test_queued_event(self):
raw_event = {
"event": "MessageHeld",
"timestamp": 1606753101.330977,
"payload": {
"message": {
"id": 1568,
"token": "VRvQMS20Bb4Y",
"direction": "outgoing",
"message_id": "ec7b6375-4045-451a-9503-2a23a607c1c1@rp.postal.example.com",
"to": "suppressed@example.com",
"from": "test@example.com",
"subject": "Test Message at November 30, 2020 16:12",
"timestamp": 1606752750.993815,
"spam_status": "NotChecked",
"tag": None
},
"status": "Held",
"details": "Recipient (suppressed@example.com) is on the suppression list",
"output": "server output",
"sent_with_ssl": None,
"timestamp": 1606752751.8933666,
"time": None
},
"uuid": "9be13015-2e54-456c-bf66-eacbe33da824"
}
response = self.client.post('/anymail/postal/tracking/',
content_type='application/json', data=json.dumps(raw_event))
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=PostalTrackingWebhookView,
event=ANY, esp_name='Postal')
event = kwargs['event']
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "queued")
self.assertEqual(event.esp_event, raw_event)
self.assertEqual(event.timestamp, datetime.fromtimestamp(1606753101, tz=utc))
self.assertEqual(event.message_id, 1568)
self.assertEqual(event.event_id, "9be13015-2e54-456c-bf66-eacbe33da824")
self.assertEqual(event.recipient, "suppressed@example.com")
self.assertEqual(event.reject_reason, None)
self.assertEqual(event.description,
"Recipient (suppressed@example.com) is on the suppression list")
self.assertEqual(event.mta_response,
"server output")
def test_failed_event(self):
raw_event = {
"event": "MessageDeliveryFailed",
"timestamp": 1606753101.084981,
"payload": {
"message": {
"id": 1571,
"token": "MzWWQPubXXWz",
"direction": "outgoing",
"message_id": "cfb29da8ed1e4ed5a6c8a0f24d7a9ef3@rp.postal.example.com",
"to": "failed@example.com",
"from": "test@example.com",
"subject": "Message delivery failed...",
"timestamp": 1606753318.072171,
"spam_status": "NotChecked",
"tag": None
},
"status": "HardFail",
"details": "Could not deliver",
"output": "server output",
"sent_with_ssl": None,
"timestamp": 1606753318.7010343,
"time": None
},
"uuid": "5fec5077-dae7-4989-94d5-e1963f3e9181"
}
response = self.client.post('/anymail/postal/tracking/',
content_type='application/json', data=json.dumps(raw_event))
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=PostalTrackingWebhookView,
event=ANY, esp_name='Postal')
event = kwargs['event']
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "failed")
self.assertEqual(event.esp_event, raw_event)
self.assertEqual(event.timestamp, datetime.fromtimestamp(1606753101, tz=utc))
self.assertEqual(event.message_id, 1571)
self.assertEqual(event.event_id, "5fec5077-dae7-4989-94d5-e1963f3e9181")
self.assertEqual(event.recipient, "failed@example.com")
self.assertEqual(event.reject_reason, None)
self.assertEqual(event.description,
"Could not deliver")
self.assertEqual(event.mta_response,
"server output")
def test_delivered_event(self):
raw_event = {
"event": "MessageSent",
"timestamp": 1606753101.354368,
"payload": {
"message": {
"id": 1563,
"token": "zw6psSlgo6ki",
"direction": "outgoing",
"message_id": "c462ad36-be49-469c-b7b2-dfd317eb40fa@rp.postal.example.com",
"to": "recipient@example.com",
"from": "test@example.com",
"subject": "Test Message at November 30, 2020 16:01",
"timestamp": 1606752104.699201,
"spam_status": "NotChecked",
"tag": "welcome-email"
},
"status": "Sent",
"details": "Message for recipient@example.com accepted",
"output": "250 2.0.0 OK\n",
"sent_with_ssl": False,
"timestamp": 1606752106.9858062,
"time": 0.89
},
"uuid": "58e8d7ee-2cd5-4db2-9af3-3f436105795a"
}
response = self.client.post('/anymail/postal/tracking/',
content_type='application/json', data=json.dumps(raw_event))
self.assertEqual(response.status_code, 200)
kwargs = self.assert_handler_called_once_with(self.tracking_handler, sender=PostalTrackingWebhookView,
event=ANY, esp_name='Postal')
event = kwargs['event']
self.assertIsInstance(event, AnymailTrackingEvent)
self.assertEqual(event.event_type, "delivered")
self.assertEqual(event.esp_event, raw_event)
self.assertEqual(event.timestamp, datetime.fromtimestamp(1606753101, tz=utc))
self.assertEqual(event.message_id, 1563)
self.assertEqual(event.recipient, "recipient@example.com")
self.assertEqual(event.tags, ["welcome-email"])
self.assertEqual(event.metadata, None)
def test_ignore_incoming_events(self):
raw_event = {
"event": "MessageDeliveryFailed",
"timestamp": 1606756014.694645,
"payload": {
"message": {
"id": 1575,
"token": "lPDuNhHfV8aU",
"direction": "incoming",
"message_id": "asdf@other-mta.example.com",
"to": "incoming@example.com",
"from": "sender@example.com",
"subject": "test",
"timestamp": 1606756008.718169,
"spam_status": "NotSpam",
"tag": None
},
"status": "HardFail",
"details": "Received a 400 from https://anymail.example.com/anymail/postal/tracking/.",
"output": "Not found",
"sent_with_ssl": False,
"timestamp": 1606756014.1078613,
"time": 0.15
},
"uuid": "a01724c0-0d1a-4090-89aa-c3da5a683375"
}
response = self.client.post('/anymail/postal/tracking/',
content_type='application/json', data=json.dumps(raw_event))
self.assertEqual(response.status_code, 200)
self.assertEqual(self.tracking_handler.call_count, 0)
def test_misconfigured_inbound(self):
errmsg = "You seem to have set Postal's *inbound* webhook to Anymail's Postal *tracking* webhook URL."
with self.assertRaisesMessage(AnymailConfigurationError, errmsg):
self.client.post('/anymail/postal/tracking/', content_type='application/json',
data=json.dumps({"rcpt_to": "to@example.org"}))