mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 11:51:05 -05:00
247 lines
8.8 KiB
Python
247 lines
8.8 KiB
Python
from datetime import datetime
|
|
|
|
from ..exceptions import AnymailRequestsAPIError
|
|
from ..message import ANYMAIL_STATUSES, AnymailRecipientStatus
|
|
from ..utils import get_anymail_setting
|
|
from .base_requests import AnymailRequestsBackend, RequestsPayload
|
|
|
|
|
|
class EmailBackend(AnymailRequestsBackend):
|
|
"""
|
|
Mandrill API Email Backend
|
|
"""
|
|
|
|
esp_name = "Mandrill"
|
|
|
|
def __init__(self, **kwargs):
|
|
"""Init options from Django settings"""
|
|
esp_name = self.esp_name
|
|
self.api_key = get_anymail_setting(
|
|
"api_key", esp_name=esp_name, kwargs=kwargs, allow_bare=True
|
|
)
|
|
api_url = get_anymail_setting(
|
|
"api_url",
|
|
esp_name=esp_name,
|
|
kwargs=kwargs,
|
|
default="https://mandrillapp.com/api/1.0",
|
|
)
|
|
if not api_url.endswith("/"):
|
|
api_url += "/"
|
|
super().__init__(api_url, **kwargs)
|
|
|
|
def build_message_payload(self, message, defaults):
|
|
return MandrillPayload(message, defaults, self)
|
|
|
|
def parse_recipient_status(self, response, payload, message):
|
|
parsed_response = self.deserialize_json_response(response, payload, message)
|
|
recipient_status = {}
|
|
try:
|
|
# Mandrill returns a list of { email, status, _id, reject_reason }
|
|
# for each recipient
|
|
for item in parsed_response:
|
|
email = item["email"]
|
|
status = item["status"]
|
|
if status not in ANYMAIL_STATUSES:
|
|
status = "unknown"
|
|
# "_id" can be missing for invalid/rejected recipients:
|
|
message_id = item.get("_id", None)
|
|
recipient_status[email] = AnymailRecipientStatus(
|
|
message_id=message_id, status=status
|
|
)
|
|
except (KeyError, TypeError) as err:
|
|
raise AnymailRequestsAPIError(
|
|
"Invalid Mandrill API response format",
|
|
email_message=message,
|
|
payload=payload,
|
|
response=response,
|
|
backend=self,
|
|
) from err
|
|
return recipient_status
|
|
|
|
|
|
def encode_date_for_mandrill(dt):
|
|
"""Format a datetime for use as a Mandrill API date field
|
|
|
|
Mandrill expects "YYYY-MM-DD HH:MM:SS" in UTC
|
|
"""
|
|
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(" ")
|
|
else:
|
|
return dt
|
|
|
|
|
|
class MandrillPayload(RequestsPayload):
|
|
def __init__(self, *args, **kwargs):
|
|
self.esp_extra = {} # late-bound in serialize_data
|
|
super().__init__(*args, **kwargs)
|
|
|
|
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):
|
|
self.process_esp_extra()
|
|
if self.is_batch():
|
|
# hide recipients from each other
|
|
self.data["message"]["preserve_recipients"] = False
|
|
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):
|
|
self.data["message"]["from_email"] = email.addr_spec
|
|
if email.display_name:
|
|
self.data["message"]["from_name"] = email.display_name
|
|
|
|
def add_recipient(self, recipient_type, email):
|
|
assert recipient_type in ["to", "cc", "bcc"]
|
|
recipient_data = {"email": email.addr_spec, "type": recipient_type}
|
|
if email.display_name:
|
|
recipient_data["name"] = email.display_name
|
|
to_list = self.data["message"].setdefault("to", [])
|
|
to_list.append(recipient_data)
|
|
|
|
def set_subject(self, subject):
|
|
self.data["message"]["subject"] = subject
|
|
|
|
def set_reply_to(self, emails):
|
|
if 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):
|
|
if "html" in self.data["message"]:
|
|
# second html body could show up through multiple alternatives,
|
|
# or html body + alternative
|
|
self.unsupported_feature("multiple html parts")
|
|
self.data["message"]["html"] = body
|
|
|
|
def add_attachment(self, attachment):
|
|
if attachment.inline:
|
|
field = "images"
|
|
name = attachment.cid
|
|
else:
|
|
field = "attachments"
|
|
name = attachment.name or ""
|
|
self.data["message"].setdefault(field, []).append(
|
|
{
|
|
"type": attachment.mimetype,
|
|
"name": name,
|
|
"content": attachment.b64content,
|
|
}
|
|
)
|
|
|
|
def set_envelope_sender(self, email):
|
|
# Only the domain is used
|
|
self.data["message"]["return_path_domain"] = email.domain
|
|
|
|
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_template_id(self, template_id):
|
|
self.data["template_name"] = template_id
|
|
self.data.setdefault("template_content", []) # Mandrill requires something here
|
|
|
|
def set_merge_data(self, merge_data):
|
|
self.data["message"]["merge_vars"] = [
|
|
{
|
|
"rcpt": rcpt,
|
|
"vars": [
|
|
# sort for testing reproducibility:
|
|
{"name": key, "content": rcpt_data[key]}
|
|
for key in sorted(rcpt_data.keys())
|
|
],
|
|
}
|
|
for rcpt, rcpt_data in merge_data.items()
|
|
]
|
|
|
|
def set_merge_global_data(self, merge_global_data):
|
|
self.data["message"]["global_merge_vars"] = [
|
|
{"name": var, "content": value} for var, value in merge_global_data.items()
|
|
]
|
|
|
|
def set_merge_metadata(self, merge_metadata):
|
|
# recipient_metadata format is similar to, but not quite the same as,
|
|
# merge_vars:
|
|
self.data["message"]["recipient_metadata"] = [
|
|
{"rcpt": rcpt, "values": rcpt_data}
|
|
for rcpt, rcpt_data in merge_metadata.items()
|
|
]
|
|
|
|
def set_esp_extra(self, extra):
|
|
# late bind in serialize_data, so that obsolete Djrill attrs can contribute
|
|
self.esp_extra = extra
|
|
|
|
def process_esp_extra(self):
|
|
if self.esp_extra is not None and len(self.esp_extra) > 0:
|
|
esp_extra = self.esp_extra
|
|
# Convert pythonic template_content dict to Mandrill name/content list
|
|
try:
|
|
template_content = esp_extra["template_content"]
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
# if it's dict-like:
|
|
if hasattr(template_content, "items"):
|
|
if esp_extra is self.esp_extra:
|
|
# don't modify caller's value
|
|
esp_extra = self.esp_extra.copy()
|
|
esp_extra["template_content"] = [
|
|
{"name": var, "content": value}
|
|
for var, value in template_content.items()
|
|
]
|
|
# Convert pythonic recipient_metadata dict to Mandrill rcpt/values list
|
|
try:
|
|
recipient_metadata = esp_extra["message"]["recipient_metadata"]
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
# if it's dict-like:
|
|
if hasattr(recipient_metadata, "keys"):
|
|
if esp_extra["message"] is self.esp_extra["message"]:
|
|
# don't modify caller's value:
|
|
esp_extra["message"] = self.esp_extra["message"].copy()
|
|
# For testing reproducibility, sort the recipients
|
|
esp_extra["message"]["recipient_metadata"] = [
|
|
{"rcpt": rcpt, "values": recipient_metadata[rcpt]}
|
|
for rcpt in sorted(recipient_metadata.keys())
|
|
]
|
|
# Merge esp_extra with payload data: shallow merge within ['message']
|
|
# and top-level keys
|
|
self.data.update({k: v for k, v in esp_extra.items() if k != "message"})
|
|
try:
|
|
self.data["message"].update(esp_extra["message"])
|
|
except KeyError:
|
|
pass
|