11import json
2- import warnings
2+ from email . utils import unquote
33
44from django .utils .dateparse import parse_datetime
55
6- from ..exceptions import AnymailConfigurationError , AnymailWarning
6+ from ..exceptions import AnymailConfigurationError
77from ..inbound import AnymailInboundMessage
88from ..signals import (
99 AnymailInboundEvent ,
@@ -161,77 +161,65 @@ class PostmarkInboundWebhookView(PostmarkBaseWebhookView):
161161 signal = inbound
162162
163163 def esp_to_anymail_event (self , esp_event ):
164- if esp_event .get ("RecordType" , "Inbound" ) != "Inbound" :
164+ # Check correct webhook (inbound events don't have RecordType):
165+ esp_record_type = esp_event .get ("RecordType" , "Inbound" )
166+ if esp_record_type != "Inbound" :
165167 raise AnymailConfigurationError (
166- "You seem to have set Postmark's *%s * webhook "
167- " to Anymail's Postmark *inbound* webhook URL." % esp_event [ "RecordType" ]
168+ f "You seem to have set Postmark's *{ esp_record_type } * webhook"
169+ f" to Anymail's Postmark *inbound* webhook URL."
168170 )
169171
170- attachments = [
171- AnymailInboundMessage .construct_attachment (
172- content_type = attachment ["ContentType" ],
173- content = (
174- attachment .get ("Content" )
175- # WORKAROUND:
176- # The test webhooks are not like their real webhooks
177- # This allows the test webhooks to be parsed.
178- or attachment ["Data" ]
179- ),
180- base64 = True ,
181- filename = attachment .get ("Name" , "" ) or None ,
182- content_id = attachment .get ("ContentID" , "" ) or None ,
183- )
184- for attachment in esp_event .get ("Attachments" , [])
185- ]
186-
187- # Warning to the user regarding the workaround of above.
188- for attachment in esp_event .get ("Attachments" , []):
189- if "Data" in attachment :
190- warnings .warn (
191- "Received a test webhook attachment. "
192- "It is recommended to test with real inbound events. "
193- "See https://github.com/anymail/django-anymail/issues/304 "
194- "for more information." ,
195- AnymailWarning ,
196- )
197- break
198-
199- message = AnymailInboundMessage .construct (
200- from_email = self ._address (esp_event .get ("FromFull" )),
201- to = ", " .join ([self ._address (to ) for to in esp_event .get ("ToFull" , [])]),
202- cc = ", " .join ([self ._address (cc ) for cc in esp_event .get ("CcFull" , [])]),
203- # bcc? Postmark specs this for inbound events,
204- # but it's unclear how it could occur
205- subject = esp_event .get ("Subject" , "" ),
206- headers = [
207- (header ["Name" ], header ["Value" ])
208- for header in esp_event .get ("Headers" , [])
209- ],
210- text = esp_event .get ("TextBody" , "" ),
211- html = esp_event .get ("HtmlBody" , "" ),
212- attachments = attachments ,
213- )
172+ headers = esp_event .get ("Headers" , [])
173+
174+ # Postmark inbound prepends "Return-Path" to Headers list
175+ # (but it doesn't appear in original message or RawEmail).
176+ # (A Return-Path anywhere else in the headers or RawEmail
177+ # can't be considered legitimate.)
178+ envelope_sender = None
179+ if len (headers ) > 0 and headers [0 ]["Name" ].lower () == "return-path" :
180+ envelope_sender = unquote (headers [0 ]["Value" ]) # remove <>
181+ headers = headers [1 :] # don't include in message construction
182+
183+ if "RawEmail" in esp_event :
184+ message = AnymailInboundMessage .parse_raw_mime (esp_event ["RawEmail" ])
185+ # Postmark provides Bcc when delivered-to is not in To header,
186+ # but doesn't add it to the RawEmail.
187+ if esp_event .get ("BccFull" ) and "Bcc" not in message :
188+ message ["Bcc" ] = self ._addresses (esp_event ["BccFull" ])
214189
215- # Postmark strips these headers and provides them as separate event fields:
216- if "Date" in esp_event and "Date" not in message :
217- message ["Date" ] = esp_event ["Date" ]
218- if "ReplyTo" in esp_event and "Reply-To" not in message :
219- message ["Reply-To" ] = esp_event ["ReplyTo" ]
220-
221- # Postmark doesn't have a separate envelope-sender field, but it can
222- # be extracted from the Received-SPF header that Postmark will have added.
223- # (More than one Received-SPF? someone's up to something weird?)
224- if len (message .get_all ("Received-SPF" , [])) == 1 :
225- received_spf = message ["Received-SPF" ].lower ()
226- if received_spf .startswith ( # not fail/softfail
227- "pass"
228- ) or received_spf .startswith ("neutral" ):
229- message .envelope_sender = message .get_param (
230- "envelope-from" , None , header = "Received-SPF"
190+ else :
191+ # RawEmail not included in payload; construct from parsed data.
192+ attachments = [
193+ AnymailInboundMessage .construct_attachment (
194+ content_type = attachment ["ContentType" ],
195+ # Real payloads have "Content", test payloads have "Data" (?!):
196+ content = attachment .get ("Content" ) or attachment ["Data" ],
197+ base64 = True ,
198+ filename = attachment .get ("Name" ),
199+ content_id = attachment .get ("ContentID" ),
231200 )
201+ for attachment in esp_event .get ("Attachments" , [])
202+ ]
203+ message = AnymailInboundMessage .construct (
204+ from_email = self ._address (esp_event .get ("FromFull" )),
205+ to = self ._addresses (esp_event .get ("ToFull" )),
206+ cc = self ._addresses (esp_event .get ("CcFull" )),
207+ bcc = self ._addresses (esp_event .get ("BccFull" )),
208+ subject = esp_event .get ("Subject" , "" ),
209+ headers = ((header ["Name" ], header ["Value" ]) for header in headers ),
210+ text = esp_event .get ("TextBody" , "" ),
211+ html = esp_event .get ("HtmlBody" , "" ),
212+ attachments = attachments ,
213+ )
214+ # Postmark strips these headers and provides them as separate event fields:
215+ if esp_event .get ("Date" ) and "Date" not in message :
216+ message ["Date" ] = esp_event ["Date" ]
217+ if esp_event .get ("ReplyTo" ) and "Reply-To" not in message :
218+ message ["Reply-To" ] = esp_event ["ReplyTo" ]
232219
233- message .envelope_recipient = esp_event .get ("OriginalRecipient" , None )
234- message .stripped_text = esp_event .get ("StrippedTextReply" , None )
220+ message .envelope_sender = envelope_sender
221+ message .envelope_recipient = esp_event .get ("OriginalRecipient" )
222+ message .stripped_text = esp_event .get ("StrippedTextReply" )
235223
236224 message .spam_detected = message .get ("X-Spam-Status" , "No" ).lower () == "yes"
237225 try :
@@ -249,11 +237,11 @@ def esp_to_anymail_event(self, esp_event):
249237 message = message ,
250238 )
251239
252- @staticmethod
253- def _address (full ):
240+ @classmethod
241+ def _address (cls , full ):
254242 """
255243 Return a formatted email address
256- from a Postmark inbound {From,To,Cc}Full dict
244+ from a Postmark inbound {From,To,Cc,Bcc }Full dict
257245 """
258246 if full is None :
259247 return ""
@@ -263,3 +251,13 @@ def _address(full):
263251 addr_spec = full .get ("Email" , "" ),
264252 )
265253 )
254+
255+ @classmethod
256+ def _addresses (cls , full_list ):
257+ """
258+ Return a formatted email address list string
259+ from a Postmark inbound {To,Cc,Bcc}Full[] list of dicts
260+ """
261+ if full_list is None :
262+ return None
263+ return ", " .join (cls ._address (addr ) for addr in full_list )
0 commit comments