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

@@ -3,10 +3,16 @@ from datetime import datetime, timezone
from email.parser import BytesParser
from email.policy import default as default_policy
from .base import AnymailBaseWebhookView
from ..inbound import AnymailInboundMessage
from ..signals import AnymailInboundEvent, AnymailTrackingEvent, EventType, RejectReason, inbound, tracking
from ..signals import (
AnymailInboundEvent,
AnymailTrackingEvent,
EventType,
RejectReason,
inbound,
tracking,
)
from .base import AnymailBaseWebhookView
class SendGridTrackingWebhookView(AnymailBaseWebhookView):
@@ -16,47 +22,50 @@ class SendGridTrackingWebhookView(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"))
return [self.esp_to_anymail_event(esp_event) for esp_event in esp_events]
event_types = {
# Map SendGrid event: Anymail normalized type
'bounce': EventType.BOUNCED,
'deferred': EventType.DEFERRED,
'delivered': EventType.DELIVERED,
'dropped': EventType.REJECTED,
'processed': EventType.QUEUED,
'click': EventType.CLICKED,
'open': EventType.OPENED,
'spamreport': EventType.COMPLAINED,
'unsubscribe': EventType.UNSUBSCRIBED,
'group_unsubscribe': EventType.UNSUBSCRIBED,
'group_resubscribe': EventType.SUBSCRIBED,
"bounce": EventType.BOUNCED,
"deferred": EventType.DEFERRED,
"delivered": EventType.DELIVERED,
"dropped": EventType.REJECTED,
"processed": EventType.QUEUED,
"click": EventType.CLICKED,
"open": EventType.OPENED,
"spamreport": EventType.COMPLAINED,
"unsubscribe": EventType.UNSUBSCRIBED,
"group_unsubscribe": EventType.UNSUBSCRIBED,
"group_resubscribe": EventType.SUBSCRIBED,
}
reject_reasons = {
# Map SendGrid reason/type strings (lowercased) to Anymail normalized reject_reason
'invalid': RejectReason.INVALID,
'unsubscribed address': RejectReason.UNSUBSCRIBED,
'bounce': RejectReason.BOUNCED,
'blocked': RejectReason.BLOCKED,
'expired': RejectReason.TIMED_OUT,
# Map SendGrid reason/type strings (lowercased)
# to Anymail normalized reject_reason
"invalid": RejectReason.INVALID,
"unsubscribed address": RejectReason.UNSUBSCRIBED,
"bounce": RejectReason.BOUNCED,
"blocked": RejectReason.BLOCKED,
"expired": RejectReason.TIMED_OUT,
}
def esp_to_anymail_event(self, esp_event):
event_type = self.event_types.get(esp_event['event'], EventType.UNKNOWN)
event_type = self.event_types.get(esp_event["event"], EventType.UNKNOWN)
try:
timestamp = datetime.fromtimestamp(esp_event['timestamp'], tz=timezone.utc)
timestamp = datetime.fromtimestamp(esp_event["timestamp"], tz=timezone.utc)
except (KeyError, ValueError):
timestamp = None
if esp_event['event'] == 'dropped':
mta_response = None # dropped at ESP before even getting to MTA
reason = esp_event.get('type', esp_event.get('reason', '')) # cause could be in 'type' or 'reason'
if esp_event["event"] == "dropped":
# message dropped at ESP before even getting to MTA:
mta_response = None
# cause could be in "type" or "reason":
reason = esp_event.get("type", esp_event.get("reason", ""))
reject_reason = self.reject_reasons.get(reason.lower(), RejectReason.OTHER)
else:
# MTA response is in 'response' for delivered; 'reason' for bounce
mta_response = esp_event.get('response', esp_event.get('reason', None))
# MTA response is in "response" for delivered; "reason" for bounce
mta_response = esp_event.get("response", esp_event.get("reason", None))
reject_reason = None
# SendGrid merges metadata ('unique_args') with the event.
@@ -73,49 +82,50 @@ class SendGridTrackingWebhookView(AnymailBaseWebhookView):
return AnymailTrackingEvent(
event_type=event_type,
timestamp=timestamp,
message_id=esp_event.get('anymail_id', esp_event.get('smtp-id')), # backwards compatibility
event_id=esp_event.get('sg_event_id', None),
recipient=esp_event.get('email', None),
# (smtp-id for backwards compatibility)
message_id=esp_event.get("anymail_id", esp_event.get("smtp-id")),
event_id=esp_event.get("sg_event_id", None),
recipient=esp_event.get("email", None),
reject_reason=reject_reason,
mta_response=mta_response,
tags=esp_event.get('category', []),
tags=esp_event.get("category", []),
metadata=metadata,
click_url=esp_event.get('url', None),
user_agent=esp_event.get('useragent', None),
click_url=esp_event.get("url", None),
user_agent=esp_event.get("useragent", None),
esp_event=esp_event,
)
# Known keys in SendGrid events (used to recover metadata above)
sendgrid_event_keys = {
'anymail_id',
'asm_group_id',
'attempt', # MTA deferred count
'category',
'cert_err',
'email',
'event',
'ip',
'marketing_campaign_id',
'marketing_campaign_name',
'newsletter', # ???
'nlvx_campaign_id',
'nlvx_campaign_split_id',
'nlvx_user_id',
'pool',
'post_type',
'reason', # MTA bounce/drop reason; SendGrid suppression reason
'response', # MTA deferred/delivered message
'send_at',
'sg_event_id',
'sg_message_id',
'smtp-id',
'status', # SMTP status code
'timestamp',
'tls',
'type', # suppression reject reason ("bounce", "blocked", "expired")
'url', # click tracking
'url_offset', # click tracking
'useragent', # click/open tracking
"anymail_id",
"asm_group_id",
"attempt", # MTA deferred count
"category",
"cert_err",
"email",
"event",
"ip",
"marketing_campaign_id",
"marketing_campaign_name",
"newsletter", # ???
"nlvx_campaign_id",
"nlvx_campaign_split_id",
"nlvx_user_id",
"pool",
"post_type",
"reason", # MTA bounce/drop reason; SendGrid suppression reason
"response", # MTA deferred/delivered message
"send_at",
"sg_event_id",
"sg_message_id",
"smtp-id",
"status", # SMTP status code
"timestamp",
"tls",
"type", # suppression reject reason ("bounce", "blocked", "expired")
"url", # click tracking
"url_offset", # click tracking
"useragent", # click/open tracking
}
@@ -129,39 +139,46 @@ class SendGridInboundWebhookView(AnymailBaseWebhookView):
return [self.esp_to_anymail_event(request)]
def esp_to_anymail_event(self, request):
# Inbound uses the entire Django request as esp_event, because we need POST and FILES.
# Note that request.POST is case-sensitive (unlike email.message.Message headers).
# Inbound uses the entire Django request as esp_event, because we need
# POST and FILES. Note that request.POST is case-sensitive (unlike
# email.message.Message headers).
esp_event = request
# Must access body before any POST fields, or it won't be available if we need
# it later (see text_charset and html_charset handling below).
_ensure_body_is_available_later = request.body # noqa: F841
if 'headers' in request.POST:
if "headers" in request.POST:
# Default (not "Send Raw") inbound fields
message = self.message_from_sendgrid_parsed(esp_event)
elif 'email' in request.POST:
elif "email" in request.POST:
# "Send Raw" full MIME
message = AnymailInboundMessage.parse_raw_mime(request.POST['email'])
message = AnymailInboundMessage.parse_raw_mime(request.POST["email"])
else:
raise KeyError("Invalid SendGrid inbound event data (missing both 'headers' and 'email' fields)")
raise KeyError(
"Invalid SendGrid inbound event data"
" (missing both 'headers' and 'email' fields)"
)
try:
envelope = json.loads(request.POST['envelope'])
envelope = json.loads(request.POST["envelope"])
except (KeyError, TypeError, ValueError):
pass
else:
message.envelope_sender = envelope['from']
message.envelope_recipient = envelope['to'][0]
message.envelope_sender = envelope["from"]
message.envelope_recipient = envelope["to"][0]
message.spam_detected = None # no simple boolean field; would need to parse the spam_report
# no simple boolean spam; would need to parse the spam_report
message.spam_detected = None
try:
message.spam_score = float(request.POST['spam_score'])
message.spam_score = float(request.POST["spam_score"])
except (KeyError, TypeError, ValueError):
pass
return AnymailInboundEvent(
event_type=EventType.INBOUND,
timestamp=None, # SendGrid doesn't provide an inbound event timestamp
event_id=None, # SendGrid doesn't provide an idempotent inbound message event id
# SendGrid doesn't provide an inbound event timestamp:
timestamp=None,
# SendGrid doesn't provide an idempotent inbound message event id:
event_id=None,
esp_event=esp_event,
message=message,
)
@@ -170,12 +187,12 @@ class SendGridInboundWebhookView(AnymailBaseWebhookView):
"""Construct a Message from SendGrid's "default" (non-raw) fields"""
try:
charsets = json.loads(request.POST['charsets'])
charsets = json.loads(request.POST["charsets"])
except (KeyError, ValueError):
charsets = {}
try:
attachment_info = json.loads(request.POST['attachment-info'])
attachment_info = json.loads(request.POST["attachment-info"])
except (KeyError, ValueError):
attachments = None
else:
@@ -186,44 +203,60 @@ class SendGridInboundWebhookView(AnymailBaseWebhookView):
file = request.FILES[attachment_id]
except KeyError:
# Django's multipart/form-data handling drops FILES with certain
# filenames (for security) or with empty filenames (Django ticket 15879).
# (To avoid this problem, enable SendGrid's "raw, full MIME" inbound option.)
# filenames (for security) or with empty filenames (Django ticket
# 15879). (To avoid this problem, enable SendGrid's "raw, full MIME"
# inbound option.)
pass
else:
# (This deliberately ignores attachment_info[attachment_id]["filename"],
# (This deliberately ignores
# attachment_info[attachment_id]["filename"],
# which has not passed through Django's filename sanitization.)
content_id = attachment_info[attachment_id].get("content-id")
attachment = AnymailInboundMessage.construct_attachment_from_uploaded_file(
file, content_id=content_id)
attachment = (
AnymailInboundMessage.construct_attachment_from_uploaded_file(
file, content_id=content_id
)
)
attachments.append(attachment)
default_charset = request.POST.encoding.lower() # (probably utf-8)
text = request.POST.get('text')
text_charset = charsets.get('text', default_charset).lower()
html = request.POST.get('html')
html_charset = charsets.get('html', default_charset).lower()
if (text and text_charset != default_charset) or (html and html_charset != default_charset):
text = request.POST.get("text")
text_charset = charsets.get("text", default_charset).lower()
html = request.POST.get("html")
html_charset = charsets.get("html", default_charset).lower()
if (text and text_charset != default_charset) or (
html and html_charset != default_charset
):
# Django has parsed text and/or html fields using the wrong charset.
# We need to re-parse the raw form data and decode each field separately,
# using the indicated charsets. The email package parses multipart/form-data
# retaining bytes content. (In theory, we could instead just change
# request.encoding and access the POST fields again, per Django docs,
# but that seems to be have bugs around the cached request._files.)
raw_data = b"".join([
b"Content-Type: ", request.META['CONTENT_TYPE'].encode('ascii'),
b"\r\n\r\n",
request.body
])
parsed_parts = BytesParser(policy=default_policy).parsebytes(raw_data).get_payload()
raw_data = b"".join(
[
b"Content-Type: ",
request.META["CONTENT_TYPE"].encode("ascii"),
b"\r\n\r\n",
request.body,
]
)
parsed_parts = (
BytesParser(policy=default_policy).parsebytes(raw_data).get_payload()
)
for part in parsed_parts:
name = part.get_param('name', header='content-disposition')
if name == 'text':
name = part.get_param("name", header="content-disposition")
if name == "text":
text = part.get_payload(decode=True).decode(text_charset)
elif name == 'html':
elif name == "html":
html = part.get_payload(decode=True).decode(html_charset)
# (subject, from, to, etc. are parsed from raw headers field,
# so no need to worry about their separate POST field charsets)
return AnymailInboundMessage.construct(
raw_headers=request.POST.get('headers', ""), # includes From, To, Cc, Subject, etc.
text=text, html=html, attachments=attachments)
# POST["headers"] includes From, To, Cc, Subject, etc.
raw_headers=request.POST.get("headers", ""),
text=text,
html=html,
attachments=attachments,
)