mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
Start factoring out common backend functionality
This commit is contained in:
@@ -1,6 +1,4 @@
|
||||
import json
|
||||
import mimetypes
|
||||
import requests
|
||||
from base64 import b64encode
|
||||
from datetime import date, datetime
|
||||
from email.mime.base import MIMEBase
|
||||
@@ -11,32 +9,32 @@ except ImportError:
|
||||
from urllib.parse import urljoin # python 3
|
||||
|
||||
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 .._version import __version__
|
||||
from ..exceptions import (AnymailError, AnymailRequestsAPIError, AnymailRecipientsRefused,
|
||||
AnymailSerializationError, AnymailUnsupportedFeature)
|
||||
from ..exceptions import (AnymailImproperlyConfigured,
|
||||
AnymailRequestsAPIError, AnymailRecipientsRefused,
|
||||
AnymailUnsupportedFeature)
|
||||
|
||||
from .base import AnymailRequestsBackend
|
||||
|
||||
|
||||
class MandrillBackend(BaseEmailBackend):
|
||||
class MandrillBackend(AnymailRequestsBackend):
|
||||
"""
|
||||
Mandrill API Email Backend
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""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:
|
||||
self.api_key = settings.MANDRILL_API_KEY
|
||||
except AttributeError:
|
||||
raise ImproperlyConfigured("Set MANDRILL_API_KEY in settings.py to use Djrill")
|
||||
|
||||
self.api_url = getattr(settings, "MANDRILL_API_URL", "https://mandrillapp.com/api/1.0")
|
||||
if not self.api_url.endswith("/"):
|
||||
self.api_url += "/"
|
||||
raise AnymailImproperlyConfigured("Set MANDRILL_API_KEY in settings.py to use Anymail Mandrill backend")
|
||||
|
||||
self.global_settings = {}
|
||||
try:
|
||||
@@ -44,7 +42,7 @@ class MandrillBackend(BaseEmailBackend):
|
||||
except AttributeError:
|
||||
pass # no MANDRILL_SETTINGS setting
|
||||
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:
|
||||
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.session = None
|
||||
|
||||
def open(self):
|
||||
"""
|
||||
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):
|
||||
def build_message_payload(self, message):
|
||||
"""Modify payload to add all message-specific options for Mandrill send call.
|
||||
|
||||
payload is a dict that will become the Mandrill send data
|
||||
@@ -160,12 +65,17 @@ class MandrillBackend(BaseEmailBackend):
|
||||
if getattr(message, 'alternatives', None):
|
||||
self._add_alternatives(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'):
|
||||
payload['template_name'] = message.template_name
|
||||
payload['template_content'] = \
|
||||
self._expand_merge_vars(getattr(message, 'template_content', {}))
|
||||
self._add_mandrill_toplevel_options(message, payload)
|
||||
return payload
|
||||
|
||||
def get_api_url(self, payload, message):
|
||||
"""Return the correct Mandrill API url for sending payload
|
||||
@@ -178,67 +88,28 @@ class MandrillBackend(BaseEmailBackend):
|
||||
api_method = "messages/send.json"
|
||||
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):
|
||||
"""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:
|
||||
recipient_status = [item["status"] for item in parsed_response]
|
||||
unique_statuses = set([item["status"] for item in parsed_response])
|
||||
except (KeyError, TypeError):
|
||||
raise AnymailRequestsAPIError("Invalid Mandrill API response format",
|
||||
email_message=message, payload=payload, response=response)
|
||||
# Error if *all* recipients are invalid or refused
|
||||
# (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)
|
||||
|
||||
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
|
||||
# (This behavior parallels smtplib.SMTPRecipientsRefused from Django's SMTP EmailBackend)
|
||||
raise AnymailRecipientsRefused(email_message=message, payload=payload, response=response)
|
||||
else:
|
||||
return "multi"
|
||||
|
||||
#
|
||||
# Payload construction
|
||||
|
||||
Reference in New Issue
Block a user