mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
Add inbound mail handling
Add normalized event, signal, and webhooks for inbound mail. Closes #43 Closes #86
This commit is contained in:
@@ -4,12 +4,14 @@ from datetime import datetime
|
||||
from django.utils.timezone import utc
|
||||
|
||||
from .base import AnymailBaseWebhookView
|
||||
from ..signals import tracking, AnymailTrackingEvent, EventType, RejectReason
|
||||
from ..inbound import AnymailInboundMessage
|
||||
from ..signals import inbound, tracking, AnymailInboundEvent, AnymailTrackingEvent, EventType, RejectReason
|
||||
|
||||
|
||||
class MailjetTrackingWebhookView(AnymailBaseWebhookView):
|
||||
"""Handler for Mailjet delivery and engagement tracking webhooks"""
|
||||
|
||||
esp_name = "Mailjet"
|
||||
signal = tracking
|
||||
|
||||
def parse_events(self, request):
|
||||
@@ -95,3 +97,84 @@ class MailjetTrackingWebhookView(AnymailBaseWebhookView):
|
||||
user_agent=esp_event.get('agent', None),
|
||||
esp_event=esp_event,
|
||||
)
|
||||
|
||||
|
||||
class MailjetInboundWebhookView(AnymailBaseWebhookView):
|
||||
"""Handler for Mailjet inbound (parse API) webhook"""
|
||||
|
||||
esp_name = "Mailjet"
|
||||
signal = inbound
|
||||
|
||||
def parse_events(self, request):
|
||||
esp_event = json.loads(request.body.decode('utf-8'))
|
||||
return [self.esp_to_anymail_event(esp_event)]
|
||||
|
||||
def esp_to_anymail_event(self, esp_event):
|
||||
# You could _almost_ reconstruct the raw mime message from Mailjet's Headers and Parts fields,
|
||||
# but it's not clear which multipart boundary to use on each individual Part. Although each Part's
|
||||
# Content-Type header still has the multipart boundary, not knowing the parent part means typical
|
||||
# nested multipart structures can't be reliably recovered from the data Mailjet provides.
|
||||
# We'll just use our standarized multipart inbound constructor.
|
||||
|
||||
headers = self._flatten_mailjet_headers(esp_event.get("Headers", {}))
|
||||
attachments = [
|
||||
self._construct_mailjet_attachment(part, esp_event)
|
||||
for part in esp_event.get("Parts", [])
|
||||
if "Attachment" in part.get("ContentRef", "") # Attachment<N> or InlineAttachment<N>
|
||||
]
|
||||
message = AnymailInboundMessage.construct(
|
||||
headers=headers,
|
||||
text=esp_event.get("Text-part", None),
|
||||
html=esp_event.get("Html-part", None),
|
||||
attachments=attachments,
|
||||
)
|
||||
|
||||
message.envelope_sender = esp_event.get("Sender", None)
|
||||
message.envelope_recipient = esp_event.get("Recipient", None)
|
||||
|
||||
message.spam_detected = None # Mailjet doesn't provide a boolean; you'll have to interpret spam_score
|
||||
try:
|
||||
message.spam_score = float(esp_event['SpamAssassinScore'])
|
||||
except (KeyError, TypeError, ValueError):
|
||||
pass
|
||||
|
||||
return AnymailInboundEvent(
|
||||
event_type=EventType.INBOUND,
|
||||
timestamp=None, # Mailjet doesn't provide inbound event timestamp (esp_event['Date'] is time sent)
|
||||
event_id=None, # Mailjet doesn't provide an idempotent inbound event id
|
||||
esp_event=esp_event,
|
||||
message=message,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _flatten_mailjet_headers(headers):
|
||||
"""Convert Mailjet's dict-of-strings-and/or-lists header format to our list-of-name-value-pairs
|
||||
|
||||
{'name1': 'value', 'name2': ['value1', 'value2']}
|
||||
--> [('name1', 'value'), ('name2', 'value1'), ('name2', 'value2')]
|
||||
"""
|
||||
result = []
|
||||
for name, values in headers.items():
|
||||
if isinstance(values, list): # Mailjet groups repeated headers together as a list of values
|
||||
for value in values:
|
||||
result.append((name, value))
|
||||
else:
|
||||
result.append((name, values)) # single-valued (non-list) header
|
||||
return result
|
||||
|
||||
def _construct_mailjet_attachment(self, part, esp_event):
|
||||
# Mailjet includes unparsed attachment headers in each part; it's easiest to temporarily
|
||||
# attach them to a MIMEPart for parsing. (We could just turn this into the attachment,
|
||||
# but we want to use the payload handling from AnymailInboundMessage.construct_attachment later.)
|
||||
part_headers = AnymailInboundMessage() # temporary container for parsed attachment headers
|
||||
for name, value in self._flatten_mailjet_headers(part.get("Headers", {})):
|
||||
part_headers.add_header(name, value)
|
||||
|
||||
content_base64 = esp_event[part["ContentRef"]] # Mailjet *always* base64-encodes attachments
|
||||
|
||||
return AnymailInboundMessage.construct_attachment(
|
||||
content_type=part_headers.get_content_type(),
|
||||
content=content_base64, base64=True,
|
||||
filename=part_headers.get_filename(None),
|
||||
content_id=part_headers.get("Content-ID", "") or None,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user