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):
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import json
|
||||
from datetime import date, datetime
|
||||
|
||||
import six
|
||||
from django.conf import settings
|
||||
from django.core.mail.backends.base import BaseEmailBackend
|
||||
from django.utils.timezone import is_naive, get_current_timezone, make_aware, utc
|
||||
@@ -23,7 +22,7 @@ class AnymailBaseBackend(BaseEmailBackend):
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AnymailBaseBackend, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.ignore_unsupported_features = get_anymail_setting('ignore_unsupported_features',
|
||||
kwargs=kwargs, default=False)
|
||||
@@ -207,7 +206,7 @@ class AnymailBaseBackend(BaseEmailBackend):
|
||||
(self.__class__.__module__, self.__class__.__name__))
|
||||
|
||||
|
||||
class BasePayload(object):
|
||||
class BasePayload:
|
||||
# Listing of EmailMessage/EmailMultiAlternatives attributes
|
||||
# to process into Payload. Each item is in the form:
|
||||
# (attr, combiner, converter)
|
||||
@@ -365,7 +364,7 @@ class BasePayload(object):
|
||||
# TypeError: must be str, not list
|
||||
# TypeError: can only concatenate list (not "str") to list
|
||||
# TypeError: Can't convert 'list' object to str implicitly
|
||||
if isinstance(value, six.string_types) or is_lazy(value):
|
||||
if isinstance(value, str) or is_lazy(value):
|
||||
raise TypeError('"{attr}" attribute must be a list or other iterable'.format(attr=attr))
|
||||
|
||||
#
|
||||
@@ -538,7 +537,7 @@ class BasePayload(object):
|
||||
except TypeError as err:
|
||||
# Add some context to the "not JSON serializable" message
|
||||
raise AnymailSerializationError(orig_err=err, email_message=self.message,
|
||||
backend=self.backend, payload=self)
|
||||
backend=self.backend, payload=self) from None
|
||||
|
||||
@staticmethod
|
||||
def _json_default(o):
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
from __future__ import print_function
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import requests
|
||||
import six
|
||||
from six.moves.urllib.parse import urljoin
|
||||
|
||||
from anymail.utils import get_anymail_setting
|
||||
from .base import AnymailBaseBackend, BasePayload
|
||||
from ..exceptions import AnymailRequestsAPIError
|
||||
from .._version import __version__
|
||||
from ..exceptions import AnymailRequestsAPIError
|
||||
|
||||
|
||||
class AnymailRequestsBackend(AnymailBaseBackend):
|
||||
@@ -19,7 +17,7 @@ class AnymailRequestsBackend(AnymailBaseBackend):
|
||||
"""Init options from Django settings"""
|
||||
self.api_url = api_url
|
||||
self.timeout = get_anymail_setting('requests_timeout', kwargs=kwargs, default=30)
|
||||
super(AnymailRequestsBackend, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
self.session = None
|
||||
|
||||
def open(self):
|
||||
@@ -57,7 +55,7 @@ class AnymailRequestsBackend(AnymailBaseBackend):
|
||||
"Session has not been opened in {class_name}._send. "
|
||||
"(This is either an implementation error in {class_name}, "
|
||||
"or you are incorrectly calling _send directly.)".format(class_name=class_name))
|
||||
return super(AnymailRequestsBackend, self)._send(message)
|
||||
return super()._send(message)
|
||||
|
||||
def post_to_esp(self, payload, message):
|
||||
"""Post payload to ESP send API endpoint, and return the raw response.
|
||||
@@ -78,7 +76,7 @@ class AnymailRequestsBackend(AnymailBaseBackend):
|
||||
exc_class = type('AnymailRequestsAPIError', (AnymailRequestsAPIError, type(err)), {})
|
||||
raise exc_class(
|
||||
"Error posting to %s:" % params.get('url', '<missing url>'),
|
||||
raised_from=err, email_message=message, payload=payload)
|
||||
email_message=message, payload=payload) from err
|
||||
self.raise_for_status(response, payload, message)
|
||||
return response
|
||||
|
||||
@@ -100,10 +98,10 @@ class AnymailRequestsBackend(AnymailBaseBackend):
|
||||
"""
|
||||
try:
|
||||
return response.json()
|
||||
except ValueError:
|
||||
except ValueError as err:
|
||||
raise AnymailRequestsAPIError("Invalid JSON in %s API response" % self.esp_name,
|
||||
email_message=message, payload=payload, response=response,
|
||||
backend=self)
|
||||
backend=self) from err
|
||||
|
||||
@staticmethod
|
||||
def _dump_api_request(response, **kwargs):
|
||||
@@ -113,22 +111,22 @@ class AnymailRequestsBackend(AnymailBaseBackend):
|
||||
# If you need the raw bytes, configure HTTPConnection logging as shown
|
||||
# in http://docs.python-requests.org/en/v3.0.0/api/#api-changes)
|
||||
request = response.request # a PreparedRequest
|
||||
print(u"\n===== Anymail API request")
|
||||
print(u"{method} {url}\n{headers}".format(
|
||||
print("\n===== Anymail API request")
|
||||
print("{method} {url}\n{headers}".format(
|
||||
method=request.method, url=request.url,
|
||||
headers=u"".join(u"{header}: {value}\n".format(header=header, value=value)
|
||||
for (header, value) in request.headers.items()),
|
||||
headers="".join("{header}: {value}\n".format(header=header, value=value)
|
||||
for (header, value) in request.headers.items()),
|
||||
))
|
||||
if request.body is not None:
|
||||
body_text = (request.body if isinstance(request.body, six.text_type)
|
||||
body_text = (request.body if isinstance(request.body, str)
|
||||
else request.body.decode("utf-8", errors="replace")
|
||||
).replace("\r\n", "\n")
|
||||
print(body_text)
|
||||
print(u"\n----- Response")
|
||||
print(u"HTTP {status} {reason}\n{headers}\n{body}".format(
|
||||
print("\n----- Response")
|
||||
print("HTTP {status} {reason}\n{headers}\n{body}".format(
|
||||
status=response.status_code, reason=response.reason,
|
||||
headers=u"".join(u"{header}: {value}\n".format(header=header, value=value)
|
||||
for (header, value) in response.headers.items()),
|
||||
headers="".join("{header}: {value}\n".format(header=header, value=value)
|
||||
for (header, value) in response.headers.items()),
|
||||
body=response.text, # Let Requests decode body content for us
|
||||
))
|
||||
|
||||
@@ -145,7 +143,7 @@ class RequestsPayload(BasePayload):
|
||||
self.headers = headers
|
||||
self.files = files
|
||||
self.auth = auth
|
||||
super(RequestsPayload, self).__init__(message, defaults, backend)
|
||||
super().__init__(message, defaults, backend)
|
||||
|
||||
def get_request_params(self, api_url):
|
||||
"""Returns a dict of requests.request params that will send payload to the ESP.
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
from datetime import datetime
|
||||
from email.utils import encode_rfc2231
|
||||
from six.moves.urllib.parse import quote
|
||||
from urllib.parse import quote
|
||||
|
||||
from requests import Request
|
||||
|
||||
from ..exceptions import AnymailRequestsAPIError, AnymailError
|
||||
from .base_requests import AnymailRequestsBackend, RequestsPayload
|
||||
from ..exceptions import AnymailError, AnymailRequestsAPIError
|
||||
from ..message import AnymailRecipientStatus
|
||||
from ..utils import get_anymail_setting, rfc2822date
|
||||
|
||||
from .base_requests import AnymailRequestsBackend, RequestsPayload
|
||||
|
||||
|
||||
# Feature-detect whether requests (urllib3) correctly uses RFC 7578 encoding for non-
|
||||
# ASCII filenames in Content-Disposition headers. (This was fixed in urllib3 v1.25.)
|
||||
@@ -17,7 +16,7 @@ from .base_requests import AnymailRequestsBackend, RequestsPayload
|
||||
# (Note: when this workaround is removed, please also remove the "old_urllib3" tox envs.)
|
||||
def is_requests_rfc_5758_compliant():
|
||||
request = Request(method='POST', url='https://www.example.com',
|
||||
files=[('attachment', (u'\N{NOT SIGN}.txt', 'test', 'text/plain'))])
|
||||
files=[('attachment', ('\N{NOT SIGN}.txt', 'test', 'text/plain'))])
|
||||
prepared = request.prepare()
|
||||
form_data = prepared.body # bytes
|
||||
return b'filename*=' not in form_data
|
||||
@@ -43,7 +42,7 @@ class EmailBackend(AnymailRequestsBackend):
|
||||
default="https://api.mailgun.net/v3")
|
||||
if not api_url.endswith("/"):
|
||||
api_url += "/"
|
||||
super(EmailBackend, self).__init__(api_url, **kwargs)
|
||||
super().__init__(api_url, **kwargs)
|
||||
|
||||
def build_message_payload(self, message, defaults):
|
||||
return MailgunPayload(message, defaults, self)
|
||||
@@ -62,10 +61,10 @@ class EmailBackend(AnymailRequestsBackend):
|
||||
try:
|
||||
message_id = parsed_response["id"]
|
||||
mailgun_message = parsed_response["message"]
|
||||
except (KeyError, TypeError):
|
||||
except (KeyError, TypeError) as err:
|
||||
raise AnymailRequestsAPIError("Invalid Mailgun API response format",
|
||||
email_message=message, payload=payload, response=response,
|
||||
backend=self)
|
||||
backend=self) from err
|
||||
if not mailgun_message.startswith("Queued"):
|
||||
raise AnymailRequestsAPIError("Unrecognized Mailgun API message '%s'" % mailgun_message,
|
||||
email_message=message, payload=payload, response=response,
|
||||
@@ -89,7 +88,7 @@ class MailgunPayload(RequestsPayload):
|
||||
self.merge_metadata = {}
|
||||
self.to_emails = []
|
||||
|
||||
super(MailgunPayload, self).__init__(message, defaults, backend, auth=auth, *args, **kwargs)
|
||||
super().__init__(message, defaults, backend, auth=auth, *args, **kwargs)
|
||||
|
||||
def get_api_endpoint(self):
|
||||
if self.sender_domain is None:
|
||||
@@ -105,7 +104,7 @@ class MailgunPayload(RequestsPayload):
|
||||
return "%s/messages" % quote(self.sender_domain, safe='')
|
||||
|
||||
def get_request_params(self, api_url):
|
||||
params = super(MailgunPayload, self).get_request_params(api_url)
|
||||
params = super().get_request_params(api_url)
|
||||
non_ascii_filenames = [filename
|
||||
for (field, (filename, content, mimetype)) in params["files"]
|
||||
if filename is not None and not isascii(filename)]
|
||||
@@ -122,9 +121,7 @@ class MailgunPayload(RequestsPayload):
|
||||
prepared = Request(**params).prepare()
|
||||
form_data = prepared.body # bytes
|
||||
for filename in non_ascii_filenames: # text
|
||||
rfc2231_filename = encode_rfc2231( # wants a str (text in PY3, bytes in PY2)
|
||||
filename if isinstance(filename, str) else filename.encode("utf-8"),
|
||||
charset="utf-8")
|
||||
rfc2231_filename = encode_rfc2231(filename, charset="utf-8")
|
||||
form_data = form_data.replace(
|
||||
b'filename*=' + rfc2231_filename.encode("utf-8"),
|
||||
b'filename="' + filename.encode("utf-8") + b'"')
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
from email.header import Header
|
||||
|
||||
from six.moves.urllib.parse import quote
|
||||
|
||||
from ..exceptions import AnymailRequestsAPIError
|
||||
from ..message import AnymailRecipientStatus, ANYMAIL_STATUSES
|
||||
from ..utils import get_anymail_setting, EmailAddress, parse_address_list
|
||||
from urllib.parse import quote
|
||||
|
||||
from .base_requests import AnymailRequestsBackend, RequestsPayload
|
||||
from ..exceptions import AnymailRequestsAPIError
|
||||
from ..message import ANYMAIL_STATUSES, AnymailRecipientStatus
|
||||
from ..utils import EmailAddress, get_anymail_setting, parse_address_list
|
||||
|
||||
|
||||
class EmailBackend(AnymailRequestsBackend):
|
||||
@@ -25,7 +23,7 @@ class EmailBackend(AnymailRequestsBackend):
|
||||
default="https://api.mailjet.com/v3")
|
||||
if not api_url.endswith("/"):
|
||||
api_url += "/"
|
||||
super(EmailBackend, self).__init__(api_url, **kwargs)
|
||||
super().__init__(api_url, **kwargs)
|
||||
|
||||
def build_message_payload(self, message, defaults):
|
||||
return MailjetPayload(message, defaults, self)
|
||||
@@ -36,7 +34,7 @@ class EmailBackend(AnymailRequestsBackend):
|
||||
raise AnymailRequestsAPIError(
|
||||
"Invalid Mailjet API key or secret",
|
||||
email_message=message, payload=payload, response=response, backend=self)
|
||||
super(EmailBackend, self).raise_for_status(response, payload, message)
|
||||
super().raise_for_status(response, payload, message)
|
||||
|
||||
def parse_recipient_status(self, response, payload, message):
|
||||
# Mailjet's (v3.0) transactional send API is not covered in their reference docs.
|
||||
@@ -61,10 +59,10 @@ class EmailBackend(AnymailRequestsBackend):
|
||||
message_id = str(item['MessageID'])
|
||||
email = item['Email']
|
||||
recipient_status[email] = AnymailRecipientStatus(message_id=message_id, status=status)
|
||||
except (KeyError, TypeError):
|
||||
except (KeyError, TypeError) as err:
|
||||
raise AnymailRequestsAPIError("Invalid Mailjet API response format",
|
||||
email_message=message, payload=payload, response=response,
|
||||
backend=self)
|
||||
backend=self) from err
|
||||
# Make sure we ended up with a status for every original recipient
|
||||
# (Mailjet only communicates "Sent")
|
||||
for recipients in payload.recipients.values():
|
||||
@@ -88,8 +86,7 @@ class MailjetPayload(RequestsPayload):
|
||||
self.metadata = None
|
||||
self.merge_data = {}
|
||||
self.merge_metadata = {}
|
||||
super(MailjetPayload, self).__init__(message, defaults, backend,
|
||||
auth=auth, headers=http_headers, *args, **kwargs)
|
||||
super().__init__(message, defaults, backend, auth=auth, headers=http_headers, *args, **kwargs)
|
||||
|
||||
def get_api_endpoint(self):
|
||||
return "send"
|
||||
@@ -153,9 +150,10 @@ class MailjetPayload(RequestsPayload):
|
||||
parsed.addr_spec)
|
||||
else:
|
||||
parsed = EmailAddress(headers["SenderName"], headers["SenderEmail"])
|
||||
except KeyError:
|
||||
except KeyError as err:
|
||||
raise AnymailRequestsAPIError("Invalid Mailjet template API response",
|
||||
email_message=self.message, response=response, backend=self.backend)
|
||||
email_message=self.message, response=response,
|
||||
backend=self.backend) from err
|
||||
self.set_from_email(parsed)
|
||||
|
||||
def _format_email_for_mailjet(self, email):
|
||||
|
||||
@@ -23,7 +23,7 @@ class EmailBackend(AnymailRequestsBackend):
|
||||
default="https://mandrillapp.com/api/1.0")
|
||||
if not api_url.endswith("/"):
|
||||
api_url += "/"
|
||||
super(EmailBackend, self).__init__(api_url, **kwargs)
|
||||
super().__init__(api_url, **kwargs)
|
||||
|
||||
def build_message_payload(self, message, defaults):
|
||||
return MandrillPayload(message, defaults, self)
|
||||
@@ -40,10 +40,10 @@ class EmailBackend(AnymailRequestsBackend):
|
||||
status = 'unknown'
|
||||
message_id = item.get('_id', None) # can be missing for invalid/rejected recipients
|
||||
recipient_status[email] = AnymailRecipientStatus(message_id=message_id, status=status)
|
||||
except (KeyError, TypeError):
|
||||
except (KeyError, TypeError) as err:
|
||||
raise AnymailRequestsAPIError("Invalid Mandrill API response format",
|
||||
email_message=message, payload=payload, response=response,
|
||||
backend=self)
|
||||
backend=self) from err
|
||||
return recipient_status
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ class MandrillPayload(RequestsPayload):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.esp_extra = {} # late-bound in serialize_data
|
||||
super(MandrillPayload, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_api_endpoint(self):
|
||||
if 'template_name' in self.data:
|
||||
|
||||
@@ -22,7 +22,7 @@ class EmailBackend(AnymailRequestsBackend):
|
||||
default="https://api.postmarkapp.com/")
|
||||
if not api_url.endswith("/"):
|
||||
api_url += "/"
|
||||
super(EmailBackend, self).__init__(api_url, **kwargs)
|
||||
super().__init__(api_url, **kwargs)
|
||||
|
||||
def build_message_payload(self, message, defaults):
|
||||
return PostmarkPayload(message, defaults, self)
|
||||
@@ -30,7 +30,7 @@ class EmailBackend(AnymailRequestsBackend):
|
||||
def raise_for_status(self, response, payload, message):
|
||||
# We need to handle 422 responses in parse_recipient_status
|
||||
if response.status_code != 422:
|
||||
super(EmailBackend, self).raise_for_status(response, payload, message)
|
||||
super().raise_for_status(response, payload, message)
|
||||
|
||||
def parse_recipient_status(self, response, payload, message):
|
||||
# Default to "unknown" status for each recipient, unless/until we find otherwise.
|
||||
@@ -51,19 +51,19 @@ class EmailBackend(AnymailRequestsBackend):
|
||||
# these fields should always be present
|
||||
error_code = one_response["ErrorCode"]
|
||||
msg = one_response["Message"]
|
||||
except (KeyError, TypeError):
|
||||
except (KeyError, TypeError) as err:
|
||||
raise AnymailRequestsAPIError("Invalid Postmark API response format",
|
||||
email_message=message, payload=payload, response=response,
|
||||
backend=self)
|
||||
backend=self) from err
|
||||
|
||||
if error_code == 0:
|
||||
# At least partial success, and (some) email was sent.
|
||||
try:
|
||||
message_id = one_response["MessageID"]
|
||||
except KeyError:
|
||||
except KeyError as err:
|
||||
raise AnymailRequestsAPIError("Invalid Postmark API success response format",
|
||||
email_message=message, payload=payload,
|
||||
response=response, backend=self)
|
||||
response=response, backend=self) from err
|
||||
|
||||
# Assume all To recipients are "sent" unless proven otherwise below.
|
||||
# (Must use "To" from API response to get correct individual MessageIDs in batch send.)
|
||||
@@ -157,7 +157,7 @@ class PostmarkPayload(RequestsPayload):
|
||||
self.cc_and_bcc_emails = [] # need to track (separately) for parse_recipient_status
|
||||
self.merge_data = None
|
||||
self.merge_metadata = None
|
||||
super(PostmarkPayload, self).__init__(message, defaults, backend, headers=headers, *args, **kwargs)
|
||||
super().__init__(message, defaults, backend, headers=headers, *args, **kwargs)
|
||||
|
||||
def get_api_endpoint(self):
|
||||
batch_send = self.is_batch() and len(self.to_emails) > 1
|
||||
@@ -174,7 +174,7 @@ class PostmarkPayload(RequestsPayload):
|
||||
return "email"
|
||||
|
||||
def get_request_params(self, api_url):
|
||||
params = super(PostmarkPayload, self).get_request_params(api_url)
|
||||
params = super().get_request_params(api_url)
|
||||
params['headers']['X-Postmark-Server-Token'] = self.server_token
|
||||
return params
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from requests.structures import CaseInsensitiveDict
|
||||
from .base_requests import AnymailRequestsBackend, RequestsPayload
|
||||
from ..exceptions import AnymailConfigurationError, AnymailRequestsAPIError, AnymailWarning
|
||||
from ..message import AnymailRecipientStatus
|
||||
from ..utils import BASIC_NUMERIC_TYPES, Mapping, get_anymail_setting, timestamp, update_deep
|
||||
from ..utils import BASIC_NUMERIC_TYPES, Mapping, get_anymail_setting, update_deep
|
||||
|
||||
|
||||
class EmailBackend(AnymailRequestsBackend):
|
||||
@@ -47,7 +47,7 @@ class EmailBackend(AnymailRequestsBackend):
|
||||
default="https://api.sendgrid.com/v3/")
|
||||
if not api_url.endswith("/"):
|
||||
api_url += "/"
|
||||
super(EmailBackend, self).__init__(api_url, **kwargs)
|
||||
super().__init__(api_url, **kwargs)
|
||||
|
||||
def build_message_payload(self, message, defaults):
|
||||
return SendGridPayload(message, defaults, self)
|
||||
@@ -84,9 +84,7 @@ class SendGridPayload(RequestsPayload):
|
||||
http_headers['Authorization'] = 'Bearer %s' % backend.api_key
|
||||
http_headers['Content-Type'] = 'application/json'
|
||||
http_headers['Accept'] = 'application/json'
|
||||
super(SendGridPayload, self).__init__(message, defaults, backend,
|
||||
headers=http_headers,
|
||||
*args, **kwargs)
|
||||
super().__init__(message, defaults, backend, headers=http_headers, *args, **kwargs)
|
||||
|
||||
def get_api_endpoint(self):
|
||||
return "mail/send"
|
||||
@@ -294,7 +292,7 @@ class SendGridPayload(RequestsPayload):
|
||||
def set_send_at(self, send_at):
|
||||
# Backend has converted pretty much everything to
|
||||
# a datetime by here; SendGrid expects unix timestamp
|
||||
self.data["send_at"] = int(timestamp(send_at)) # strip microseconds
|
||||
self.data["send_at"] = int(send_at.timestamp()) # strip microseconds
|
||||
|
||||
def set_tags(self, tags):
|
||||
self.data["categories"] = tags
|
||||
|
||||
@@ -30,7 +30,7 @@ class EmailBackend(AnymailRequestsBackend):
|
||||
)
|
||||
if not api_url.endswith("/"):
|
||||
api_url += "/"
|
||||
super(EmailBackend, self).__init__(api_url, **kwargs)
|
||||
super().__init__(api_url, **kwargs)
|
||||
|
||||
def build_message_payload(self, message, defaults):
|
||||
return SendinBluePayload(message, defaults, self)
|
||||
@@ -53,10 +53,10 @@ class EmailBackend(AnymailRequestsBackend):
|
||||
parsed_response = self.deserialize_json_response(response, payload, message)
|
||||
try:
|
||||
message_id = parsed_response['messageId']
|
||||
except (KeyError, TypeError):
|
||||
except (KeyError, TypeError) as err:
|
||||
raise AnymailRequestsAPIError("Invalid SendinBlue API response format",
|
||||
email_message=message, payload=payload, response=response,
|
||||
backend=self)
|
||||
backend=self) from err
|
||||
|
||||
status = AnymailRecipientStatus(message_id=message_id, status="queued")
|
||||
return {recipient.addr_spec: status for recipient in payload.all_recipients}
|
||||
@@ -71,7 +71,7 @@ class SendinBluePayload(RequestsPayload):
|
||||
http_headers['api-key'] = backend.api_key
|
||||
http_headers['Content-Type'] = 'application/json'
|
||||
|
||||
super(SendinBluePayload, self).__init__(message, defaults, backend, headers=http_headers, *args, **kwargs)
|
||||
super().__init__(message, defaults, backend, headers=http_headers, *args, **kwargs)
|
||||
|
||||
def get_api_endpoint(self):
|
||||
return "smtp/email"
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import absolute_import # we want the sparkpost package, not our own module
|
||||
|
||||
from .base import AnymailBaseBackend, BasePayload
|
||||
from ..exceptions import AnymailAPIError, AnymailImproperlyInstalled, AnymailConfigurationError
|
||||
from ..message import AnymailRecipientStatus
|
||||
@@ -7,8 +5,8 @@ from ..utils import get_anymail_setting
|
||||
|
||||
try:
|
||||
from sparkpost import SparkPost, SparkPostException
|
||||
except ImportError:
|
||||
raise AnymailImproperlyInstalled(missing_package='sparkpost', backend='sparkpost')
|
||||
except ImportError as err:
|
||||
raise AnymailImproperlyInstalled(missing_package='sparkpost', backend='sparkpost') from err
|
||||
|
||||
|
||||
class EmailBackend(AnymailBaseBackend):
|
||||
@@ -20,7 +18,7 @@ class EmailBackend(AnymailBaseBackend):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Init options from Django settings"""
|
||||
super(EmailBackend, self).__init__(**kwargs)
|
||||
super().__init__(**kwargs)
|
||||
# SPARKPOST_API_KEY is optional - library reads from env by default
|
||||
self.api_key = get_anymail_setting('api_key', esp_name=self.esp_name,
|
||||
kwargs=kwargs, allow_bare=True, default=None)
|
||||
@@ -43,7 +41,7 @@ class EmailBackend(AnymailBaseBackend):
|
||||
"You may need to set ANYMAIL = {'SPARKPOST_API_KEY': ...} "
|
||||
"or ANYMAIL_SPARKPOST_API_KEY in your Django settings, "
|
||||
"or SPARKPOST_API_KEY in your environment." % str(err)
|
||||
)
|
||||
) from err
|
||||
|
||||
# Note: SparkPost python API doesn't expose requests session sharing
|
||||
# (so there's no need to implement open/close connection management here)
|
||||
@@ -60,7 +58,7 @@ class EmailBackend(AnymailBaseBackend):
|
||||
str(err), backend=self, email_message=message, payload=payload,
|
||||
response=getattr(err, 'response', None), # SparkPostAPIException requests.Response
|
||||
status_code=getattr(err, 'status', None), # SparkPostAPIException HTTP status_code
|
||||
)
|
||||
) from err
|
||||
return response
|
||||
|
||||
def parse_recipient_status(self, response, payload, message):
|
||||
@@ -72,7 +70,7 @@ class EmailBackend(AnymailBaseBackend):
|
||||
raise AnymailAPIError(
|
||||
"%s in SparkPost.transmissions.send result %r" % (str(err), response),
|
||||
backend=self, email_message=message, payload=payload,
|
||||
)
|
||||
) from err
|
||||
|
||||
# SparkPost doesn't (yet*) tell us *which* recipients were accepted or rejected.
|
||||
# (* looks like undocumented 'rcpt_to_errors' might provide this info.)
|
||||
|
||||
@@ -25,7 +25,7 @@ class EmailBackend(AnymailBaseBackend):
|
||||
# Allow replacing the payload, for testing.
|
||||
# (Real backends would generally not implement this option.)
|
||||
self._payload_class = kwargs.pop('payload_class', TestPayload)
|
||||
super(EmailBackend, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
if not hasattr(mail, 'outbox'):
|
||||
mail.outbox = [] # see django.core.mail.backends.locmem
|
||||
|
||||
@@ -60,8 +60,8 @@ class EmailBackend(AnymailBaseBackend):
|
||||
def parse_recipient_status(self, response, payload, message):
|
||||
try:
|
||||
return response['recipient_status']
|
||||
except KeyError:
|
||||
raise AnymailAPIError('Unparsable test response')
|
||||
except KeyError as err:
|
||||
raise AnymailAPIError('Unparsable test response') from err
|
||||
|
||||
|
||||
class TestPayload(BasePayload):
|
||||
|
||||
Reference in New Issue
Block a user