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:
@@ -1,9 +1,9 @@
|
||||
import json
|
||||
import warnings
|
||||
from email.utils import unquote
|
||||
|
||||
from django.utils.dateparse import parse_datetime
|
||||
|
||||
from ..exceptions import AnymailConfigurationError, AnymailWarning
|
||||
from ..exceptions import AnymailConfigurationError
|
||||
from ..inbound import AnymailInboundMessage
|
||||
from ..signals import (
|
||||
AnymailInboundEvent,
|
||||
@@ -161,77 +161,65 @@ class PostmarkInboundWebhookView(PostmarkBaseWebhookView):
|
||||
signal = inbound
|
||||
|
||||
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(
|
||||
"You seem to have set Postmark's *%s* webhook "
|
||||
"to Anymail's Postmark *inbound* webhook URL." % esp_event["RecordType"]
|
||||
f"You seem to have set Postmark's *{esp_record_type}* webhook"
|
||||
f" to Anymail's Postmark *inbound* webhook URL."
|
||||
)
|
||||
|
||||
attachments = [
|
||||
AnymailInboundMessage.construct_attachment(
|
||||
content_type=attachment["ContentType"],
|
||||
content=(
|
||||
attachment.get("Content")
|
||||
# WORKAROUND:
|
||||
# The test webhooks are not like their real webhooks
|
||||
# This allows the test webhooks to be parsed.
|
||||
or attachment["Data"]
|
||||
),
|
||||
base64=True,
|
||||
filename=attachment.get("Name", "") or None,
|
||||
content_id=attachment.get("ContentID", "") or None,
|
||||
headers = esp_event.get("Headers", [])
|
||||
|
||||
# Postmark inbound prepends "Return-Path" to Headers list
|
||||
# (but it doesn't appear in original message or RawEmail).
|
||||
# (A Return-Path anywhere else in the headers or RawEmail
|
||||
# can't be considered legitimate.)
|
||||
envelope_sender = None
|
||||
if len(headers) > 0 and headers[0]["Name"].lower() == "return-path":
|
||||
envelope_sender = unquote(headers[0]["Value"]) # remove <>
|
||||
headers = headers[1:] # don't include in message construction
|
||||
|
||||
if "RawEmail" in esp_event:
|
||||
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.
|
||||
for attachment in esp_event.get("Attachments", []):
|
||||
if "Data" in attachment:
|
||||
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.envelope_sender = envelope_sender
|
||||
message.envelope_recipient = esp_event.get("OriginalRecipient")
|
||||
message.stripped_text = esp_event.get("StrippedTextReply")
|
||||
|
||||
message.spam_detected = message.get("X-Spam-Status", "No").lower() == "yes"
|
||||
try:
|
||||
@@ -249,11 +237,11 @@ class PostmarkInboundWebhookView(PostmarkBaseWebhookView):
|
||||
message=message,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _address(full):
|
||||
@classmethod
|
||||
def _address(cls, full):
|
||||
"""
|
||||
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:
|
||||
return ""
|
||||
@@ -263,3 +251,13 @@ class PostmarkInboundWebhookView(PostmarkBaseWebhookView):
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user