mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 11:51:05 -05:00
Start factoring out common backend functionality
This commit is contained in:
248
anymail/backends/base.py
Normal file
248
anymail/backends/base.py
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
import json
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from django.core.mail.backends.base import BaseEmailBackend
|
||||||
|
|
||||||
|
from .._version import __version__
|
||||||
|
from ..exceptions import AnymailError, AnymailRequestsAPIError, AnymailSerializationError, AnymailImproperlyConfigured
|
||||||
|
|
||||||
|
|
||||||
|
class AnymailBaseBackend(BaseEmailBackend):
|
||||||
|
"""
|
||||||
|
Base Anymail email backend
|
||||||
|
"""
|
||||||
|
|
||||||
|
def open(self):
|
||||||
|
"""
|
||||||
|
Open and persist a connection to the ESP's API, and whether
|
||||||
|
a new connection was created.
|
||||||
|
|
||||||
|
Callers must ensure they later call close, if (and only if) open
|
||||||
|
returns True.
|
||||||
|
"""
|
||||||
|
# Subclasses should use an instance property to maintain a cached
|
||||||
|
# connection, and return True iff they initialize that instance
|
||||||
|
# property in _this_ open call. (If the cached connection already
|
||||||
|
# exists, just do nothing and return False.)
|
||||||
|
#
|
||||||
|
# Subclasses should swallow operational errors if self.fail_silently
|
||||||
|
# (e.g., network errors), but otherwise can raise any errors.
|
||||||
|
#
|
||||||
|
# (Returning a bool to indicate whether connection was created is
|
||||||
|
# borrowed from django.core.email.backends.SMTPBackend)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""
|
||||||
|
Close the cached connection created by open.
|
||||||
|
|
||||||
|
You must only call close if your code called open and it returned True.
|
||||||
|
"""
|
||||||
|
# Subclasses should tear down the cached connection and clear
|
||||||
|
# the instance property.
|
||||||
|
#
|
||||||
|
# Subclasses should swallow operational errors if self.fail_silently
|
||||||
|
# (e.g., network errors), but otherwise can raise any errors.
|
||||||
|
pass
|
||||||
|
|
||||||
|
def send_messages(self, email_messages):
|
||||||
|
"""
|
||||||
|
Sends one or more EmailMessage objects and returns the number of email
|
||||||
|
messages sent.
|
||||||
|
"""
|
||||||
|
# This API is specified by Django's core BaseEmailBackend
|
||||||
|
# (so you can't change it to, e.g., return detailed status).
|
||||||
|
# Subclasses shouldn't need to override.
|
||||||
|
|
||||||
|
num_sent = 0
|
||||||
|
if not email_messages:
|
||||||
|
return num_sent
|
||||||
|
|
||||||
|
created_session = self.open()
|
||||||
|
|
||||||
|
try:
|
||||||
|
for message in email_messages:
|
||||||
|
try:
|
||||||
|
sent = self._send(message)
|
||||||
|
except AnymailError:
|
||||||
|
if self.fail_silently:
|
||||||
|
sent = False
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
if sent:
|
||||||
|
num_sent += 1
|
||||||
|
finally:
|
||||||
|
if created_session:
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
return num_sent
|
||||||
|
|
||||||
|
def _send(self, message):
|
||||||
|
"""Sends the EmailMessage message, and returns True if the message was sent.
|
||||||
|
|
||||||
|
This should only be called by the base send_messages loop.
|
||||||
|
|
||||||
|
Implementations must raise exceptions derived from AnymailError for
|
||||||
|
anticipated failures that should be suppressed in fail_silently mode.
|
||||||
|
"""
|
||||||
|
message.anymail_status = None
|
||||||
|
esp_response_attr = "%s_response" % self.esp_name.lower() # e.g., message.mandrill_response
|
||||||
|
setattr(message, esp_response_attr, None) # until we have a response
|
||||||
|
if not message.recipients():
|
||||||
|
return False
|
||||||
|
|
||||||
|
payload = self.build_message_payload(message)
|
||||||
|
# FUTURE: if pre-send-signal OK...
|
||||||
|
response = self.post_to_esp(payload, message)
|
||||||
|
|
||||||
|
parsed_response = self.deserialize_response(response, payload, message)
|
||||||
|
setattr(message, esp_response_attr, parsed_response)
|
||||||
|
message.anymail_status = self.validate_response(parsed_response, response, payload, message)
|
||||||
|
# FUTURE: post-send signal
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def build_message_payload(self, message):
|
||||||
|
"""Return a payload with all message-specific options for ESP send call.
|
||||||
|
|
||||||
|
message is an EmailMessage, possibly with additional Anymail-specific attrs
|
||||||
|
|
||||||
|
Can raise AnymailUnsupportedFeature for unsupported options in message.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("%s.%s must implement build_message_payload" %
|
||||||
|
(self.__class__.__module__, self.__class__.__name__))
|
||||||
|
|
||||||
|
def post_to_esp(self, payload, message):
|
||||||
|
"""Post payload to ESP send API endpoint, and return the raw response.
|
||||||
|
|
||||||
|
payload is the result of build_message_payload
|
||||||
|
message is the original EmailMessage
|
||||||
|
return should be a raw response
|
||||||
|
|
||||||
|
Can raise AnymailAPIError (or derived exception) for problems posting to the ESP
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("%s.%s must implement build_message_payload" %
|
||||||
|
(self.__class__.__module__, self.__class__.__name__))
|
||||||
|
|
||||||
|
def deserialize_response(self, response, payload, message):
|
||||||
|
"""Deserialize a raw ESP response
|
||||||
|
|
||||||
|
Can raise AnymailAPIError (or derived exception) if response is unparsable
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("%s.%s must implement build_message_payload" %
|
||||||
|
(self.__class__.__module__, self.__class__.__name__))
|
||||||
|
|
||||||
|
def validate_response(self, parsed_response, response, payload, message):
|
||||||
|
"""Validate parsed_response, raising exceptions for any problems, and return normalized status.
|
||||||
|
|
||||||
|
Extend this to provide your own validation checks.
|
||||||
|
Validation exceptions should inherit from anymail.exceptions.AnymailError
|
||||||
|
for proper fail_silently behavior.
|
||||||
|
|
||||||
|
If *all* recipients are refused or invalid, should raise AnymailRecipientsRefused
|
||||||
|
|
||||||
|
Returns one of "sent", "queued", "refused", "error" or "multi"
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("%s.%s must implement validate_response" %
|
||||||
|
(self.__class__.__module__, self.__class__.__name__))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def esp_name(self):
|
||||||
|
"""
|
||||||
|
Read-only name of the ESP for this backend.
|
||||||
|
|
||||||
|
(E.g., MailgunBackend will return "Mailgun")
|
||||||
|
"""
|
||||||
|
return self.__class__.__name__.replace("Backend", "")
|
||||||
|
|
||||||
|
|
||||||
|
class AnymailRequestsBackend(AnymailBaseBackend):
|
||||||
|
"""
|
||||||
|
Base Anymail email backend for ESPs that use an HTTP API via requests
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, api_url, **kwargs):
|
||||||
|
"""Init options from Django settings"""
|
||||||
|
self.api_url = api_url
|
||||||
|
super(AnymailRequestsBackend, self).__init__(**kwargs)
|
||||||
|
self.session = None
|
||||||
|
|
||||||
|
def open(self):
|
||||||
|
if self.session:
|
||||||
|
return False # already exists
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.session = requests.Session()
|
||||||
|
except requests.RequestException:
|
||||||
|
if not self.fail_silently:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
self.session.headers["User-Agent"] = "Anymail/%s %s" % (
|
||||||
|
__version__, self.session.headers.get("User-Agent", ""))
|
||||||
|
return True
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self.session is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.session.close()
|
||||||
|
except requests.RequestException:
|
||||||
|
if not self.fail_silently:
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
self.session = None
|
||||||
|
|
||||||
|
def _send(self, message):
|
||||||
|
if self.session is None:
|
||||||
|
class_name = self.__class__.__name__
|
||||||
|
raise RuntimeError(
|
||||||
|
"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)
|
||||||
|
|
||||||
|
def get_api_url(self, payload, message):
|
||||||
|
"""Return the correct ESP url for sending payload
|
||||||
|
|
||||||
|
Override this to substitute your own logic for determining API endpoint.
|
||||||
|
"""
|
||||||
|
return self.api_url
|
||||||
|
|
||||||
|
def serialize_payload(self, payload, message):
|
||||||
|
"""Return payload serialized to post data.
|
||||||
|
|
||||||
|
Should raise AnymailSerializationError if payload is not serializable
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return json.dumps(payload)
|
||||||
|
except TypeError as err:
|
||||||
|
# Add some context to the "not JSON serializable" message
|
||||||
|
raise AnymailSerializationError(orig_err=err, email_message=message, payload=payload)
|
||||||
|
|
||||||
|
def post_to_esp(self, payload, message):
|
||||||
|
"""Post payload to ESP send API endpoint, and return the raw response.
|
||||||
|
|
||||||
|
payload is the result of build_message_payload
|
||||||
|
message is the original EmailMessage
|
||||||
|
return should be a requests.Response
|
||||||
|
|
||||||
|
Can raise AnymailRequestsAPIError for HTTP errors in the post
|
||||||
|
"""
|
||||||
|
api_url = self.get_api_url(payload, message)
|
||||||
|
post_data = self.serialize_payload(payload, message)
|
||||||
|
|
||||||
|
response = self.session.post(api_url, data=post_data)
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise AnymailRequestsAPIError(email_message=message, payload=payload, response=response)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def deserialize_response(self, response, payload, message):
|
||||||
|
"""Return parsed ESP API response
|
||||||
|
|
||||||
|
Can raise AnymailRequestsAPIError if response is unparsable
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return response.json()
|
||||||
|
except ValueError:
|
||||||
|
raise AnymailRequestsAPIError("Invalid JSON in %s API response" % self.esp_name,
|
||||||
|
email_message=message, payload=payload, response=response)
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
import json
|
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import requests
|
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from email.mime.base import MIMEBase
|
from email.mime.base import MIMEBase
|
||||||
@@ -11,32 +9,32 @@ except ImportError:
|
|||||||
from urllib.parse import urljoin # python 3
|
from urllib.parse import urljoin # python 3
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
|
||||||
from django.core.mail.backends.base import BaseEmailBackend
|
|
||||||
from django.core.mail.message import sanitize_address, DEFAULT_ATTACHMENT_MIME_TYPE
|
from django.core.mail.message import sanitize_address, DEFAULT_ATTACHMENT_MIME_TYPE
|
||||||
|
|
||||||
from .._version import __version__
|
from ..exceptions import (AnymailImproperlyConfigured,
|
||||||
from ..exceptions import (AnymailError, AnymailRequestsAPIError, AnymailRecipientsRefused,
|
AnymailRequestsAPIError, AnymailRecipientsRefused,
|
||||||
AnymailSerializationError, AnymailUnsupportedFeature)
|
AnymailUnsupportedFeature)
|
||||||
|
|
||||||
|
from .base import AnymailRequestsBackend
|
||||||
|
|
||||||
|
|
||||||
class MandrillBackend(BaseEmailBackend):
|
class MandrillBackend(AnymailRequestsBackend):
|
||||||
"""
|
"""
|
||||||
Mandrill API Email Backend
|
Mandrill API Email Backend
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
"""Init options from Django settings"""
|
"""Init options from Django settings"""
|
||||||
super(MandrillBackend, self).__init__(**kwargs)
|
api_url = getattr(settings, "MANDRILL_API_URL", "https://mandrillapp.com/api/1.0")
|
||||||
|
if not api_url.endswith("/"):
|
||||||
|
api_url += "/"
|
||||||
|
|
||||||
|
super(MandrillBackend, self).__init__(api_url, **kwargs)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.api_key = settings.MANDRILL_API_KEY
|
self.api_key = settings.MANDRILL_API_KEY
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise ImproperlyConfigured("Set MANDRILL_API_KEY in settings.py to use Djrill")
|
raise AnymailImproperlyConfigured("Set MANDRILL_API_KEY in settings.py to use Anymail Mandrill backend")
|
||||||
|
|
||||||
self.api_url = getattr(settings, "MANDRILL_API_URL", "https://mandrillapp.com/api/1.0")
|
|
||||||
if not self.api_url.endswith("/"):
|
|
||||||
self.api_url += "/"
|
|
||||||
|
|
||||||
self.global_settings = {}
|
self.global_settings = {}
|
||||||
try:
|
try:
|
||||||
@@ -44,7 +42,7 @@ class MandrillBackend(BaseEmailBackend):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass # no MANDRILL_SETTINGS setting
|
pass # no MANDRILL_SETTINGS setting
|
||||||
except (TypeError, ValueError): # e.g., not enumerable
|
except (TypeError, ValueError): # e.g., not enumerable
|
||||||
raise ImproperlyConfigured("MANDRILL_SETTINGS must be a dict or mapping")
|
raise AnymailImproperlyConfigured("MANDRILL_SETTINGS must be a dict or mapping")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.global_settings["subaccount"] = settings.MANDRILL_SUBACCOUNT
|
self.global_settings["subaccount"] = settings.MANDRILL_SUBACCOUNT
|
||||||
@@ -54,100 +52,7 @@ class MandrillBackend(BaseEmailBackend):
|
|||||||
self.ignore_recipient_status = getattr(settings, "MANDRILL_IGNORE_RECIPIENT_STATUS", False)
|
self.ignore_recipient_status = getattr(settings, "MANDRILL_IGNORE_RECIPIENT_STATUS", False)
|
||||||
self.session = None
|
self.session = None
|
||||||
|
|
||||||
def open(self):
|
def build_message_payload(self, message):
|
||||||
"""
|
|
||||||
Ensure we have a requests Session to connect to the Mandrill API.
|
|
||||||
Returns True if a new session was created (and the caller must close it).
|
|
||||||
"""
|
|
||||||
if self.session:
|
|
||||||
return False # already exists
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.session = requests.Session()
|
|
||||||
except requests.RequestException:
|
|
||||||
if not self.fail_silently:
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
self.session.headers["User-Agent"] = "Djrill/%s %s" % (
|
|
||||||
__version__, self.session.headers.get("User-Agent", ""))
|
|
||||||
return True
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""
|
|
||||||
Close the Mandrill API Session unconditionally.
|
|
||||||
|
|
||||||
(You should call this only if you called open and it returned True;
|
|
||||||
else someone else created the session and will clean it up themselves.)
|
|
||||||
"""
|
|
||||||
if self.session is None:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
self.session.close()
|
|
||||||
except requests.RequestException:
|
|
||||||
if not self.fail_silently:
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
self.session = None
|
|
||||||
|
|
||||||
def send_messages(self, email_messages):
|
|
||||||
"""
|
|
||||||
Sends one or more EmailMessage objects and returns the number of email
|
|
||||||
messages sent.
|
|
||||||
"""
|
|
||||||
if not email_messages:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
created_session = self.open()
|
|
||||||
if not self.session:
|
|
||||||
return 0 # exception in self.open with fail_silently
|
|
||||||
|
|
||||||
num_sent = 0
|
|
||||||
try:
|
|
||||||
for message in email_messages:
|
|
||||||
sent = self._send(message)
|
|
||||||
if sent:
|
|
||||||
num_sent += 1
|
|
||||||
finally:
|
|
||||||
if created_session:
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
return num_sent
|
|
||||||
|
|
||||||
def _send(self, message):
|
|
||||||
message.mandrill_response = None # until we have a response
|
|
||||||
if not message.recipients():
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
|
||||||
payload = self.get_base_payload()
|
|
||||||
self.build_send_payload(payload, message)
|
|
||||||
response = self.post_to_mandrill(payload, message)
|
|
||||||
|
|
||||||
# add the response from mandrill to the EmailMessage so callers can inspect it
|
|
||||||
message.mandrill_response = self.parse_response(response, payload, message)
|
|
||||||
self.validate_response(message.mandrill_response, response, payload, message)
|
|
||||||
|
|
||||||
except AnymailError:
|
|
||||||
# every *expected* error is derived from AnymailError;
|
|
||||||
# we deliberately don't silence unexpected errors
|
|
||||||
if not self.fail_silently:
|
|
||||||
raise
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_base_payload(self):
|
|
||||||
"""Return non-message-dependent payload for Mandrill send call
|
|
||||||
|
|
||||||
(The return value will be modified for the send, so must be a copy
|
|
||||||
of any shared state.)
|
|
||||||
"""
|
|
||||||
payload = {
|
|
||||||
"key": self.api_key,
|
|
||||||
}
|
|
||||||
return payload
|
|
||||||
|
|
||||||
def build_send_payload(self, payload, message):
|
|
||||||
"""Modify payload to add all message-specific options for Mandrill send call.
|
"""Modify payload to add all message-specific options for Mandrill send call.
|
||||||
|
|
||||||
payload is a dict that will become the Mandrill send data
|
payload is a dict that will become the Mandrill send data
|
||||||
@@ -160,12 +65,17 @@ class MandrillBackend(BaseEmailBackend):
|
|||||||
if getattr(message, 'alternatives', None):
|
if getattr(message, 'alternatives', None):
|
||||||
self._add_alternatives(message, msg_dict)
|
self._add_alternatives(message, msg_dict)
|
||||||
self._add_attachments(message, msg_dict)
|
self._add_attachments(message, msg_dict)
|
||||||
payload.setdefault('message', {}).update(msg_dict)
|
|
||||||
|
payload = {
|
||||||
|
"key": self.api_key,
|
||||||
|
"message": msg_dict,
|
||||||
|
}
|
||||||
if hasattr(message, 'template_name'):
|
if hasattr(message, 'template_name'):
|
||||||
payload['template_name'] = message.template_name
|
payload['template_name'] = message.template_name
|
||||||
payload['template_content'] = \
|
payload['template_content'] = \
|
||||||
self._expand_merge_vars(getattr(message, 'template_content', {}))
|
self._expand_merge_vars(getattr(message, 'template_content', {}))
|
||||||
self._add_mandrill_toplevel_options(message, payload)
|
self._add_mandrill_toplevel_options(message, payload)
|
||||||
|
return payload
|
||||||
|
|
||||||
def get_api_url(self, payload, message):
|
def get_api_url(self, payload, message):
|
||||||
"""Return the correct Mandrill API url for sending payload
|
"""Return the correct Mandrill API url for sending payload
|
||||||
@@ -178,67 +88,28 @@ class MandrillBackend(BaseEmailBackend):
|
|||||||
api_method = "messages/send.json"
|
api_method = "messages/send.json"
|
||||||
return urljoin(self.api_url, api_method)
|
return urljoin(self.api_url, api_method)
|
||||||
|
|
||||||
def serialize_payload(self, payload, message):
|
|
||||||
"""Return payload serialized to a json str.
|
|
||||||
|
|
||||||
Override this to substitute your own JSON serializer (e.g., to handle dates)
|
|
||||||
"""
|
|
||||||
return json.dumps(payload)
|
|
||||||
|
|
||||||
def post_to_mandrill(self, payload, message):
|
|
||||||
"""Post payload to correct Mandrill send API endpoint, and return the response.
|
|
||||||
|
|
||||||
payload is a dict to use as Mandrill send data
|
|
||||||
message is the original EmailMessage
|
|
||||||
return should be a requests.Response
|
|
||||||
|
|
||||||
Can raise AnymailSerializationError if payload is not serializable
|
|
||||||
Can raise AnymailRequestsAPIError for HTTP errors in the post
|
|
||||||
"""
|
|
||||||
api_url = self.get_api_url(payload, message)
|
|
||||||
try:
|
|
||||||
json_payload = self.serialize_payload(payload, message)
|
|
||||||
except TypeError as err:
|
|
||||||
# Add some context to the "not JSON serializable" message
|
|
||||||
raise AnymailSerializationError(
|
|
||||||
orig_err=err, email_message=message, payload=payload)
|
|
||||||
|
|
||||||
response = self.session.post(api_url, data=json_payload)
|
|
||||||
if response.status_code != 200:
|
|
||||||
raise AnymailRequestsAPIError(email_message=message, payload=payload, response=response)
|
|
||||||
return response
|
|
||||||
|
|
||||||
def parse_response(self, response, payload, message):
|
|
||||||
"""Return parsed json from Mandrill API response
|
|
||||||
|
|
||||||
Can raise AnymailRequestsAPIError if response is not valid JSON
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return response.json()
|
|
||||||
except ValueError:
|
|
||||||
raise AnymailRequestsAPIError("Invalid JSON in Mandrill API response",
|
|
||||||
email_message=message, payload=payload, response=response)
|
|
||||||
|
|
||||||
def validate_response(self, parsed_response, response, payload, message):
|
def validate_response(self, parsed_response, response, payload, message):
|
||||||
"""Validate parsed_response, raising exceptions for any problems.
|
"""Validate parsed_response, raising exceptions for any problems.
|
||||||
|
|
||||||
Extend this to provide your own validation checks.
|
|
||||||
Validation exceptions should inherit from anymail.exceptions.DjrillException
|
|
||||||
for proper fail_silently behavior.
|
|
||||||
|
|
||||||
The base version here checks for invalid or refused recipients.
|
|
||||||
"""
|
"""
|
||||||
if self.ignore_recipient_status:
|
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
recipient_status = [item["status"] for item in parsed_response]
|
unique_statuses = set([item["status"] for item in parsed_response])
|
||||||
except (KeyError, TypeError):
|
except (KeyError, TypeError):
|
||||||
raise AnymailRequestsAPIError("Invalid Mandrill API response format",
|
raise AnymailRequestsAPIError("Invalid Mandrill API response format",
|
||||||
email_message=message, payload=payload, response=response)
|
email_message=message, payload=payload, response=response)
|
||||||
|
|
||||||
|
if unique_statuses == {"sent"}:
|
||||||
|
return "sent"
|
||||||
|
elif unique_statuses == {"queued"}:
|
||||||
|
return "queued"
|
||||||
|
elif unique_statuses.issubset({"invalid", "rejected"}):
|
||||||
|
if self.ignore_recipient_status:
|
||||||
|
return "refused"
|
||||||
|
else:
|
||||||
# Error if *all* recipients are invalid or refused
|
# Error if *all* recipients are invalid or refused
|
||||||
# (This behavior parallels smtplib.SMTPRecipientsRefused from Django's SMTP EmailBackend)
|
# (This behavior parallels smtplib.SMTPRecipientsRefused from Django's SMTP EmailBackend)
|
||||||
if all([status in ('invalid', 'rejected') for status in recipient_status]):
|
|
||||||
raise AnymailRecipientsRefused(email_message=message, payload=payload, response=response)
|
raise AnymailRecipientsRefused(email_message=message, payload=payload, response=response)
|
||||||
|
else:
|
||||||
|
return "multi"
|
||||||
|
|
||||||
#
|
#
|
||||||
# Payload construction
|
# Payload construction
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
import json
|
import json
|
||||||
from requests import HTTPError
|
from requests import HTTPError
|
||||||
|
|
||||||
@@ -67,6 +68,10 @@ class AnymailError(Exception):
|
|||||||
return description
|
return description
|
||||||
|
|
||||||
|
|
||||||
|
class AnymailImproperlyConfigured(AnymailError, ImproperlyConfigured):
|
||||||
|
"""Exception for configuration problems"""
|
||||||
|
|
||||||
|
|
||||||
class AnymailAPIError(AnymailError):
|
class AnymailAPIError(AnymailError):
|
||||||
"""Exception for unsuccessful response from ESP's API."""
|
"""Exception for unsuccessful response from ESP's API."""
|
||||||
|
|
||||||
|
|||||||
@@ -525,7 +525,10 @@ class DjrillMandrillFeatureTests(DjrillBackendMockAPITestCase):
|
|||||||
msg = mail.EmailMessage('Subject', 'Message', 'from@example.com', ['to1@example.com'],)
|
msg = mail.EmailMessage('Subject', 'Message', 'from@example.com', ['to1@example.com'],)
|
||||||
sent = msg.send()
|
sent = msg.send()
|
||||||
self.assertEqual(sent, 1)
|
self.assertEqual(sent, 1)
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
self.assertEqual(msg.mandrill_response, response)
|
self.assertEqual(msg.mandrill_response, response)
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
self.assertEqual(msg.anymail_status, "sent")
|
||||||
|
|
||||||
def test_send_failed_mandrill_response(self):
|
def test_send_failed_mandrill_response(self):
|
||||||
""" If the send fails, mandrill_response should be set to None """
|
""" If the send fails, mandrill_response should be set to None """
|
||||||
@@ -533,7 +536,10 @@ class DjrillMandrillFeatureTests(DjrillBackendMockAPITestCase):
|
|||||||
msg = mail.EmailMessage('Subject', 'Message', 'from@example.com', ['to1@example.com'],)
|
msg = mail.EmailMessage('Subject', 'Message', 'from@example.com', ['to1@example.com'],)
|
||||||
sent = msg.send(fail_silently=True)
|
sent = msg.send(fail_silently=True)
|
||||||
self.assertEqual(sent, 0)
|
self.assertEqual(sent, 0)
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
self.assertIsNone(msg.mandrill_response)
|
self.assertIsNone(msg.mandrill_response)
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
self.assertIsNone(msg.anymail_status)
|
||||||
|
|
||||||
def test_send_unparsable_mandrill_response(self):
|
def test_send_unparsable_mandrill_response(self):
|
||||||
"""If the send succeeds, but a non-JSON API response, should raise an API exception"""
|
"""If the send succeeds, but a non-JSON API response, should raise an API exception"""
|
||||||
@@ -541,7 +547,10 @@ class DjrillMandrillFeatureTests(DjrillBackendMockAPITestCase):
|
|||||||
msg = mail.EmailMessage('Subject', 'Message', 'from@example.com', ['to1@example.com'],)
|
msg = mail.EmailMessage('Subject', 'Message', 'from@example.com', ['to1@example.com'],)
|
||||||
with self.assertRaises(AnymailAPIError):
|
with self.assertRaises(AnymailAPIError):
|
||||||
msg.send()
|
msg.send()
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
self.assertIsNone(msg.mandrill_response)
|
self.assertIsNone(msg.mandrill_response)
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
self.assertIsNone(msg.anymail_status)
|
||||||
|
|
||||||
def test_json_serialization_errors(self):
|
def test_json_serialization_errors(self):
|
||||||
"""Try to provide more information about non-json-serializable data"""
|
"""Try to provide more information about non-json-serializable data"""
|
||||||
|
|||||||
Reference in New Issue
Block a user