mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 11:51:05 -05:00
Refactor backend
* Break apart massive _send call * Try to facilitate subclassing * Centralize fail_silently handling during _send * Include original EmailMessage as exception attr
This commit is contained in:
@@ -12,9 +12,11 @@ class DjrillError(Exception):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Optional kwargs:
|
Optional kwargs:
|
||||||
|
email_message: the original EmailMessage being sent
|
||||||
payload: data arg (*not* json-stringified) for the Mandrill send call
|
payload: data arg (*not* json-stringified) for the Mandrill send call
|
||||||
response: requests.Response from the send call
|
response: requests.Response from the send call
|
||||||
"""
|
"""
|
||||||
|
self.email_message = kwargs.pop('email_message', None)
|
||||||
self.payload = kwargs.pop('payload', None)
|
self.payload = kwargs.pop('payload', None)
|
||||||
if isinstance(self, HTTPError):
|
if isinstance(self, HTTPError):
|
||||||
# must leave response in kwargs for HTTPError
|
# must leave response in kwargs for HTTPError
|
||||||
@@ -57,7 +59,7 @@ class DjrillError(Exception):
|
|||||||
description += "\n" + json.dumps(json_response, indent=2)
|
description += "\n" + json.dumps(json_response, indent=2)
|
||||||
except (AttributeError, KeyError, ValueError): # not JSON = ValueError
|
except (AttributeError, KeyError, ValueError): # not JSON = ValueError
|
||||||
try:
|
try:
|
||||||
description += self.response.text
|
description += " " + self.response.text
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
return description
|
return description
|
||||||
|
|||||||
@@ -12,30 +12,10 @@ 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 ..._version import __version__
|
||||||
from ...exceptions import (MandrillAPIError, MandrillRecipientsRefused,
|
from ...exceptions import (DjrillError, MandrillAPIError, MandrillRecipientsRefused,
|
||||||
NotSerializableForMandrillError, NotSupportedByMandrillError)
|
NotSerializableForMandrillError, NotSupportedByMandrillError)
|
||||||
|
|
||||||
|
|
||||||
def encode_date_for_mandrill(dt):
|
|
||||||
"""Format a date or datetime for use as a Mandrill API date field
|
|
||||||
|
|
||||||
datetime becomes "YYYY-MM-DD HH:MM:SS"
|
|
||||||
converted to UTC, if timezone-aware
|
|
||||||
microseconds removed
|
|
||||||
date becomes "YYYY-MM-DD 00:00:00"
|
|
||||||
anything else gets returned intact
|
|
||||||
"""
|
|
||||||
if isinstance(dt, datetime):
|
|
||||||
dt = dt.replace(microsecond=0)
|
|
||||||
if dt.utcoffset() is not None:
|
|
||||||
dt = (dt - dt.utcoffset()).replace(tzinfo=None)
|
|
||||||
return dt.isoformat(' ')
|
|
||||||
elif isinstance(dt, date):
|
|
||||||
return dt.isoformat() + ' 00:00:00'
|
|
||||||
else:
|
|
||||||
return dt
|
|
||||||
|
|
||||||
|
|
||||||
class DjrillBackend(BaseEmailBackend):
|
class DjrillBackend(BaseEmailBackend):
|
||||||
"""
|
"""
|
||||||
Mandrill API Email Backend
|
Mandrill API Email Backend
|
||||||
@@ -63,9 +43,6 @@ class DjrillBackend(BaseEmailBackend):
|
|||||||
raise ImproperlyConfigured("You have not set your mandrill api key "
|
raise ImproperlyConfigured("You have not set your mandrill api key "
|
||||||
"in the settings.py file.")
|
"in the settings.py file.")
|
||||||
|
|
||||||
self.api_send = self.api_url + "/messages/send.json"
|
|
||||||
self.api_send_template = self.api_url + "/messages/send-template.json"
|
|
||||||
|
|
||||||
def open(self):
|
def open(self):
|
||||||
"""
|
"""
|
||||||
Ensure we have a requests Session to connect to the Mandrill API.
|
Ensure we have a requests Session to connect to the Mandrill API.
|
||||||
@@ -127,72 +104,133 @@ class DjrillBackend(BaseEmailBackend):
|
|||||||
|
|
||||||
def _send(self, message):
|
def _send(self, message):
|
||||||
message.mandrill_response = None # until we have a response
|
message.mandrill_response = None # until we have a response
|
||||||
|
|
||||||
if not message.recipients():
|
if not message.recipients():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
api_url = self.api_send
|
|
||||||
api_params = {
|
|
||||||
"key": self.api_key,
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
msg_dict = self._build_standard_message_dict(message)
|
payload = self.get_base_payload()
|
||||||
self._add_mandrill_options(message, msg_dict)
|
self.build_send_payload(payload, message)
|
||||||
if getattr(message, 'alternatives', None):
|
response = self.post_to_mandrill(payload, message)
|
||||||
self._add_alternatives(message, msg_dict)
|
|
||||||
self._add_attachments(message, msg_dict)
|
|
||||||
api_params['message'] = msg_dict
|
|
||||||
|
|
||||||
# check if template is set in message to send it via
|
# add the response from mandrill to the EmailMessage so callers can inspect it
|
||||||
# api url: /messages/send-template.json
|
message.mandrill_response = self.parse_response(response, payload, message)
|
||||||
if hasattr(message, 'template_name'):
|
self.validate_response(message.mandrill_response, response, payload, message)
|
||||||
api_url = self.api_send_template
|
|
||||||
api_params['template_name'] = message.template_name
|
|
||||||
api_params['template_content'] = \
|
|
||||||
self._expand_merge_vars(getattr(message, 'template_content', {}))
|
|
||||||
|
|
||||||
self._add_mandrill_toplevel_options(message, api_params)
|
except DjrillError:
|
||||||
|
# every *expected* error is derived from DjrillError;
|
||||||
except NotSupportedByMandrillError:
|
# we deliberately don't silence unexpected errors
|
||||||
if not self.fail_silently:
|
if not self.fail_silently:
|
||||||
raise
|
raise
|
||||||
return False
|
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.
|
||||||
|
|
||||||
|
payload is a dict that will become the Mandrill send data
|
||||||
|
message is an EmailMessage, possibly with additional Mandrill-specific attrs
|
||||||
|
|
||||||
|
Can raise NotSupportedByMandrillError for unsupported options in message.
|
||||||
|
"""
|
||||||
|
msg_dict = self._build_standard_message_dict(message)
|
||||||
|
self._add_mandrill_options(message, msg_dict)
|
||||||
|
if getattr(message, 'alternatives', None):
|
||||||
|
self._add_alternatives(message, msg_dict)
|
||||||
|
self._add_attachments(message, msg_dict)
|
||||||
|
payload.setdefault('message', {}).update(msg_dict)
|
||||||
|
if hasattr(message, 'template_name'):
|
||||||
|
payload['template_name'] = message.template_name
|
||||||
|
payload['template_content'] = \
|
||||||
|
self._expand_merge_vars(getattr(message, 'template_content', {}))
|
||||||
|
self._add_mandrill_toplevel_options(message, payload)
|
||||||
|
|
||||||
|
def get_api_url(self, payload, message):
|
||||||
|
"""Return the correct Mandrill API url for sending payload
|
||||||
|
|
||||||
|
Override this to substitute your own logic for determining API endpoint.
|
||||||
|
"""
|
||||||
|
if 'template_name' in payload:
|
||||||
|
return self.api_url + "/messages/send-template.json"
|
||||||
|
else:
|
||||||
|
return self.api_url + "/messages/send.json"
|
||||||
|
|
||||||
|
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 NotSerializableForMandrillError if payload is not serializable
|
||||||
|
Can raise MandrillAPIError for HTTP errors in the post
|
||||||
|
"""
|
||||||
|
api_url = self.get_api_url(payload, message)
|
||||||
try:
|
try:
|
||||||
api_data = json.dumps(api_params)
|
json_payload = self.serialize_payload(payload, message)
|
||||||
except TypeError as err:
|
except TypeError as err:
|
||||||
if self.fail_silently:
|
|
||||||
return False
|
|
||||||
# Add some context to the "not JSON serializable" message
|
# Add some context to the "not JSON serializable" message
|
||||||
raise NotSerializableForMandrillError(orig_err=err)
|
raise NotSerializableForMandrillError(
|
||||||
|
orig_err=err, email_message=message, payload=payload)
|
||||||
response = self.session.post(api_url, data=api_data)
|
|
||||||
|
|
||||||
|
response = self.session.post(api_url, data=json_payload)
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
if not self.fail_silently:
|
raise MandrillAPIError(email_message=message, payload=payload, response=response)
|
||||||
raise MandrillAPIError(payload=api_params, response=response)
|
return response
|
||||||
return False
|
|
||||||
|
|
||||||
# add the response from mandrill to the EmailMessage so callers can inspect it
|
def parse_response(self, response, payload, message):
|
||||||
|
"""Return parsed json from Mandrill API response
|
||||||
|
|
||||||
|
Can raise MandrillAPIError if response is not valid JSON
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
message.mandrill_response = response.json()
|
return response.json()
|
||||||
recipient_status = [item["status"] for item in message.mandrill_response]
|
except ValueError:
|
||||||
except (ValueError, KeyError):
|
raise MandrillAPIError("Invalid JSON in Mandrill API response",
|
||||||
if not self.fail_silently:
|
email_message=message, payload=payload, response=response)
|
||||||
raise MandrillAPIError("Error parsing Mandrill API response",
|
|
||||||
payload=api_params, response=response)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
def validate_response(self, parsed_response, response, payload, message):
|
||||||
|
"""Validate parsed_response, raising exceptions for any problems.
|
||||||
|
|
||||||
|
Extend this to provide your own validation checks.
|
||||||
|
Validation exceptions should inherit from djrill.exceptions.DjrillException
|
||||||
|
for proper fail_silently behavior.
|
||||||
|
|
||||||
|
The base version here checks for invalid or refused recipients.
|
||||||
|
"""
|
||||||
|
if self.ignore_recipient_status:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
recipient_status = [item["status"] for item in parsed_response]
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
raise MandrillAPIError("Invalid Mandrill API response format",
|
||||||
|
email_message=message, payload=payload, response=response)
|
||||||
# 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 (not self.ignore_recipient_status and
|
if all([status in ('invalid', 'rejected') for status in recipient_status]):
|
||||||
all([status in ('invalid', 'rejected') for status in recipient_status])):
|
raise MandrillRecipientsRefused(email_message=message, payload=payload, response=response)
|
||||||
if not self.fail_silently:
|
|
||||||
raise MandrillRecipientsRefused(payload=api_params, response=response)
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
#
|
||||||
|
# Payload construction
|
||||||
|
#
|
||||||
|
|
||||||
def _build_standard_message_dict(self, message):
|
def _build_standard_message_dict(self, message):
|
||||||
"""Create a Mandrill send message struct from a Django EmailMessage.
|
"""Create a Mandrill send message struct from a Django EmailMessage.
|
||||||
@@ -251,8 +289,7 @@ class DjrillBackend(BaseEmailBackend):
|
|||||||
|
|
||||||
# Mandrill attributes that require conversion:
|
# Mandrill attributes that require conversion:
|
||||||
if hasattr(message, 'send_at'):
|
if hasattr(message, 'send_at'):
|
||||||
api_params['send_at'] = encode_date_for_mandrill(message.send_at)
|
api_params['send_at'] = self.encode_date_for_mandrill(message.send_at)
|
||||||
|
|
||||||
|
|
||||||
def _make_mandrill_to_list(self, message, recipients, recipient_type="to"):
|
def _make_mandrill_to_list(self, message, recipients, recipient_type="to"):
|
||||||
"""Create a Mandrill 'to' field from a list of emails.
|
"""Create a Mandrill 'to' field from a list of emails.
|
||||||
@@ -335,14 +372,16 @@ class DjrillBackend(BaseEmailBackend):
|
|||||||
if len(message.alternatives) > 1:
|
if len(message.alternatives) > 1:
|
||||||
raise NotSupportedByMandrillError(
|
raise NotSupportedByMandrillError(
|
||||||
"Too many alternatives attached to the message. "
|
"Too many alternatives attached to the message. "
|
||||||
"Mandrill only accepts plain text and html emails.")
|
"Mandrill only accepts plain text and html emails.",
|
||||||
|
email_message=message)
|
||||||
|
|
||||||
(content, mimetype) = message.alternatives[0]
|
(content, mimetype) = message.alternatives[0]
|
||||||
if mimetype != 'text/html':
|
if mimetype != 'text/html':
|
||||||
raise NotSupportedByMandrillError(
|
raise NotSupportedByMandrillError(
|
||||||
"Invalid alternative mimetype '%s'. "
|
"Invalid alternative mimetype '%s'. "
|
||||||
"Mandrill only accepts plain text and html emails."
|
"Mandrill only accepts plain text and html emails."
|
||||||
% mimetype)
|
% mimetype,
|
||||||
|
email_message=message)
|
||||||
|
|
||||||
msg_dict['html'] = content
|
msg_dict['html'] = content
|
||||||
|
|
||||||
@@ -413,3 +452,23 @@ class DjrillBackend(BaseEmailBackend):
|
|||||||
'content': content_b64.decode('ascii'),
|
'content': content_b64.decode('ascii'),
|
||||||
}
|
}
|
||||||
return mandrill_attachment, is_embedded_image
|
return mandrill_attachment, is_embedded_image
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def encode_date_for_mandrill(cls, dt):
|
||||||
|
"""Format a date or datetime for use as a Mandrill API date field
|
||||||
|
|
||||||
|
datetime becomes "YYYY-MM-DD HH:MM:SS"
|
||||||
|
converted to UTC, if timezone-aware
|
||||||
|
microseconds removed
|
||||||
|
date becomes "YYYY-MM-DD 00:00:00"
|
||||||
|
anything else gets returned intact
|
||||||
|
"""
|
||||||
|
if isinstance(dt, datetime):
|
||||||
|
dt = dt.replace(microsecond=0)
|
||||||
|
if dt.utcoffset() is not None:
|
||||||
|
dt = (dt - dt.utcoffset()).replace(tzinfo=None)
|
||||||
|
return dt.isoformat(' ')
|
||||||
|
elif isinstance(dt, date):
|
||||||
|
return dt.isoformat() + ' 00:00:00'
|
||||||
|
else:
|
||||||
|
return dt
|
||||||
|
|||||||
@@ -79,6 +79,14 @@ Removed DjrillBackendHTTPError
|
|||||||
This exception was deprecated in Djrill 0.3. Replace uses of it
|
This exception was deprecated in Djrill 0.3. Replace uses of it
|
||||||
with :exc:`djrill.MandrillAPIError`.
|
with :exc:`djrill.MandrillAPIError`.
|
||||||
|
|
||||||
|
Refactored Djrill backend and exceptions
|
||||||
|
Several internal details of ``djrill.mail.backends.DjrillBackend``
|
||||||
|
and Djrill's exception classes have been significantly updated for 2.0.
|
||||||
|
The intent is to make it easier to maintain and extend the backend
|
||||||
|
(including creating your own subclasses to override Djrill's default
|
||||||
|
behavior). As a result, though, any existing code that depended on
|
||||||
|
undocumented Djrill internals may need to be updated.
|
||||||
|
|
||||||
|
|
||||||
Other Djrill 2.0 Changes
|
Other Djrill 2.0 Changes
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|||||||
Reference in New Issue
Block a user