mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 11:51:05 -05:00
For MANDRILL_API_KEY (e.g.,), look for these settings:
* ANYMAIL = { 'MANDRILL_API_KEY': '...' }
* ANYMAIL_MANDRILL_API_KEY = "..."
* MANDRILL_API_KEY = "..."
(the "bare" third version is used only for settings that
might be reasonably shared with other apps, like api keys)
237 lines
8.4 KiB
Python
237 lines
8.4 KiB
Python
from datetime import date, datetime
|
|
|
|
from ..exceptions import AnymailRequestsAPIError, AnymailRecipientsRefused
|
|
from ..utils import last, combine, get_anymail_setting
|
|
|
|
from .base import AnymailRequestsBackend, RequestsPayload
|
|
|
|
|
|
class MandrillBackend(AnymailRequestsBackend):
|
|
"""
|
|
Mandrill API Email Backend
|
|
"""
|
|
|
|
def __init__(self, **kwargs):
|
|
"""Init options from Django settings"""
|
|
self.api_key = get_anymail_setting('MANDRILL_API_KEY', allow_bare=True)
|
|
api_url = get_anymail_setting("MANDRILL_API_URL", "https://mandrillapp.com/api/1.0")
|
|
if not api_url.endswith("/"):
|
|
api_url += "/"
|
|
super(MandrillBackend, self).__init__(api_url, **kwargs)
|
|
|
|
def build_message_payload(self, message):
|
|
return MandrillPayload(message, self.send_defaults, self)
|
|
|
|
def validate_response(self, parsed_response, response, payload, message):
|
|
"""Validate parsed_response, raising exceptions for any problems.
|
|
"""
|
|
try:
|
|
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)
|
|
|
|
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"
|
|
|
|
|
|
def _expand_merge_vars(vardict):
|
|
"""Convert a Python dict to an array of name-content used by Mandrill.
|
|
|
|
{ name: value, ... } --> [ {'name': name, 'content': value }, ... ]
|
|
"""
|
|
# For testing reproducibility, we sort the keys
|
|
return [{'name': name, 'content': vardict[name]}
|
|
for name in sorted(vardict.keys())]
|
|
|
|
|
|
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 MandrillPayload(RequestsPayload):
|
|
|
|
def get_api_endpoint(self):
|
|
if 'template_name' in self.data:
|
|
return "messages/send-template.json"
|
|
else:
|
|
return "messages/send.json"
|
|
|
|
def serialize_data(self):
|
|
return self.serialize_json(self.data)
|
|
|
|
#
|
|
# Payload construction
|
|
#
|
|
|
|
def init_payload(self):
|
|
self.data = {
|
|
"key": self.backend.api_key,
|
|
"message": {},
|
|
}
|
|
|
|
def set_from_email(self, email):
|
|
if not getattr(self.message, "use_template_from", False): # Djrill compat!
|
|
self.data["message"]["from_email"] = email.email
|
|
if email.name:
|
|
self.data["message"]["from_name"] = email.name
|
|
|
|
def add_recipient(self, recipient_type, email):
|
|
assert recipient_type in ["to", "cc", "bcc"]
|
|
to_list = self.data["message"].setdefault("to", [])
|
|
to_list.append({"email": email.email, "name": email.name, "type": recipient_type})
|
|
|
|
def set_subject(self, subject):
|
|
if not getattr(self.message, "use_template_subject", False): # Djrill compat!
|
|
self.data["message"]["subject"] = subject
|
|
|
|
def set_reply_to(self, emails):
|
|
reply_to = ", ".join([str(email) for email in emails])
|
|
self.data["message"].setdefault("headers", {})["Reply-To"] = reply_to
|
|
|
|
def set_extra_headers(self, headers):
|
|
self.data["message"].setdefault("headers", {}).update(headers)
|
|
|
|
def set_text_body(self, body):
|
|
self.data["message"]["text"] = body
|
|
|
|
def set_html_body(self, body):
|
|
self.data["message"]["html"] = body
|
|
|
|
def add_alternative(self, content, mimetype):
|
|
if mimetype != 'text/html':
|
|
self.unsupported_feature("alternative part with mimetype '%s'" % mimetype)
|
|
if "html" in self.data["message"]:
|
|
self.unsupported_feature("multiple html parts")
|
|
self.data["message"]["html"] = content
|
|
|
|
def add_attachment(self, attachment):
|
|
key = "images" if attachment.inline else "attachments"
|
|
self.data["message"].setdefault(key, []).append({
|
|
"type": attachment.mimetype,
|
|
"name": attachment.name or "",
|
|
"content": attachment.b64content
|
|
})
|
|
|
|
def set_metadata(self, metadata):
|
|
self.data["message"]["metadata"] = metadata
|
|
|
|
def set_send_at(self, send_at):
|
|
self.data["send_at"] = encode_date_for_mandrill(send_at)
|
|
|
|
def set_tags(self, tags):
|
|
self.data["message"]["tags"] = tags
|
|
|
|
def set_track_clicks(self, track_clicks):
|
|
self.data["message"]["track_clicks"] = track_clicks
|
|
|
|
def set_track_opens(self, track_opens):
|
|
self.data["message"]["track_opens"] = track_opens
|
|
|
|
def set_esp_extra(self, extra):
|
|
pass
|
|
|
|
# Djrill leftovers
|
|
|
|
esp_message_attrs = (
|
|
('async', last, None),
|
|
('ip_pool', last, None),
|
|
('from_name', last, None), # overrides display name parsed from from_email above
|
|
('important', last, None),
|
|
('auto_text', last, None),
|
|
('auto_html', last, None),
|
|
('inline_css', last, None),
|
|
('url_strip_qs', last, None),
|
|
('tracking_domain', last, None),
|
|
('signing_domain', last, None),
|
|
('return_path_domain', last, None),
|
|
('merge_language', last, None),
|
|
('preserve_recipients', last, None),
|
|
('view_content_link', last, None),
|
|
('subaccount', last, None),
|
|
('google_analytics_domains', last, None),
|
|
('google_analytics_campaign', last, None),
|
|
('global_merge_vars', combine, _expand_merge_vars),
|
|
('merge_vars', combine, None),
|
|
('recipient_metadata', combine, None),
|
|
('template_name', last, None),
|
|
('template_content', combine, _expand_merge_vars),
|
|
)
|
|
|
|
def set_async(self, async):
|
|
self.data["async"] = async
|
|
|
|
def set_ip_pool(self, ip_pool):
|
|
self.data["ip_pool"] = ip_pool
|
|
|
|
def set_template_name(self, template_name):
|
|
self.data["template_name"] = template_name
|
|
self.data.setdefault("template_content", []) # Mandrill requires something here
|
|
|
|
def set_template_content(self, template_content):
|
|
self.data["template_content"] = template_content
|
|
|
|
def set_merge_vars(self, merge_vars):
|
|
# For testing reproducibility, we sort the recipients
|
|
self.data['message']['merge_vars'] = [
|
|
{'rcpt': rcpt, 'vars': _expand_merge_vars(merge_vars[rcpt])}
|
|
for rcpt in sorted(merge_vars.keys())
|
|
]
|
|
|
|
def set_recipient_metadata(self, recipient_metadata):
|
|
# For testing reproducibility, we sort the recipients
|
|
self.data['message']['recipient_metadata'] = [
|
|
{'rcpt': rcpt, 'values': recipient_metadata[rcpt]}
|
|
for rcpt in sorted(recipient_metadata.keys())
|
|
]
|
|
|
|
# Set up simple set_<attr> functions for any missing esp_message_attrs attrs
|
|
# (avoids dozens of simple `self.data["message"][<attr>] = value` functions)
|
|
|
|
@classmethod
|
|
def define_message_attr_setters(cls):
|
|
for (attr, _, _) in cls.esp_message_attrs:
|
|
setter_name = 'set_%s' % attr
|
|
try:
|
|
getattr(cls, setter_name)
|
|
except AttributeError:
|
|
setter = cls.make_setter(attr, setter_name)
|
|
setattr(cls, setter_name, setter)
|
|
|
|
@staticmethod
|
|
def make_setter(attr, setter_name):
|
|
# sure wish we could use functools.partial to create instance methods (descriptors)
|
|
def setter(self, value):
|
|
self.data["message"][attr] = value
|
|
setter.__name__ = setter_name
|
|
return setter
|
|
|
|
MandrillPayload.define_message_attr_setters()
|