Reformat code with automated tools

Apply standardized code style
This commit is contained in:
medmunds
2023-02-06 12:27:43 -08:00
committed by Mike Edmunds
parent 40891fcb4a
commit b4e22c63b3
94 changed files with 12936 additions and 7443 deletions

View File

@@ -1,10 +1,16 @@
import json
from datetime import datetime, timezone
from .base import AnymailBaseWebhookView
from ..inbound import AnymailInboundMessage
from ..signals import inbound, tracking, AnymailInboundEvent, AnymailTrackingEvent, EventType, RejectReason
from ..signals import (
AnymailInboundEvent,
AnymailTrackingEvent,
EventType,
RejectReason,
inbound,
tracking,
)
from .base import AnymailBaseWebhookView
class MailjetTrackingWebhookView(AnymailBaseWebhookView):
@@ -14,7 +20,7 @@ class MailjetTrackingWebhookView(AnymailBaseWebhookView):
signal = tracking
def parse_events(self, request):
esp_events = json.loads(request.body.decode('utf-8'))
esp_events = json.loads(request.body.decode("utf-8"))
# Mailjet webhook docs say the payload is "a JSON array of event objects,"
# but that's not true if "group events" isn't enabled in webhook config...
try:
@@ -28,65 +34,71 @@ class MailjetTrackingWebhookView(AnymailBaseWebhookView):
# https://dev.mailjet.com/guides/#events
event_types = {
# Map Mailjet event: Anymail normalized type
'sent': EventType.DELIVERED, # accepted by receiving MTA
'open': EventType.OPENED,
'click': EventType.CLICKED,
'bounce': EventType.BOUNCED,
'blocked': EventType.REJECTED,
'spam': EventType.COMPLAINED,
'unsub': EventType.UNSUBSCRIBED,
"sent": EventType.DELIVERED, # accepted by receiving MTA
"open": EventType.OPENED,
"click": EventType.CLICKED,
"bounce": EventType.BOUNCED,
"blocked": EventType.REJECTED,
"spam": EventType.COMPLAINED,
"unsub": EventType.UNSUBSCRIBED,
}
reject_reasons = {
# Map Mailjet error strings to Anymail normalized reject_reason
# error_related_to: recipient
'user unknown': RejectReason.BOUNCED,
'mailbox inactive': RejectReason.BOUNCED,
'quota exceeded': RejectReason.BOUNCED,
'blacklisted': RejectReason.BLOCKED, # might also be previous unsubscribe
'spam reporter': RejectReason.SPAM,
"user unknown": RejectReason.BOUNCED,
"mailbox inactive": RejectReason.BOUNCED,
"quota exceeded": RejectReason.BOUNCED,
"blacklisted": RejectReason.BLOCKED, # might also be previous unsubscribe
"spam reporter": RejectReason.SPAM,
# error_related_to: domain
'invalid domain': RejectReason.BOUNCED,
'no mail host': RejectReason.BOUNCED,
'relay/access denied': RejectReason.BOUNCED,
'greylisted': RejectReason.OTHER, # see special handling below
'typofix': RejectReason.INVALID,
# error_related_to: spam (all Mailjet policy/filtering; see above for spam complaints)
'sender blocked': RejectReason.BLOCKED,
'content blocked': RejectReason.BLOCKED,
'policy issue': RejectReason.BLOCKED,
"invalid domain": RejectReason.BOUNCED,
"no mail host": RejectReason.BOUNCED,
"relay/access denied": RejectReason.BOUNCED,
"greylisted": RejectReason.OTHER, # see special handling below
"typofix": RejectReason.INVALID,
# error_related_to: spam
# (all Mailjet policy/filtering; see above for spam complaints)
"sender blocked": RejectReason.BLOCKED,
"content blocked": RejectReason.BLOCKED,
"policy issue": RejectReason.BLOCKED,
# error_related_to: mailjet
'preblocked': RejectReason.BLOCKED,
'duplicate in campaign': RejectReason.OTHER,
"preblocked": RejectReason.BLOCKED,
"duplicate in campaign": RejectReason.OTHER,
}
def esp_to_anymail_event(self, esp_event):
event_type = self.event_types.get(esp_event['event'], EventType.UNKNOWN)
if esp_event.get('error', None) == 'greylisted' and not esp_event.get('hard_bounce', False):
# "This is a temporary error due to possible unrecognised senders. Delivery will be re-attempted."
event_type = self.event_types.get(esp_event["event"], EventType.UNKNOWN)
if esp_event.get("error", None) == "greylisted" and not esp_event.get(
"hard_bounce", False
):
# "This is a temporary error due to possible unrecognised senders.
# Delivery will be re-attempted."
event_type = EventType.DEFERRED
try:
timestamp = datetime.fromtimestamp(esp_event['time'], tz=timezone.utc)
timestamp = datetime.fromtimestamp(esp_event["time"], tz=timezone.utc)
except (KeyError, ValueError):
timestamp = None
try:
# convert bigint MessageID to str to match backend AnymailRecipientStatus
message_id = str(esp_event['MessageID'])
message_id = str(esp_event["MessageID"])
except (KeyError, TypeError):
message_id = None
if 'error' in esp_event:
reject_reason = self.reject_reasons.get(esp_event['error'], RejectReason.OTHER)
if "error" in esp_event:
reject_reason = self.reject_reasons.get(
esp_event["error"], RejectReason.OTHER
)
else:
reject_reason = None
tag = esp_event.get('customcampaign', None)
tag = esp_event.get("customcampaign", None)
tags = [tag] if tag else []
try:
metadata = json.loads(esp_event['Payload'])
metadata = json.loads(esp_event["Payload"])
except (KeyError, ValueError):
metadata = {}
@@ -95,13 +107,13 @@ class MailjetTrackingWebhookView(AnymailBaseWebhookView):
timestamp=timestamp,
message_id=message_id,
event_id=None,
recipient=esp_event.get('email', None),
recipient=esp_event.get("email", None),
reject_reason=reject_reason,
mta_response=esp_event.get('smtp_reply', None),
mta_response=esp_event.get("smtp_reply", None),
tags=tags,
metadata=metadata,
click_url=esp_event.get('url', None),
user_agent=esp_event.get('agent', None),
click_url=esp_event.get("url", None),
user_agent=esp_event.get("agent", None),
esp_event=esp_event,
)
@@ -113,21 +125,23 @@ class MailjetInboundWebhookView(AnymailBaseWebhookView):
signal = inbound
def parse_events(self, request):
esp_event = json.loads(request.body.decode('utf-8'))
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.
# 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.
# Just use our standardized 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>
# if ContentRef is Attachment<N> or InlineAttachment<N>:
if "Attachment" in part.get("ContentRef", "")
]
message = AnymailInboundMessage.construct(
headers=headers,
@@ -139,49 +153,62 @@ class MailjetInboundWebhookView(AnymailBaseWebhookView):
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
# Mailjet doesn't provide a spam boolean; you'll have to interpret spam_score
message.spam_detected = None
try:
message.spam_score = float(esp_event['SpamAssassinScore'])
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
# Mailjet doesn't provide inbound event timestamp
# (esp_event["Date"] is time sent):
timestamp=None,
# Mailjet doesn't provide an idempotent inbound event id:
event_id=None,
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
"""
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
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
# single-valued (non-list) header
result.append((name, values))
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
# 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.)
# temporary container for parsed attachment headers:
part_headers = AnymailInboundMessage()
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
# Mailjet *always* base64-encodes attachments
content_base64 = esp_event[part["ContentRef"]]
return AnymailInboundMessage.construct_attachment(
content_type=part_headers.get_content_type(),
content=content_base64, base64=True,
content=content_base64,
base64=True,
filename=part_headers.get_filename(None),
content_id=part_headers.get("Content-ID", "") or None,
)