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

@@ -5,24 +5,40 @@ from base64 import b64decode
from django.http import HttpResponse
from django.utils.dateparse import parse_datetime
from .base import AnymailBaseWebhookView
from ..exceptions import (
AnymailAPIError, AnymailConfigurationError, AnymailImproperlyInstalled, AnymailWebhookValidationFailure,
_LazyError)
AnymailAPIError,
AnymailConfigurationError,
AnymailImproperlyInstalled,
AnymailWebhookValidationFailure,
_LazyError,
)
from ..inbound import AnymailInboundMessage
from ..signals import AnymailInboundEvent, AnymailTrackingEvent, EventType, RejectReason, inbound, tracking
from ..signals import (
AnymailInboundEvent,
AnymailTrackingEvent,
EventType,
RejectReason,
inbound,
tracking,
)
from ..utils import get_anymail_setting, getfirst
from .base import AnymailBaseWebhookView
try:
import boto3
from botocore.exceptions import ClientError
from ..backends.amazon_ses import _get_anymail_boto3_params
except ImportError:
# This module gets imported by anymail.urls, so don't complain about boto3 missing
# unless one of the Amazon SES webhook views is actually used and needs it
boto3 = _LazyError(AnymailImproperlyInstalled(missing_package='boto3', backend='amazon_ses'))
boto3 = _LazyError(
AnymailImproperlyInstalled(missing_package="boto3", backend="amazon_ses")
)
ClientError = object
_get_anymail_boto3_params = _LazyError(AnymailImproperlyInstalled(missing_package='boto3', backend='amazon_ses'))
_get_anymail_boto3_params = _LazyError(
AnymailImproperlyInstalled(missing_package="boto3", backend="amazon_ses")
)
class AmazonSESBaseWebhookView(AnymailBaseWebhookView):
@@ -31,23 +47,32 @@ class AmazonSESBaseWebhookView(AnymailBaseWebhookView):
esp_name = "Amazon SES"
def __init__(self, **kwargs):
# whether to automatically respond to SNS SubscriptionConfirmation requests; default True
# (Future: could also take a TopicArn or list to auto-confirm)
# whether to automatically respond to SNS SubscriptionConfirmation requests;
# default True. (Future: could also take a TopicArn or list to auto-confirm)
self.auto_confirm_enabled = get_anymail_setting(
"auto_confirm_sns_subscriptions", esp_name=self.esp_name, kwargs=kwargs, default=True)
# boto3 params for connecting to S3 (inbound downloads) and SNS (auto-confirm subscriptions):
self.session_params, self.client_params = _get_anymail_boto3_params(kwargs=kwargs)
"auto_confirm_sns_subscriptions",
esp_name=self.esp_name,
kwargs=kwargs,
default=True,
)
# boto3 params for connecting to S3 (inbound downloads)
# and SNS (auto-confirm subscriptions):
self.session_params, self.client_params = _get_anymail_boto3_params(
kwargs=kwargs
)
super().__init__(**kwargs)
@staticmethod
def _parse_sns_message(request):
# cache so we don't have to parse the json multiple times
if not hasattr(request, '_sns_message'):
if not hasattr(request, "_sns_message"):
try:
body = request.body.decode(request.encoding or 'utf-8')
body = request.body.decode(request.encoding or "utf-8")
request._sns_message = json.loads(body)
except (TypeError, ValueError, UnicodeDecodeError) as err:
raise AnymailAPIError("Malformed SNS message body %r" % request.body) from err
raise AnymailAPIError(
"Malformed SNS message body %r" % request.body
) from err
return request._sns_message
def validate_request(self, request):
@@ -57,18 +82,24 @@ class AmazonSESBaseWebhookView(AnymailBaseWebhookView):
body_type = sns_message.get("Type", "<<missing>>")
if header_type != body_type:
raise AnymailWebhookValidationFailure(
'SNS header "x-amz-sns-message-type: %s" doesn\'t match body "Type": "%s"'
% (header_type, body_type))
'SNS header "x-amz-sns-message-type: %s"'
' doesn\'t match body "Type": "%s"' % (header_type, body_type)
)
if header_type not in ["Notification", "SubscriptionConfirmation", "UnsubscribeConfirmation"]:
if header_type not in [
"Notification",
"SubscriptionConfirmation",
"UnsubscribeConfirmation",
]:
raise AnymailAPIError("Unknown SNS message type '%s'" % header_type)
header_id = request.META.get("HTTP_X_AMZ_SNS_MESSAGE_ID", "<<missing>>")
body_id = sns_message.get("MessageId", "<<missing>>")
if header_id != body_id:
raise AnymailWebhookValidationFailure(
'SNS header "x-amz-sns-message-id: %s" doesn\'t match body "MessageId": "%s"'
% (header_id, body_id))
'SNS header "x-amz-sns-message-id: %s"'
' doesn\'t match body "MessageId": "%s"' % (header_id, body_id)
)
# Future: Verify SNS message signature
# https://docs.aws.amazon.com/sns/latest/dg/SendMessageToHttp.verify.signature.html
@@ -76,7 +107,8 @@ class AmazonSESBaseWebhookView(AnymailBaseWebhookView):
def post(self, request, *args, **kwargs):
# request has *not* yet been validated at this point
if self.basic_auth and not request.META.get("HTTP_AUTHORIZATION"):
# Amazon SNS requires a proper 401 response before it will attempt to send basic auth
# Amazon SNS requires a proper 401 response
# before it will attempt to send basic auth
response = HttpResponse(status=401)
response["WWW-Authenticate"] = 'Basic realm="Anymail WEBHOOK_SECRET"'
return response
@@ -92,10 +124,16 @@ class AmazonSESBaseWebhookView(AnymailBaseWebhookView):
try:
ses_event = json.loads(message_string)
except (TypeError, ValueError) as err:
if message_string == "Successfully validated SNS topic for Amazon SES event publishing.":
pass # this Notification is generated after SubscriptionConfirmation
if (
"Successfully validated SNS topic for Amazon SES event publishing."
== message_string
):
# this Notification is generated after SubscriptionConfirmation
pass
else:
raise AnymailAPIError("Unparsable SNS Message %r" % message_string) from err
raise AnymailAPIError(
"Unparsable SNS Message %r" % message_string
) from err
else:
events = self.esp_to_anymail_events(ses_event, sns_message)
elif sns_type == "SubscriptionConfirmation":
@@ -107,43 +145,63 @@ class AmazonSESBaseWebhookView(AnymailBaseWebhookView):
raise NotImplementedError()
def auto_confirm_sns_subscription(self, sns_message):
"""Automatically accept a subscription to Amazon SNS topics, if the request is expected.
"""
Automatically accept a subscription to Amazon SNS topics,
if the request is expected.
If an SNS SubscriptionConfirmation arrives with HTTP basic auth proving it is meant for us,
automatically load the SubscribeURL to confirm the subscription.
If an SNS SubscriptionConfirmation arrives with HTTP basic auth proving it is
meant for us, automatically load the SubscribeURL to confirm the subscription.
"""
if not self.auto_confirm_enabled:
return
if not self.basic_auth:
# Note: basic_auth (shared secret) confirms the notification was meant for us.
# If WEBHOOK_SECRET isn't set, Anymail logs a warning but allows the request.
# (Also, verifying the SNS message signature would be insufficient here:
# if someone else tried to point their own SNS topic at our webhook url,
# SNS would send a SubscriptionConfirmation with a valid Amazon signature.)
# basic_auth (shared secret) confirms the notification was meant for us.
# If WEBHOOK_SECRET isn't set, Anymail logs a warning but allows the
# request. (Also, verifying the SNS message signature would be insufficient
# here: if someone else tried to point their own SNS topic at our webhook
# url, SNS would send a SubscriptionConfirmation with a valid Amazon
# signature.)
raise AnymailWebhookValidationFailure(
"Anymail received an unexpected SubscriptionConfirmation request for Amazon SNS topic "
"'{topic_arn!s}'. (Anymail can automatically confirm SNS subscriptions if you set a "
"WEBHOOK_SECRET and use that in your SNS notification url. Or you can manually confirm "
"this subscription in the SNS dashboard with token '{token!s}'.)"
"".format(topic_arn=sns_message.get('TopicArn'), token=sns_message.get('Token')))
"Anymail received an unexpected SubscriptionConfirmation request for "
"Amazon SNS topic '{topic_arn!s}'. (Anymail can automatically confirm "
"SNS subscriptions if you set a WEBHOOK_SECRET and use that in your "
"SNS notification url. Or you can manually confirm this subscription "
"in the SNS dashboard with token '{token!s}'.)".format(
topic_arn=sns_message.get("TopicArn"),
token=sns_message.get("Token"),
)
)
# WEBHOOK_SECRET *is* set, so the request's basic auth has been verified by now (in run_validators).
# We're good to confirm...
# WEBHOOK_SECRET *is* set, so the request's basic auth has been verified by now
# (in run_validators). We're good to confirm...
topic_arn = sns_message["TopicArn"]
token = sns_message["Token"]
# Must confirm in TopicArn's own region (which may be different from the default)
# Must confirm in TopicArn's own region
# (which may be different from the default)
try:
(_arn_tag, _partition, _service, region, _account, _resource) = topic_arn.split(":", maxsplit=6)
(
_arn_tag,
_partition,
_service,
region,
_account,
_resource,
) = topic_arn.split(":", maxsplit=6)
except (TypeError, ValueError):
raise ValueError("Invalid ARN format '{topic_arn!s}'".format(topic_arn=topic_arn))
raise ValueError(
"Invalid ARN format '{topic_arn!s}'".format(topic_arn=topic_arn)
)
client_params = self.client_params.copy()
client_params["region_name"] = region
sns_client = boto3.session.Session(**self.session_params).client('sns', **client_params)
sns_client = boto3.session.Session(**self.session_params).client(
"sns", **client_params
)
sns_client.confirm_subscription(
TopicArn=topic_arn, Token=token, AuthenticateOnUnsubscribe='true')
TopicArn=topic_arn, Token=token, AuthenticateOnUnsubscribe="true"
)
class AmazonSESTrackingWebhookView(AmazonSESBaseWebhookView):
@@ -153,16 +211,19 @@ class AmazonSESTrackingWebhookView(AmazonSESBaseWebhookView):
def esp_to_anymail_events(self, ses_event, sns_message):
# Amazon SES has two notification formats, which are almost exactly the same:
# - https://docs.aws.amazon.com/ses/latest/DeveloperGuide/event-publishing-retrieving-sns-contents.html
# - https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html
# https://docs.aws.amazon.com/ses/latest/DeveloperGuide/event-publishing-retrieving-sns-contents.html
# https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html
# This code should handle either.
ses_event_type = getfirst(ses_event, ["eventType", "notificationType"], "<<type missing>>")
ses_event_type = getfirst(
ses_event, ["eventType", "notificationType"], "<<type missing>>"
)
if ses_event_type == "Received":
# This is an inbound event
raise AnymailConfigurationError(
"You seem to have set an Amazon SES *inbound* receipt rule to publish "
"to an SNS Topic that posts to Anymail's *tracking* webhook URL. "
"(SNS TopicArn %s)" % sns_message.get("TopicArn"))
"(SNS TopicArn %s)" % sns_message.get("TopicArn")
)
event_id = sns_message.get("MessageId") # unique to the SNS notification
try:
@@ -171,7 +232,8 @@ class AmazonSESTrackingWebhookView(AmazonSESBaseWebhookView):
timestamp = None
mail_object = ses_event.get("mail", {})
message_id = mail_object.get("messageId") # same as MessageId in SendRawEmail response
# same as MessageId in SendRawEmail response:
message_id = mail_object.get("messageId")
all_recipients = mail_object.get("destination", [])
# Recover tags and metadata from custom headers
@@ -187,7 +249,8 @@ class AmazonSESTrackingWebhookView(AmazonSESBaseWebhookView):
except (ValueError, TypeError, KeyError):
pass
common_props = dict( # AnymailTrackingEvent props for all recipients
# AnymailTrackingEvent props for all recipients:
common_props = dict(
esp_event=ses_event,
event_id=event_id,
message_id=message_id,
@@ -195,12 +258,13 @@ class AmazonSESTrackingWebhookView(AmazonSESBaseWebhookView):
tags=tags,
timestamp=timestamp,
)
per_recipient_props = [ # generate individual events for each of these
dict(recipient=email_address)
for email_address in all_recipients
# generate individual events for each of these:
per_recipient_props = [
dict(recipient=email_address) for email_address in all_recipients
]
event_object = ses_event.get(ses_event_type.lower(), {}) # e.g., ses_event["bounce"]
# event-type-specific data (e.g., ses_event["bounce"]):
event_object = ses_event.get(ses_event_type.lower(), {})
if ses_event_type == "Bounce":
common_props.update(
@@ -208,10 +272,13 @@ class AmazonSESTrackingWebhookView(AmazonSESBaseWebhookView):
description="{bounceType}: {bounceSubType}".format(**event_object),
reject_reason=RejectReason.BOUNCED,
)
per_recipient_props = [dict(
recipient=recipient["emailAddress"],
mta_response=recipient.get("diagnosticCode"),
) for recipient in event_object["bouncedRecipients"]]
per_recipient_props = [
dict(
recipient=recipient["emailAddress"],
mta_response=recipient.get("diagnosticCode"),
)
for recipient in event_object["bouncedRecipients"]
]
elif ses_event_type == "Complaint":
common_props.update(
event_type=EventType.COMPLAINED,
@@ -219,17 +286,18 @@ class AmazonSESTrackingWebhookView(AmazonSESBaseWebhookView):
reject_reason=RejectReason.SPAM,
user_agent=event_object.get("userAgent"),
)
per_recipient_props = [dict(
recipient=recipient["emailAddress"],
) for recipient in event_object["complainedRecipients"]]
per_recipient_props = [
dict(recipient=recipient["emailAddress"])
for recipient in event_object["complainedRecipients"]
]
elif ses_event_type == "Delivery":
common_props.update(
event_type=EventType.DELIVERED,
mta_response=event_object.get("smtpResponse"),
)
per_recipient_props = [dict(
recipient=recipient,
) for recipient in event_object["recipients"]]
per_recipient_props = [
dict(recipient=recipient) for recipient in event_object["recipients"]
]
elif ses_event_type == "Send":
common_props.update(
event_type=EventType.SENT,
@@ -256,7 +324,8 @@ class AmazonSESTrackingWebhookView(AmazonSESBaseWebhookView):
click_url=event_object.get("link"),
)
elif ses_event_type == "Rendering Failure":
event_object = ses_event["failure"] # rather than ses_event["rendering failure"]
# (this type doesn't follow usual event_object naming)
event_object = ses_event["failure"]
common_props.update(
event_type=EventType.FAILED,
description=event_object["errorMessage"],
@@ -285,8 +354,9 @@ class AmazonSESInboundWebhookView(AmazonSESBaseWebhookView):
# This is not an inbound event
raise AnymailConfigurationError(
"You seem to have set an Amazon SES *sending* event or notification "
"to publish to an SNS Topic that posts to Anymail's *inbound* webhook URL. "
"(SNS TopicArn %s)" % sns_message.get("TopicArn"))
"to publish to an SNS Topic that posts to Anymail's *inbound* webhook "
"URL. (SNS TopicArn %s)" % sns_message.get("TopicArn")
)
receipt_object = ses_event.get("receipt", {})
action_object = receipt_object.get("action", {})
@@ -301,11 +371,13 @@ class AmazonSESInboundWebhookView(AmazonSESBaseWebhookView):
else:
message = AnymailInboundMessage.parse_raw_mime(content)
elif action_type == "S3":
# download message from s3 into memory, then parse
# (SNS has 15s limit for an http response; hope download doesn't take that long)
# download message from s3 into memory, then parse. (SNS has 15s limit
# for an http response; hope download doesn't take that long)
bucket_name = action_object["bucketName"]
object_key = action_object["objectKey"]
s3 = boto3.session.Session(**self.session_params).client("s3", **self.client_params)
s3 = boto3.session.Session(**self.session_params).client(
"s3", **self.client_params
)
content = io.BytesIO()
try:
s3.download_fileobj(bucket_name, object_key, content)
@@ -314,46 +386,62 @@ class AmazonSESInboundWebhookView(AmazonSESBaseWebhookView):
except ClientError as err:
# improve the botocore error message
raise AnymailBotoClientAPIError(
"Anymail AmazonSESInboundWebhookView couldn't download S3 object '{bucket_name}:{object_key}'"
"Anymail AmazonSESInboundWebhookView couldn't download"
" S3 object '{bucket_name}:{object_key}'"
"".format(bucket_name=bucket_name, object_key=object_key),
client_error=err) from err
client_error=err,
) from err
finally:
content.close()
else:
raise AnymailConfigurationError(
"Anymail's Amazon SES inbound webhook works only with 'SNS' or 'S3' receipt rule actions, "
"not SNS notifications for {action_type!s} actions. (SNS TopicArn {topic_arn!s})"
"".format(action_type=action_type, topic_arn=sns_message.get("TopicArn")))
"Anymail's Amazon SES inbound webhook works only with 'SNS' or 'S3'"
" receipt rule actions, not SNS notifications for {action_type!s}"
" actions. (SNS TopicArn {topic_arn!s})"
"".format(
action_type=action_type, topic_arn=sns_message.get("TopicArn")
)
)
message.envelope_sender = mail_object.get("source") # "the envelope MAIL FROM address"
# "the envelope MAIL FROM address":
message.envelope_sender = mail_object.get("source")
try:
# "recipients that were matched by the active receipt rule"
message.envelope_recipient = receipt_object["recipients"][0]
except (KeyError, TypeError, IndexError):
pass
spam_status = receipt_object.get("spamVerdict", {}).get("status", "").upper()
message.spam_detected = {"PASS": False, "FAIL": True}.get(spam_status) # else None if unsure
# spam_detected = False if no spam, True if spam, or None if unsure:
message.spam_detected = {"PASS": False, "FAIL": True}.get(spam_status)
event_id = mail_object.get("messageId") # "unique ID assigned to the email by Amazon SES"
# "unique ID assigned to the email by Amazon SES":
event_id = mail_object.get("messageId")
try:
timestamp = parse_datetime(mail_object["timestamp"]) # "time at which the email was received"
# "time at which the email was received":
timestamp = parse_datetime(mail_object["timestamp"])
except (KeyError, ValueError):
timestamp = None
return [AnymailInboundEvent(
event_type=EventType.INBOUND,
event_id=event_id,
message=message,
timestamp=timestamp,
esp_event=ses_event,
)]
return [
AnymailInboundEvent(
event_type=EventType.INBOUND,
event_id=event_id,
message=message,
timestamp=timestamp,
esp_event=ses_event,
)
]
class AnymailBotoClientAPIError(AnymailAPIError, ClientError):
"""An AnymailAPIError that is also a Boto ClientError"""
def __init__(self, *args, client_error):
assert isinstance(client_error, ClientError)
# init self as boto ClientError (which doesn't cooperatively subclass):
super().__init__(error_response=client_error.response, operation_name=client_error.operation_name)
super().__init__(
error_response=client_error.response,
operation_name=client_error.operation_name,
)
# emulate AnymailError init:
self.args = args