mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
Drop Python 2 and Django 1.11 support
Minimum supported versions are now Django 2.0, Python 3.5. This touches a lot of code, to: * Remove obsolete portability code and workarounds (six, backports of email parsers, test utils, etc.) * Use Python 3 syntax (class defs, raise ... from, etc.) * Correct inheritance for mixin classes * Fix outdated docs content and links * Suppress Python 3 "unclosed SSLSocket" ResourceWarnings that are beyond our control (in integration tests due to boto3, python-sparkpost)
This commit is contained in:
@@ -1,10 +1,6 @@
|
||||
from email.charset import Charset, QP
|
||||
from email.header import Header
|
||||
from email.mime.base import MIMEBase
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
from django.core.mail import BadHeaderError
|
||||
|
||||
from .base import AnymailBaseBackend, BasePayload
|
||||
from .._version import __version__
|
||||
from ..exceptions import AnymailAPIError, AnymailImproperlyInstalled
|
||||
@@ -15,42 +11,14 @@ try:
|
||||
import boto3
|
||||
from botocore.client import Config
|
||||
from botocore.exceptions import BotoCoreError, ClientError, ConnectionError
|
||||
except ImportError:
|
||||
raise AnymailImproperlyInstalled(missing_package='boto3', backend='amazon_ses')
|
||||
except ImportError as err:
|
||||
raise AnymailImproperlyInstalled(missing_package='boto3', backend='amazon_ses') from err
|
||||
|
||||
|
||||
# boto3 has several root exception classes; this is meant to cover all of them
|
||||
BOTO_BASE_ERRORS = (BotoCoreError, ClientError, ConnectionError)
|
||||
|
||||
|
||||
# Work around Python 2 bug in email.message.Message.to_string, where long headers
|
||||
# containing commas or semicolons get an extra space inserted after every ',' or ';'
|
||||
# not already followed by a space. https://bugs.python.org/issue25257
|
||||
if Header("test,Python2,header,comma,bug", maxlinelen=20).encode() == "test,Python2,header,comma,bug":
|
||||
# no workaround needed
|
||||
HeaderBugWorkaround = None
|
||||
|
||||
def add_header(message, name, val):
|
||||
message[name] = val
|
||||
|
||||
else:
|
||||
# workaround: custom Header subclass that won't consider ',' and ';' as folding candidates
|
||||
|
||||
class HeaderBugWorkaround(Header):
|
||||
def encode(self, splitchars=' ', **kwargs): # only split on spaces, rather than splitchars=';, '
|
||||
return Header.encode(self, splitchars, **kwargs)
|
||||
|
||||
def add_header(message, name, val):
|
||||
# Must bypass Django's SafeMIMEMessage.__set_item__, because its call to
|
||||
# forbid_multi_line_headers converts the val back to a str, undoing this
|
||||
# workaround. That makes this code responsible for sanitizing val:
|
||||
if '\n' in val or '\r' in val:
|
||||
raise BadHeaderError("Header values can't contain newlines (got %r for header %r)" % (val, name))
|
||||
val = HeaderBugWorkaround(val, header_name=name)
|
||||
assert isinstance(message, MIMEBase)
|
||||
MIMEBase.__setitem__(message, name, val)
|
||||
|
||||
|
||||
class EmailBackend(AnymailBaseBackend):
|
||||
"""
|
||||
Amazon SES Email Backend (using boto3)
|
||||
@@ -60,7 +28,7 @@ class EmailBackend(AnymailBaseBackend):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Init options from Django settings"""
|
||||
super(EmailBackend, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
# AMAZON_SES_CLIENT_PARAMS is optional - boto3 can find credentials several other ways
|
||||
self.session_params, self.client_params = _get_anymail_boto3_params(kwargs=kwargs)
|
||||
self.configuration_set_name = get_anymail_setting("configuration_set_name", esp_name=self.esp_name,
|
||||
@@ -77,6 +45,8 @@ class EmailBackend(AnymailBaseBackend):
|
||||
except BOTO_BASE_ERRORS:
|
||||
if not self.fail_silently:
|
||||
raise
|
||||
else:
|
||||
return True # created client
|
||||
|
||||
def close(self):
|
||||
if self.client is None:
|
||||
@@ -98,7 +68,7 @@ class EmailBackend(AnymailBaseBackend):
|
||||
except BOTO_BASE_ERRORS as err:
|
||||
# ClientError has a response attr with parsed json error response (other errors don't)
|
||||
raise AnymailAPIError(str(err), backend=self, email_message=message, payload=payload,
|
||||
response=getattr(err, 'response', None), raised_from=err)
|
||||
response=getattr(err, 'response', None)) from err
|
||||
return response
|
||||
|
||||
def parse_recipient_status(self, response, payload, message):
|
||||
@@ -125,12 +95,9 @@ class AmazonSESBasePayload(BasePayload):
|
||||
|
||||
class AmazonSESSendRawEmailPayload(AmazonSESBasePayload):
|
||||
def init_payload(self):
|
||||
super(AmazonSESSendRawEmailPayload, self).init_payload()
|
||||
super().init_payload()
|
||||
self.all_recipients = []
|
||||
self.mime_message = self.message.message()
|
||||
if HeaderBugWorkaround and "Subject" in self.mime_message:
|
||||
# (message.message() will have already checked subject for BadHeaderError)
|
||||
self.mime_message.replace_header("Subject", HeaderBugWorkaround(self.message.subject))
|
||||
|
||||
# Work around an Amazon SES bug where, if all of:
|
||||
# - the message body (text or html) contains non-ASCII characters
|
||||
@@ -165,7 +132,7 @@ class AmazonSESSendRawEmailPayload(AmazonSESBasePayload):
|
||||
except (KeyError, TypeError) as err:
|
||||
raise AnymailAPIError(
|
||||
"%s parsing Amazon SES send result %r" % (str(err), response),
|
||||
backend=self.backend, email_message=self.message, payload=self)
|
||||
backend=self.backend, email_message=self.message, payload=self) from None
|
||||
|
||||
recipient_status = AnymailRecipientStatus(message_id=message_id, status="queued")
|
||||
return {recipient.addr_spec: recipient_status for recipient in self.all_recipients}
|
||||
@@ -248,14 +215,14 @@ class AmazonSESSendRawEmailPayload(AmazonSESBasePayload):
|
||||
# (See "How do message tags work?" in https://aws.amazon.com/blogs/ses/introducing-sending-metrics/
|
||||
# and https://forums.aws.amazon.com/thread.jspa?messageID=782922.)
|
||||
# To support reliable retrieval in webhooks, just use custom headers for metadata.
|
||||
add_header(self.mime_message, "X-Metadata", self.serialize_json(metadata))
|
||||
self.mime_message["X-Metadata"] = self.serialize_json(metadata)
|
||||
|
||||
def set_tags(self, tags):
|
||||
# See note about Amazon SES Message Tags and custom headers in set_metadata above.
|
||||
# To support reliable retrieval in webhooks, use custom headers for tags.
|
||||
# (There are no restrictions on number or content for custom header tags.)
|
||||
for tag in tags:
|
||||
add_header(self.mime_message, "X-Tag", tag) # creates multiple X-Tag headers, one per tag
|
||||
self.mime_message.add_header("X-Tag", tag) # creates multiple X-Tag headers, one per tag
|
||||
|
||||
# Also *optionally* pass a single Message Tag if the AMAZON_SES_MESSAGE_TAG_NAME
|
||||
# Anymail setting is set (default no). The AWS API restricts tag content in this case.
|
||||
@@ -278,7 +245,7 @@ class AmazonSESSendRawEmailPayload(AmazonSESBasePayload):
|
||||
|
||||
class AmazonSESSendBulkTemplatedEmailPayload(AmazonSESBasePayload):
|
||||
def init_payload(self):
|
||||
super(AmazonSESSendBulkTemplatedEmailPayload, self).init_payload()
|
||||
super().init_payload()
|
||||
# late-bind recipients and merge_data in call_send_api
|
||||
self.recipients = {"to": [], "cc": [], "bcc": []}
|
||||
self.merge_data = {}
|
||||
@@ -311,7 +278,7 @@ class AmazonSESSendBulkTemplatedEmailPayload(AmazonSESBasePayload):
|
||||
except (KeyError, TypeError) as err:
|
||||
raise AnymailAPIError(
|
||||
"%s parsing Amazon SES send result %r" % (str(err), response),
|
||||
backend=self.backend, email_message=self.message, payload=self)
|
||||
backend=self.backend, email_message=self.message, payload=self) from None
|
||||
|
||||
to_addrs = [to.addr_spec for to in self.recipients["to"]]
|
||||
if len(anymail_statuses) != len(to_addrs):
|
||||
|
||||
Reference in New Issue
Block a user