mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
Postmark inbound: improve inbound parsing
- Support Postmark's RawEmail option; recommend it in docs - Handle Bcc when provided by Postmark - Obtain `envelope_sender` from Return-Path info Postmark now adds, rather than parsing Received-SPF Related: - Add `AnymailInboundMessage.bcc` convenience prop - Test against full Postmark "check" inbound payloads (which don't match their docs or real inbound payloads) - Don't warn about receiving "check" payload
This commit is contained in:
@@ -59,6 +59,19 @@ Breaking changes
|
|||||||
|
|
||||||
* Require urllib3 1.25 or later (released 2019-04-29).
|
* Require urllib3 1.25 or later (released 2019-04-29).
|
||||||
|
|
||||||
|
Features
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
* **Postmark inbound:**
|
||||||
|
|
||||||
|
* Handle Postmark's "Include raw email content in JSON payload"
|
||||||
|
inbound option. Enabling this setting is recommended to get
|
||||||
|
the most accurate representation of any received email.
|
||||||
|
* Obtain ``envelope_sender`` from *Return-Path* Postmark now provides.
|
||||||
|
(Replaces potentially faulty *Received-SPF* header parsing.)
|
||||||
|
* Add *Bcc* header to inbound message if provided. Postmark adds bcc
|
||||||
|
when the delivered-to address does not appear in the *To* header.
|
||||||
|
|
||||||
Other
|
Other
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,12 @@ class AnymailInboundMessage(Message):
|
|||||||
# equivalent to Python 3.2+ message['Cc'].addresses
|
# equivalent to Python 3.2+ message['Cc'].addresses
|
||||||
return self.get_address_header("Cc")
|
return self.get_address_header("Cc")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bcc(self):
|
||||||
|
"""list of EmailAddress objects from Bcc header"""
|
||||||
|
# equivalent to Python 3.2+ message['Bcc'].addresses
|
||||||
|
return self.get_address_header("Bcc")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def subject(self):
|
def subject(self):
|
||||||
"""str value of Subject header, or None"""
|
"""str value of Subject header, or None"""
|
||||||
@@ -233,6 +239,7 @@ class AnymailInboundMessage(Message):
|
|||||||
from_email=None,
|
from_email=None,
|
||||||
to=None,
|
to=None,
|
||||||
cc=None,
|
cc=None,
|
||||||
|
bcc=None,
|
||||||
subject=None,
|
subject=None,
|
||||||
headers=None,
|
headers=None,
|
||||||
text=None,
|
text=None,
|
||||||
@@ -252,6 +259,7 @@ class AnymailInboundMessage(Message):
|
|||||||
:param from_email: {str|None} value for From header
|
:param from_email: {str|None} value for From header
|
||||||
:param to: {str|None} value for To header
|
:param to: {str|None} value for To header
|
||||||
:param cc: {str|None} value for Cc header
|
:param cc: {str|None} value for Cc header
|
||||||
|
:param bcc: {str|None} value for Bcc header
|
||||||
:param subject: {str|None} value for Subject header
|
:param subject: {str|None} value for Subject header
|
||||||
:param headers: {sequence[(str, str)]|mapping|None} additional headers
|
:param headers: {sequence[(str, str)]|mapping|None} additional headers
|
||||||
:param text: {str|None} plaintext body
|
:param text: {str|None} plaintext body
|
||||||
@@ -279,6 +287,9 @@ class AnymailInboundMessage(Message):
|
|||||||
if cc is not None:
|
if cc is not None:
|
||||||
del msg["Cc"]
|
del msg["Cc"]
|
||||||
msg["Cc"] = cc
|
msg["Cc"] = cc
|
||||||
|
if bcc is not None:
|
||||||
|
del msg["Bcc"]
|
||||||
|
msg["Bcc"] = bcc
|
||||||
if subject is not None:
|
if subject is not None:
|
||||||
del msg["Subject"]
|
del msg["Subject"]
|
||||||
msg["Subject"] = subject
|
msg["Subject"] = subject
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import json
|
import json
|
||||||
import warnings
|
from email.utils import unquote
|
||||||
|
|
||||||
from django.utils.dateparse import parse_datetime
|
from django.utils.dateparse import parse_datetime
|
||||||
|
|
||||||
from ..exceptions import AnymailConfigurationError, AnymailWarning
|
from ..exceptions import AnymailConfigurationError
|
||||||
from ..inbound import AnymailInboundMessage
|
from ..inbound import AnymailInboundMessage
|
||||||
from ..signals import (
|
from ..signals import (
|
||||||
AnymailInboundEvent,
|
AnymailInboundEvent,
|
||||||
@@ -161,77 +161,65 @@ class PostmarkInboundWebhookView(PostmarkBaseWebhookView):
|
|||||||
signal = inbound
|
signal = inbound
|
||||||
|
|
||||||
def esp_to_anymail_event(self, esp_event):
|
def esp_to_anymail_event(self, esp_event):
|
||||||
if esp_event.get("RecordType", "Inbound") != "Inbound":
|
# Check correct webhook (inbound events don't have RecordType):
|
||||||
|
esp_record_type = esp_event.get("RecordType", "Inbound")
|
||||||
|
if esp_record_type != "Inbound":
|
||||||
raise AnymailConfigurationError(
|
raise AnymailConfigurationError(
|
||||||
"You seem to have set Postmark's *%s* webhook "
|
f"You seem to have set Postmark's *{esp_record_type}* webhook"
|
||||||
"to Anymail's Postmark *inbound* webhook URL." % esp_event["RecordType"]
|
f" to Anymail's Postmark *inbound* webhook URL."
|
||||||
)
|
)
|
||||||
|
|
||||||
attachments = [
|
headers = esp_event.get("Headers", [])
|
||||||
AnymailInboundMessage.construct_attachment(
|
|
||||||
content_type=attachment["ContentType"],
|
# Postmark inbound prepends "Return-Path" to Headers list
|
||||||
content=(
|
# (but it doesn't appear in original message or RawEmail).
|
||||||
attachment.get("Content")
|
# (A Return-Path anywhere else in the headers or RawEmail
|
||||||
# WORKAROUND:
|
# can't be considered legitimate.)
|
||||||
# The test webhooks are not like their real webhooks
|
envelope_sender = None
|
||||||
# This allows the test webhooks to be parsed.
|
if len(headers) > 0 and headers[0]["Name"].lower() == "return-path":
|
||||||
or attachment["Data"]
|
envelope_sender = unquote(headers[0]["Value"]) # remove <>
|
||||||
),
|
headers = headers[1:] # don't include in message construction
|
||||||
base64=True,
|
|
||||||
filename=attachment.get("Name", "") or None,
|
if "RawEmail" in esp_event:
|
||||||
content_id=attachment.get("ContentID", "") or None,
|
message = AnymailInboundMessage.parse_raw_mime(esp_event["RawEmail"])
|
||||||
|
# Postmark provides Bcc when delivered-to is not in To header,
|
||||||
|
# but doesn't add it to the RawEmail.
|
||||||
|
if esp_event.get("BccFull") and "Bcc" not in message:
|
||||||
|
message["Bcc"] = self._addresses(esp_event["BccFull"])
|
||||||
|
|
||||||
|
else:
|
||||||
|
# RawEmail not included in payload; construct from parsed data.
|
||||||
|
attachments = [
|
||||||
|
AnymailInboundMessage.construct_attachment(
|
||||||
|
content_type=attachment["ContentType"],
|
||||||
|
# Real payloads have "Content", test payloads have "Data" (?!):
|
||||||
|
content=attachment.get("Content") or attachment["Data"],
|
||||||
|
base64=True,
|
||||||
|
filename=attachment.get("Name"),
|
||||||
|
content_id=attachment.get("ContentID"),
|
||||||
|
)
|
||||||
|
for attachment in esp_event.get("Attachments", [])
|
||||||
|
]
|
||||||
|
message = AnymailInboundMessage.construct(
|
||||||
|
from_email=self._address(esp_event.get("FromFull")),
|
||||||
|
to=self._addresses(esp_event.get("ToFull")),
|
||||||
|
cc=self._addresses(esp_event.get("CcFull")),
|
||||||
|
bcc=self._addresses(esp_event.get("BccFull")),
|
||||||
|
subject=esp_event.get("Subject", ""),
|
||||||
|
headers=((header["Name"], header["Value"]) for header in headers),
|
||||||
|
text=esp_event.get("TextBody", ""),
|
||||||
|
html=esp_event.get("HtmlBody", ""),
|
||||||
|
attachments=attachments,
|
||||||
)
|
)
|
||||||
for attachment in esp_event.get("Attachments", [])
|
# Postmark strips these headers and provides them as separate event fields:
|
||||||
]
|
if esp_event.get("Date") and "Date" not in message:
|
||||||
|
message["Date"] = esp_event["Date"]
|
||||||
|
if esp_event.get("ReplyTo") and "Reply-To" not in message:
|
||||||
|
message["Reply-To"] = esp_event["ReplyTo"]
|
||||||
|
|
||||||
# Warning to the user regarding the workaround of above.
|
message.envelope_sender = envelope_sender
|
||||||
for attachment in esp_event.get("Attachments", []):
|
message.envelope_recipient = esp_event.get("OriginalRecipient")
|
||||||
if "Data" in attachment:
|
message.stripped_text = esp_event.get("StrippedTextReply")
|
||||||
warnings.warn(
|
|
||||||
"Received a test webhook attachment. "
|
|
||||||
"It is recommended to test with real inbound events. "
|
|
||||||
"See https://github.com/anymail/django-anymail/issues/304 "
|
|
||||||
"for more information.",
|
|
||||||
AnymailWarning,
|
|
||||||
)
|
|
||||||
break
|
|
||||||
|
|
||||||
message = AnymailInboundMessage.construct(
|
|
||||||
from_email=self._address(esp_event.get("FromFull")),
|
|
||||||
to=", ".join([self._address(to) for to in esp_event.get("ToFull", [])]),
|
|
||||||
cc=", ".join([self._address(cc) for cc in esp_event.get("CcFull", [])]),
|
|
||||||
# bcc? Postmark specs this for inbound events,
|
|
||||||
# but it's unclear how it could occur
|
|
||||||
subject=esp_event.get("Subject", ""),
|
|
||||||
headers=[
|
|
||||||
(header["Name"], header["Value"])
|
|
||||||
for header in esp_event.get("Headers", [])
|
|
||||||
],
|
|
||||||
text=esp_event.get("TextBody", ""),
|
|
||||||
html=esp_event.get("HtmlBody", ""),
|
|
||||||
attachments=attachments,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Postmark strips these headers and provides them as separate event fields:
|
|
||||||
if "Date" in esp_event and "Date" not in message:
|
|
||||||
message["Date"] = esp_event["Date"]
|
|
||||||
if "ReplyTo" in esp_event and "Reply-To" not in message:
|
|
||||||
message["Reply-To"] = esp_event["ReplyTo"]
|
|
||||||
|
|
||||||
# Postmark doesn't have a separate envelope-sender field, but it can
|
|
||||||
# be extracted from the Received-SPF header that Postmark will have added.
|
|
||||||
# (More than one Received-SPF? someone's up to something weird?)
|
|
||||||
if len(message.get_all("Received-SPF", [])) == 1:
|
|
||||||
received_spf = message["Received-SPF"].lower()
|
|
||||||
if received_spf.startswith( # not fail/softfail
|
|
||||||
"pass"
|
|
||||||
) or received_spf.startswith("neutral"):
|
|
||||||
message.envelope_sender = message.get_param(
|
|
||||||
"envelope-from", None, header="Received-SPF"
|
|
||||||
)
|
|
||||||
|
|
||||||
message.envelope_recipient = esp_event.get("OriginalRecipient", None)
|
|
||||||
message.stripped_text = esp_event.get("StrippedTextReply", None)
|
|
||||||
|
|
||||||
message.spam_detected = message.get("X-Spam-Status", "No").lower() == "yes"
|
message.spam_detected = message.get("X-Spam-Status", "No").lower() == "yes"
|
||||||
try:
|
try:
|
||||||
@@ -249,11 +237,11 @@ class PostmarkInboundWebhookView(PostmarkBaseWebhookView):
|
|||||||
message=message,
|
message=message,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def _address(full):
|
def _address(cls, full):
|
||||||
"""
|
"""
|
||||||
Return a formatted email address
|
Return a formatted email address
|
||||||
from a Postmark inbound {From,To,Cc}Full dict
|
from a Postmark inbound {From,To,Cc,Bcc}Full dict
|
||||||
"""
|
"""
|
||||||
if full is None:
|
if full is None:
|
||||||
return ""
|
return ""
|
||||||
@@ -263,3 +251,13 @@ class PostmarkInboundWebhookView(PostmarkBaseWebhookView):
|
|||||||
addr_spec=full.get("Email", ""),
|
addr_spec=full.get("Email", ""),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _addresses(cls, full_list):
|
||||||
|
"""
|
||||||
|
Return a formatted email address list string
|
||||||
|
from a Postmark inbound {To,Cc,Bcc}Full[] list of dicts
|
||||||
|
"""
|
||||||
|
if full_list is None:
|
||||||
|
return None
|
||||||
|
return ", ".join(cls._address(addr) for addr in full_list)
|
||||||
|
|||||||
@@ -244,19 +244,28 @@ a `dict` of Postmark `delivery <https://postmarkapp.com/developer/webhooks/deliv
|
|||||||
Inbound webhook
|
Inbound webhook
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
If you want to receive email from Postmark through Anymail's normalized :ref:`inbound <inbound>`
|
To receive email from Postmark through Anymail's normalized
|
||||||
handling, follow Postmark's `Inbound Processing`_ guide to configure
|
:ref:`inbound <inbound>` handling, follow Postmark's guide to
|
||||||
an inbound server pointing to Anymail's inbound webhook.
|
`Configure an inbound server`_ that posts to Anymail's inbound webhook.
|
||||||
|
|
||||||
The InboundHookUrl setting will be:
|
In their step 4, set the inbound webhook URL to:
|
||||||
|
|
||||||
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/postmark/inbound/`
|
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/postmark/inbound/`
|
||||||
|
|
||||||
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
|
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
|
||||||
* *yoursite.example.com* is your Django site
|
* *yoursite.example.com* is your Django site
|
||||||
|
|
||||||
Anymail handles the "parse an email" part of Postmark's instructions for you, but you'll
|
We recommend enabling the "Include raw email content in JSON payload" checkbox.
|
||||||
likely want to work through the other sections to set up a custom inbound domain, and
|
Anymail's inbound handling supports either choice, but raw email is preferred
|
||||||
perhaps configure inbound spam blocking.
|
to get the most accurate representation of any received message. (If you are using
|
||||||
|
Postmark's server API, this is the ``RawEmailEnabled`` option.)
|
||||||
|
|
||||||
|
.. versionchanged:: 10.0
|
||||||
|
Added handling for Postmark's "include raw email content".
|
||||||
|
|
||||||
|
You may also want to read through the "Inbound domain forwarding" and
|
||||||
|
"Configure inbound blocking" sections of Postmark's `Inbound Processing`_ guide.
|
||||||
|
|
||||||
|
.. _Configure an inbound server:
|
||||||
|
https://postmarkapp.com/developer/user-guide/inbound/configure-an-inbound-server
|
||||||
.. _Inbound Processing: https://postmarkapp.com/developer/user-guide/inbound
|
.. _Inbound Processing: https://postmarkapp.com/developer/user-guide/inbound
|
||||||
|
|||||||
64
tests/test_files/postmark-inbound-test-payload-with-raw.json
Normal file
64
tests/test_files/postmark-inbound-test-payload-with-raw.json
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"FromName": "Postmarkapp Support",
|
||||||
|
"MessageStream": "inbound",
|
||||||
|
"From": "support@postmarkapp.com",
|
||||||
|
"FromFull": {
|
||||||
|
"Email": "support@postmarkapp.com",
|
||||||
|
"Name": "Postmarkapp Support",
|
||||||
|
"MailboxHash": ""
|
||||||
|
},
|
||||||
|
"To": "\"Firstname Lastname\" <mailbox+SampleHash@inbound.postmarkapp.com>",
|
||||||
|
"ToFull": [
|
||||||
|
{
|
||||||
|
"Email": "mailbox+SampleHash@inbound.postmarkapp.com",
|
||||||
|
"Name": "Firstname Lastname",
|
||||||
|
"MailboxHash": "SampleHash"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Cc": "\"First Cc\" <firstcc@postmarkapp.com>, secondCc@postmarkapp.com",
|
||||||
|
"CcFull": [
|
||||||
|
{
|
||||||
|
"Email": "firstcc@postmarkapp.com",
|
||||||
|
"Name": "First Cc",
|
||||||
|
"MailboxHash": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Email": "secondCc@postmarkapp.com",
|
||||||
|
"Name": "",
|
||||||
|
"MailboxHash": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Bcc": "\"First Bcc\" <firstbcc@postmarkapp.com>",
|
||||||
|
"BccFull": [
|
||||||
|
{
|
||||||
|
"Email": "firstbcc@postmarkapp.com",
|
||||||
|
"Name": "First Bcc",
|
||||||
|
"MailboxHash": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"OriginalRecipient": "mailbox+SampleHash@inbound.postmarkapp.com",
|
||||||
|
"Subject": "Test subject",
|
||||||
|
"MessageID": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"ReplyTo": "replyto@example.com",
|
||||||
|
"MailboxHash": "SampleHash",
|
||||||
|
"Date": "Fri, 5 May 2023 17:41:16 -0400",
|
||||||
|
"TextBody": "This is a test text body.",
|
||||||
|
"HtmlBody": "<html><body><p>This is a test html body.<\/p><\/body><\/html>",
|
||||||
|
"StrippedTextReply": "This is the reply text",
|
||||||
|
"RawEmail": "From: Postmarkapp Support <support@postmarkapp.com>\r\nTo: Firstname Lastname <mailbox+SampleHash@inbound.postmarkapp.com>\r\nSubject: Test subject\r\n\r\nThis is a test text body.\r\n",
|
||||||
|
"Tag": "TestTag",
|
||||||
|
"Headers": [
|
||||||
|
{
|
||||||
|
"Name": "X-Header-Test",
|
||||||
|
"Value": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Attachments": [
|
||||||
|
{
|
||||||
|
"Name": "test.txt",
|
||||||
|
"ContentType": "text/plain",
|
||||||
|
"Data": "VGhpcyBpcyBhdHRhY2htZW50IGNvbnRlbnRzLCBiYXNlLTY0IGVuY29kZWQu",
|
||||||
|
"ContentLength": 45
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
63
tests/test_files/postmark-inbound-test-payload.json
Normal file
63
tests/test_files/postmark-inbound-test-payload.json
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"FromName": "Postmarkapp Support",
|
||||||
|
"MessageStream": "inbound",
|
||||||
|
"From": "support@postmarkapp.com",
|
||||||
|
"FromFull": {
|
||||||
|
"Email": "support@postmarkapp.com",
|
||||||
|
"Name": "Postmarkapp Support",
|
||||||
|
"MailboxHash": ""
|
||||||
|
},
|
||||||
|
"To": "\"Firstname Lastname\" <mailbox+SampleHash@inbound.postmarkapp.com>",
|
||||||
|
"ToFull": [
|
||||||
|
{
|
||||||
|
"Email": "mailbox+SampleHash@inbound.postmarkapp.com",
|
||||||
|
"Name": "Firstname Lastname",
|
||||||
|
"MailboxHash": "SampleHash"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Cc": "\"First Cc\" <firstcc@postmarkapp.com>, secondCc@postmarkapp.com",
|
||||||
|
"CcFull": [
|
||||||
|
{
|
||||||
|
"Email": "firstcc@postmarkapp.com",
|
||||||
|
"Name": "First Cc",
|
||||||
|
"MailboxHash": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Email": "secondCc@postmarkapp.com",
|
||||||
|
"Name": "",
|
||||||
|
"MailboxHash": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Bcc": "\"First Bcc\" <firstbcc@postmarkapp.com>",
|
||||||
|
"BccFull": [
|
||||||
|
{
|
||||||
|
"Email": "firstbcc@postmarkapp.com",
|
||||||
|
"Name": "First Bcc",
|
||||||
|
"MailboxHash": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"OriginalRecipient": "mailbox+SampleHash@inbound.postmarkapp.com",
|
||||||
|
"Subject": "Test subject",
|
||||||
|
"MessageID": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"ReplyTo": "replyto@example.com",
|
||||||
|
"MailboxHash": "SampleHash",
|
||||||
|
"Date": "Fri, 5 May 2023 17:44:33 -0400",
|
||||||
|
"TextBody": "This is a test text body.",
|
||||||
|
"HtmlBody": "<html><body><p>This is a test html body.<\/p><\/body><\/html>",
|
||||||
|
"StrippedTextReply": "This is the reply text",
|
||||||
|
"Tag": "TestTag",
|
||||||
|
"Headers": [
|
||||||
|
{
|
||||||
|
"Name": "X-Header-Test",
|
||||||
|
"Value": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Attachments": [
|
||||||
|
{
|
||||||
|
"Name": "test.txt",
|
||||||
|
"ContentType": "text/plain",
|
||||||
|
"Data": "VGhpcyBpcyBhdHRhY2htZW50IGNvbnRlbnRzLCBiYXNlLTY0IGVuY29kZWQu",
|
||||||
|
"ContentLength": 45
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -284,6 +284,7 @@ class AnymailInboundMessageConveniencePropTests(SimpleTestCase):
|
|||||||
from_email='"Sender, Inc." <sender@example.com>',
|
from_email='"Sender, Inc." <sender@example.com>',
|
||||||
to="First To <to1@example.com>, to2@example.com",
|
to="First To <to1@example.com>, to2@example.com",
|
||||||
cc="First Cc <cc1@example.com>, cc2@example.com",
|
cc="First Cc <cc1@example.com>, cc2@example.com",
|
||||||
|
bcc="bcc@example.com",
|
||||||
)
|
)
|
||||||
self.assertEqual(str(msg.from_email), '"Sender, Inc." <sender@example.com>')
|
self.assertEqual(str(msg.from_email), '"Sender, Inc." <sender@example.com>')
|
||||||
self.assertEqual(msg.from_email.addr_spec, "sender@example.com")
|
self.assertEqual(msg.from_email.addr_spec, "sender@example.com")
|
||||||
@@ -301,6 +302,9 @@ class AnymailInboundMessageConveniencePropTests(SimpleTestCase):
|
|||||||
self.assertEqual(msg.cc[0].address, "First Cc <cc1@example.com>")
|
self.assertEqual(msg.cc[0].address, "First Cc <cc1@example.com>")
|
||||||
self.assertEqual(msg.cc[1].address, "cc2@example.com")
|
self.assertEqual(msg.cc[1].address, "cc2@example.com")
|
||||||
|
|
||||||
|
self.assertEqual(len(msg.bcc), 1)
|
||||||
|
self.assertEqual(msg.bcc[0].address, "bcc@example.com")
|
||||||
|
|
||||||
# Default None/empty lists
|
# Default None/empty lists
|
||||||
msg = AnymailInboundMessage()
|
msg = AnymailInboundMessage()
|
||||||
self.assertIsNone(msg.from_email)
|
self.assertIsNone(msg.from_email)
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
import json
|
import json
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
|
from textwrap import dedent
|
||||||
from unittest.mock import ANY
|
from unittest.mock import ANY
|
||||||
|
|
||||||
from django.test import tag
|
from django.test import tag
|
||||||
|
|
||||||
from anymail.exceptions import AnymailConfigurationError, AnymailWarning
|
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.postmark import PostmarkInboundWebhookView
|
from anymail.webhooks.postmark import PostmarkInboundWebhookView
|
||||||
|
|
||||||
from .utils import sample_email_content, sample_image_content
|
from .utils import sample_email_content, sample_image_content, test_file_content
|
||||||
from .webhook_cases import WebhookTestCase
|
from .webhook_cases import WebhookTestCase
|
||||||
|
|
||||||
|
|
||||||
@tag("postmark")
|
@tag("postmark")
|
||||||
class PostmarkInboundTestCase(WebhookTestCase):
|
class PostmarkInboundTestCase(WebhookTestCase):
|
||||||
def test_inbound_basics(self):
|
def test_inbound_basics(self):
|
||||||
|
# without "Include raw email content in JSON payload"
|
||||||
raw_event = {
|
raw_event = {
|
||||||
"FromFull": {
|
"FromFull": {
|
||||||
"Email": "from+test@example.org",
|
"Email": "from+test@example.org",
|
||||||
@@ -32,9 +34,10 @@ class PostmarkInboundTestCase(WebhookTestCase):
|
|||||||
],
|
],
|
||||||
"CcFull": [{"Email": "cc@example.com", "Name": "", "MailboxHash": ""}],
|
"CcFull": [{"Email": "cc@example.com", "Name": "", "MailboxHash": ""}],
|
||||||
"BccFull": [
|
"BccFull": [
|
||||||
|
# Postmark provides Bcc if delivered-to address is not in To field:
|
||||||
{
|
{
|
||||||
"Email": "bcc@example.com",
|
"Email": "test@inbound.example.com",
|
||||||
"Name": "Postmark documents blind cc on inbound email (?)",
|
"Name": "",
|
||||||
"MailboxHash": "",
|
"MailboxHash": "",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -48,34 +51,29 @@ class PostmarkInboundTestCase(WebhookTestCase):
|
|||||||
"StrippedTextReply": "stripped plaintext body",
|
"StrippedTextReply": "stripped plaintext body",
|
||||||
"Tag": "",
|
"Tag": "",
|
||||||
"Headers": [
|
"Headers": [
|
||||||
|
{"Name": "Return-Path", "Value": "<envelope-from@example.org>"},
|
||||||
{
|
{
|
||||||
"Name": "Received",
|
"Name": "Received",
|
||||||
"Value": "from mail.example.org by inbound.postmarkapp.com ...",
|
"Value": "from mail.example.org by inbound.postmarkapp.com ...",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Name": "X-Spam-Checker-Version",
|
"Name": "X-Spam-Checker-Version",
|
||||||
"Value": "SpamAssassin 3.4.0 (2014-02-07)"
|
"Value": "SpamAssassin 3.4.0 (2014-02-07) on p-pm-smtp-inbound01b-aws-useast2b", # NOQA: E501
|
||||||
" onp-pm-smtp-inbound01b-aws-useast2b",
|
|
||||||
},
|
},
|
||||||
{"Name": "X-Spam-Status", "Value": "No"},
|
{"Name": "X-Spam-Status", "Value": "No"},
|
||||||
{"Name": "X-Spam-Score", "Value": "1.7"},
|
{"Name": "X-Spam-Score", "Value": "1.7"},
|
||||||
{"Name": "X-Spam-Tests", "Value": "SPF_PASS"},
|
{"Name": "X-Spam-Tests", "Value": "SPF_PASS"},
|
||||||
{
|
|
||||||
"Name": "Received-SPF",
|
|
||||||
"Value": "Pass (sender SPF authorized) identity=mailfrom;"
|
|
||||||
" client-ip=333.3.3.3;"
|
|
||||||
" helo=mail-02.example.org;"
|
|
||||||
" envelope-from=envelope-from@example.org;"
|
|
||||||
" receiver=test@inbound.example.com",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"Name": "Received",
|
"Name": "Received",
|
||||||
"Value": "by mail.example.org for <test@inbound.example.com> ...",
|
"Value": "by mail.example.org for <test@inbound.example.com> ...",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Name": "Received",
|
"Name": "Received",
|
||||||
"Value": "by 10.10.1.71 with HTTP;"
|
"Value": "by 10.10.1.71 with HTTP; Wed, 11 Oct 2017 18:31:04 -0700 (PDT)",
|
||||||
" Wed, 11 Oct 2017 18:31:04 -0700 (PDT)",
|
},
|
||||||
|
{
|
||||||
|
"Name": "Return-Path",
|
||||||
|
"Value": "<fake-return-path@postmark-should-have-removed>",
|
||||||
},
|
},
|
||||||
{"Name": "MIME-Version", "Value": "1.0"},
|
{"Name": "MIME-Version", "Value": "1.0"},
|
||||||
{"Name": "Message-ID", "Value": "<CAEPk3R+4Zr@mail.example.org>"},
|
{"Name": "Message-ID", "Value": "<CAEPk3R+4Zr@mail.example.org>"},
|
||||||
@@ -114,6 +112,7 @@ class PostmarkInboundTestCase(WebhookTestCase):
|
|||||||
["Test Inbound <test@inbound.example.com>", "other@example.com"],
|
["Test Inbound <test@inbound.example.com>", "other@example.com"],
|
||||||
)
|
)
|
||||||
self.assertEqual([str(e) for e in message.cc], ["cc@example.com"])
|
self.assertEqual([str(e) for e in message.cc], ["cc@example.com"])
|
||||||
|
self.assertEqual([str(e) for e in message.bcc], ["test@inbound.example.com"])
|
||||||
self.assertEqual(message.subject, "Test subject")
|
self.assertEqual(message.subject, "Test subject")
|
||||||
self.assertEqual(message.date.isoformat(" "), "2017-10-11 18:31:04-07:00")
|
self.assertEqual(message.date.isoformat(" "), "2017-10-11 18:31:04-07:00")
|
||||||
self.assertEqual(message.text, "Test body plain")
|
self.assertEqual(message.text, "Test body plain")
|
||||||
@@ -165,27 +164,14 @@ class PostmarkInboundTestCase(WebhookTestCase):
|
|||||||
"ContentType": 'message/rfc822; charset="us-ascii"',
|
"ContentType": 'message/rfc822; charset="us-ascii"',
|
||||||
"ContentLength": len(email_content),
|
"ContentLength": len(email_content),
|
||||||
},
|
},
|
||||||
# This is an attachement like send by the test webhook
|
|
||||||
# A workaround is implemented to handle it.
|
|
||||||
# Once Postmark solves the bug on their side this workaround
|
|
||||||
# can be reverted.
|
|
||||||
{
|
|
||||||
"Name": "test.txt",
|
|
||||||
"ContentType": "text/plain",
|
|
||||||
"Data": "VGhpcyBpcyBhdHRhY2htZW50IGNvbnRlbnRzLCBiYXNlLTY0IGVuY29kZWQu",
|
|
||||||
"ContentLength": 45,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
with self.assertWarnsRegex(
|
response = self.client.post(
|
||||||
AnymailWarning, r"Received a test webhook attachment. "
|
"/anymail/postmark/inbound/",
|
||||||
):
|
content_type="application/json",
|
||||||
response = self.client.post(
|
data=json.dumps(raw_event),
|
||||||
"/anymail/postmark/inbound/",
|
)
|
||||||
content_type="application/json",
|
|
||||||
data=json.dumps(raw_event),
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
kwargs = self.assert_handler_called_once_with(
|
kwargs = self.assert_handler_called_once_with(
|
||||||
self.inbound_handler,
|
self.inbound_handler,
|
||||||
@@ -196,7 +182,7 @@ class PostmarkInboundTestCase(WebhookTestCase):
|
|||||||
event = kwargs["event"]
|
event = kwargs["event"]
|
||||||
message = event.message
|
message = event.message
|
||||||
attachments = message.attachments # AnymailInboundMessage convenience accessor
|
attachments = message.attachments # AnymailInboundMessage convenience accessor
|
||||||
self.assertEqual(len(attachments), 3)
|
self.assertEqual(len(attachments), 2)
|
||||||
self.assertEqual(attachments[0].get_filename(), "test.txt")
|
self.assertEqual(attachments[0].get_filename(), "test.txt")
|
||||||
self.assertEqual(attachments[0].get_content_type(), "text/plain")
|
self.assertEqual(attachments[0].get_content_type(), "text/plain")
|
||||||
self.assertEqual(attachments[0].get_content_text(), "test attachment")
|
self.assertEqual(attachments[0].get_content_text(), "test attachment")
|
||||||
@@ -205,14 +191,6 @@ class PostmarkInboundTestCase(WebhookTestCase):
|
|||||||
attachments[1].get_content_bytes(), email_content
|
attachments[1].get_content_bytes(), email_content
|
||||||
)
|
)
|
||||||
|
|
||||||
# Attachment of test webhook
|
|
||||||
self.assertEqual(attachments[2].get_filename(), "test.txt")
|
|
||||||
self.assertEqual(attachments[2].get_content_type(), "text/plain")
|
|
||||||
self.assertEqual(
|
|
||||||
attachments[2].get_content_text(),
|
|
||||||
"This is attachment contents, base-64 encoded.",
|
|
||||||
)
|
|
||||||
|
|
||||||
inlines = message.inline_attachments
|
inlines = message.inline_attachments
|
||||||
self.assertEqual(len(inlines), 1)
|
self.assertEqual(len(inlines), 1)
|
||||||
inline = inlines["abc123"]
|
inline = inlines["abc123"]
|
||||||
@@ -220,119 +198,160 @@ class PostmarkInboundTestCase(WebhookTestCase):
|
|||||||
self.assertEqual(inline.get_content_type(), "image/png")
|
self.assertEqual(inline.get_content_type(), "image/png")
|
||||||
self.assertEqual(inline.get_content_bytes(), image_content)
|
self.assertEqual(inline.get_content_bytes(), image_content)
|
||||||
|
|
||||||
def test_envelope_sender(self):
|
def test_inbound_with_raw_email(self):
|
||||||
# Anymail extracts envelope-sender from Postmark Received-SPF header
|
# With "Include raw email content in JSON payload"
|
||||||
raw_event = {
|
raw_event = {
|
||||||
"Headers": [
|
# (Postmark's "RawEmail" actually uses \n rather than \r\n)
|
||||||
|
"RawEmail": dedent(
|
||||||
|
"""\
|
||||||
|
Received: from mail.example.org by inbound.postmarkapp.com ...
|
||||||
|
X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07)
|
||||||
|
\ton p-pm-smtp-inbound01b-aws-useast2b
|
||||||
|
X-Spam-Status: No
|
||||||
|
X-Spam-Score: 1.7
|
||||||
|
X-Spam-Tests: SPF_PASS
|
||||||
|
Received: by mail.example.org for <test@inbound.example.com> ...
|
||||||
|
Received: by 10.10.1.71 with HTTP; Wed, 11 Oct 2017 18:31:04 -0700 (PDT)
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Message-ID: <CAEPk3R+4Zr@mail.example.org>
|
||||||
|
Return-Path: <fake-return-path@postmark-should-have-removed>
|
||||||
|
From: "Displayed From" <from+test@example.org>
|
||||||
|
To: Test Inbound <test@inbound.example.com>, other@example.com
|
||||||
|
Cc: cc@example.com
|
||||||
|
Reply-To: from+test@milter.example.org
|
||||||
|
Subject: Test subject
|
||||||
|
Date: Wed, 11 Oct 2017 18:31:04 -0700
|
||||||
|
Content-Type: multipart/alternative; boundary="BoUnDaRy1"
|
||||||
|
|
||||||
|
--BoUnDaRy1
|
||||||
|
Content-Type: text/plain; charset="utf-8"
|
||||||
|
Content-Transfer-Encoding: 7bit
|
||||||
|
|
||||||
|
Test body plain
|
||||||
|
--BoUnDaRy1
|
||||||
|
Content-Type: multipart/related; boundary="bOuNdArY2"
|
||||||
|
|
||||||
|
--bOuNdArY2
|
||||||
|
Content-Type: text/html; charset="utf-8"
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
<div>Test body html</div>
|
||||||
|
--bOuNdArY2
|
||||||
|
Content-Type: text/plain
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
Content-Disposition: attachment; filename="attachment.txt"
|
||||||
|
|
||||||
|
This is an attachment
|
||||||
|
--bOuNdArY2--
|
||||||
|
|
||||||
|
--BoUnDaRy1--
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
"BccFull": [
|
||||||
|
# Postmark provides Bcc if delivered-to address is not in To field
|
||||||
|
# (but not in RawEmail)
|
||||||
{
|
{
|
||||||
"Name": "Received-SPF",
|
"Email": "test@inbound.example.com",
|
||||||
"Value": "Pass (sender SPF authorized) identity=mailfrom;"
|
"Name": "",
|
||||||
" client-ip=333.3.3.3;"
|
"MailboxHash": "",
|
||||||
" helo=mail-02.example.org;"
|
|
||||||
" envelope-from=envelope-from@example.org;"
|
|
||||||
" receiver=test@inbound.example.com",
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"OriginalRecipient": "test@inbound.example.com",
|
||||||
|
"MessageID": "22c74902-a0c1-4511-804f2-341342852c90",
|
||||||
|
"StrippedTextReply": "stripped plaintext body",
|
||||||
|
"Tag": "",
|
||||||
|
"Headers": [
|
||||||
|
{"Name": "Return-Path", "Value": "<envelope-from@example.org>"},
|
||||||
|
# ... All the other headers would be here ...
|
||||||
|
# This is a fake header (not in RawEmail) to make sure we only
|
||||||
|
# add headers in one place:
|
||||||
|
{
|
||||||
|
"Name": "X-No-Duplicates",
|
||||||
|
"Value": "headers only from RawEmail",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"Attachments": [
|
||||||
|
# ... Real attachments would appear here. ...
|
||||||
|
# This is a fake one (not in RawEmail) to make sure we only
|
||||||
|
# add attachments in one place:
|
||||||
|
{
|
||||||
|
"Name": "no-duplicates.txt",
|
||||||
|
"Content": b64encode("fake attachment".encode("utf-8")).decode(
|
||||||
|
"ascii"
|
||||||
|
),
|
||||||
|
"ContentType": "text/plain",
|
||||||
|
"ContentLength": len("fake attachment"),
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/anymail/postmark/inbound/",
|
"/anymail/postmark/inbound/",
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
data=json.dumps(raw_event),
|
data=json.dumps(raw_event),
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
kwargs = self.assert_handler_called_once_with(
|
||||||
|
self.inbound_handler,
|
||||||
|
sender=PostmarkInboundWebhookView,
|
||||||
|
event=ANY,
|
||||||
|
esp_name="Postmark",
|
||||||
|
)
|
||||||
|
# AnymailInboundEvent
|
||||||
|
event = kwargs["event"]
|
||||||
|
self.assertIsInstance(event, AnymailInboundEvent)
|
||||||
|
self.assertEqual(event.event_type, "inbound")
|
||||||
|
# Postmark doesn't provide inbound event timestamp:
|
||||||
|
self.assertIsNone(event.timestamp)
|
||||||
|
self.assertEqual(event.event_id, "22c74902-a0c1-4511-804f2-341342852c90")
|
||||||
|
self.assertIsInstance(event.message, AnymailInboundMessage)
|
||||||
|
self.assertEqual(event.esp_event, raw_event)
|
||||||
|
|
||||||
|
# AnymailInboundMessage - convenience properties
|
||||||
|
message = event.message
|
||||||
|
|
||||||
|
self.assertEqual(message.from_email.display_name, "Displayed From")
|
||||||
|
self.assertEqual(message.from_email.addr_spec, "from+test@example.org")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.get_kwargs(self.inbound_handler)["event"].message.envelope_sender,
|
[str(e) for e in message.to],
|
||||||
"envelope-from@example.org",
|
["Test Inbound <test@inbound.example.com>", "other@example.com"],
|
||||||
)
|
)
|
||||||
|
self.assertEqual([str(e) for e in message.cc], ["cc@example.com"])
|
||||||
|
self.assertEqual([str(e) for e in message.bcc], ["test@inbound.example.com"])
|
||||||
|
self.assertEqual(message.subject, "Test subject")
|
||||||
|
self.assertEqual(message.date.isoformat(" "), "2017-10-11 18:31:04-07:00")
|
||||||
|
self.assertEqual(message.text, "Test body plain")
|
||||||
|
self.assertEqual(message.html, "<div>Test body html</div>")
|
||||||
|
|
||||||
# Allow neutral SPF response
|
self.assertEqual(message.envelope_sender, "envelope-from@example.org")
|
||||||
self.client.post(
|
self.assertEqual(message.envelope_recipient, "test@inbound.example.com")
|
||||||
"/anymail/postmark/inbound/",
|
self.assertEqual(message.stripped_text, "stripped plaintext body")
|
||||||
content_type="application/json",
|
# Postmark doesn't provide stripped html:
|
||||||
data=json.dumps(
|
self.assertIsNone(message.stripped_html)
|
||||||
{
|
self.assertIs(message.spam_detected, False)
|
||||||
"Headers": [
|
self.assertEqual(message.spam_score, 1.7)
|
||||||
{
|
|
||||||
"Name": "Received-SPF",
|
# AnymailInboundMessage - other headers
|
||||||
"Value": "Neutral (no SPF record exists)"
|
self.assertEqual(message["Message-ID"], "<CAEPk3R+4Zr@mail.example.org>")
|
||||||
" identity=mailfrom;"
|
self.assertEqual(message["Reply-To"], "from+test@milter.example.org")
|
||||||
" envelope-from=envelope-from@example.org",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.get_kwargs(self.inbound_handler)["event"].message.envelope_sender,
|
message.get_all("Received"),
|
||||||
"envelope-from@example.org",
|
[
|
||||||
|
"from mail.example.org by inbound.postmarkapp.com ...",
|
||||||
|
"by mail.example.org for <test@inbound.example.com> ...",
|
||||||
|
"by 10.10.1.71 with HTTP; Wed, 11 Oct 2017 18:31:04 -0700 (PDT)",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ignore fail/softfail
|
# Attachments (from RawEmail only, not also from parsed):
|
||||||
self.client.post(
|
attachments = message.attachments
|
||||||
"/anymail/postmark/inbound/",
|
self.assertEqual(len(attachments), 1)
|
||||||
content_type="application/json",
|
self.assertEqual(attachments[0].get_filename(), "attachment.txt")
|
||||||
data=json.dumps(
|
self.assertEqual(attachments[0].get_content_type(), "text/plain")
|
||||||
{
|
self.assertEqual(attachments[0].get_content_text(), "This is an attachment")
|
||||||
"Headers": [
|
|
||||||
{
|
|
||||||
"Name": "Received-SPF",
|
|
||||||
"Value": "Fail (sender not SPF authorized)"
|
|
||||||
" identity=mailfrom;"
|
|
||||||
" envelope-from=spoofed@example.org",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self.assertIsNone(
|
|
||||||
self.get_kwargs(self.inbound_handler)["event"].message.envelope_sender
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ignore garbage
|
# Make sure we didn't load headers from both RawEmail and parsed:
|
||||||
self.client.post(
|
self.assertNotIn("X-No-Duplicates", message)
|
||||||
"/anymail/postmark/inbound/",
|
|
||||||
content_type="application/json",
|
|
||||||
data=json.dumps(
|
|
||||||
{
|
|
||||||
"Headers": [
|
|
||||||
{
|
|
||||||
"Name": "Received-SPF",
|
|
||||||
"Value": "ThisIsNotAValidReceivedSPFHeader@example.org",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self.assertIsNone(
|
|
||||||
self.get_kwargs(self.inbound_handler)["event"].message.envelope_sender
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ignore multiple Received-SPF headers
|
|
||||||
self.client.post(
|
|
||||||
"/anymail/postmark/inbound/",
|
|
||||||
content_type="application/json",
|
|
||||||
data=json.dumps(
|
|
||||||
{
|
|
||||||
"Headers": [
|
|
||||||
{
|
|
||||||
"Name": "Received-SPF",
|
|
||||||
"Value": "Fail (sender not SPF authorized)"
|
|
||||||
" identity=mailfrom;"
|
|
||||||
" envelope-from=spoofed@example.org",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Name": "Received-SPF",
|
|
||||||
"Value": "Pass (malicious sender added this)"
|
|
||||||
" identity=mailfrom;"
|
|
||||||
" envelope-from=spoofed@example.org",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self.assertIsNone(
|
|
||||||
self.get_kwargs(self.inbound_handler)["event"].message.envelope_sender
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_misconfigured_tracking(self):
|
def test_misconfigured_tracking(self):
|
||||||
errmsg = (
|
errmsg = (
|
||||||
@@ -345,3 +364,31 @@ class PostmarkInboundTestCase(WebhookTestCase):
|
|||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
data=json.dumps({"RecordType": "Delivery"}),
|
data=json.dumps({"RecordType": "Delivery"}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_check_payload(self):
|
||||||
|
# Postmark's "Check" button in the inbound webhook dashboard posts a static
|
||||||
|
# payload that doesn't match their docs or actual inbound message payloads.
|
||||||
|
# (Its attachments have "Data" rather than "Content".)
|
||||||
|
# They apparently have no plans to fix it, so make sure Anymail can handle it.
|
||||||
|
for filename in [
|
||||||
|
# Actual test payloads from 2023-05-05:
|
||||||
|
"postmark-inbound-test-payload.json",
|
||||||
|
"postmark-inbound-test-payload-with-raw.json",
|
||||||
|
]:
|
||||||
|
with self.subTest(filename=filename):
|
||||||
|
self.inbound_handler.reset_mock() # (subTest doesn't setUp/tearDown)
|
||||||
|
test_payload = test_file_content(filename)
|
||||||
|
response = self.client.post(
|
||||||
|
"/anymail/postmark/inbound/",
|
||||||
|
content_type="application/json",
|
||||||
|
data=test_payload,
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assert_handler_called_once_with(
|
||||||
|
self.inbound_handler,
|
||||||
|
sender=PostmarkInboundWebhookView,
|
||||||
|
event=ANY,
|
||||||
|
esp_name="Postmark",
|
||||||
|
)
|
||||||
|
# Don't care about the actual test message contents here,
|
||||||
|
# just want to make sure it parses and signals inbound without error.
|
||||||
|
|||||||
Reference in New Issue
Block a user