Files
django-anymail/anymail/backends/test.py
Rodrigo Nicolas Carreras 33f680686b Add merge_headers option for Amazon SES
Add new `merge_headers` message option 
for per-recipient headers with template sends. 

* Support in base backend
* Implement in Amazon SES backend
  (Requires boto3 >= 1.34.98.)

---------

Co-authored-by: Mike Edmunds <medmunds@gmail.com>
2024-05-21 11:55:29 -07:00

162 lines
5.6 KiB
Python

from django.core import mail
from ..exceptions import AnymailAPIError
from ..message import AnymailRecipientStatus
from .base import AnymailBaseBackend, BasePayload
class EmailBackend(AnymailBaseBackend):
"""
Anymail backend that simulates sending messages, useful for testing.
Sent messages are collected in django.core.mail.outbox
(as with Django's locmem backend).
In addition:
* Anymail send params parsed from the message will be attached
to the outbox message as a dict in the attr `anymail_test_params`
* If the caller supplies an `anymail_test_response` attr on the message,
that will be used instead of the default "sent" response. It can be either
an AnymailRecipientStatus or an instance of AnymailAPIError (or a subclass)
to raise an exception.
"""
esp_name = "Test"
def __init__(self, *args, **kwargs):
# Allow replacing the payload, for testing.
# (Real backends would generally not implement this option.)
self._payload_class = kwargs.pop("payload_class", TestPayload)
super().__init__(*args, **kwargs)
if not hasattr(mail, "outbox"):
mail.outbox = [] # see django.core.mail.backends.locmem
def get_esp_message_id(self, message):
# Get a unique ID for the message. The message must have been added to
# the outbox first.
return mail.outbox.index(message)
def build_message_payload(self, message, defaults):
return self._payload_class(backend=self, message=message, defaults=defaults)
def post_to_esp(self, payload, message):
# Keep track of the sent messages and params (for test cases)
message.anymail_test_params = payload.get_params()
mail.outbox.append(message)
try:
# Tests can supply their own message.test_response:
response = message.anymail_test_response
if isinstance(response, AnymailAPIError):
raise response
except AttributeError:
# Default is to return 'sent' for each recipient
status = AnymailRecipientStatus(
message_id=self.get_esp_message_id(message), status="sent"
)
response = {
"recipient_status": {
email: status for email in payload.recipient_emails
}
}
return response
def parse_recipient_status(self, response, payload, message):
try:
return response["recipient_status"]
except KeyError as err:
raise AnymailAPIError("Unparsable test response") from err
class TestPayload(BasePayload):
# For test purposes, just keep a dict of the params we've received.
# (This approach is also useful for native API backends -- think of
# payload.params as collecting kwargs for esp_native_api.send().)
def init_payload(self):
self.params = {}
self.recipient_emails = []
def get_params(self):
# Test backend callers can check message.anymail_test_params['is_batch_send']
# to verify whether Anymail thought the message should use batch send logic.
self.params["is_batch_send"] = self.is_batch()
return self.params
def set_from_email(self, email):
self.params["from"] = email
def set_envelope_sender(self, email):
self.params["envelope_sender"] = email.addr_spec
def set_to(self, emails):
self.params["to"] = emails
self.recipient_emails += [email.addr_spec for email in emails]
def set_cc(self, emails):
self.params["cc"] = emails
self.recipient_emails += [email.addr_spec for email in emails]
def set_bcc(self, emails):
self.params["bcc"] = emails
self.recipient_emails += [email.addr_spec for email in emails]
def set_subject(self, subject):
self.params["subject"] = subject
def set_reply_to(self, emails):
self.params["reply_to"] = emails
def set_extra_headers(self, headers):
self.params["extra_headers"] = headers
def set_text_body(self, body):
self.params["text_body"] = body
def set_html_body(self, body):
self.params["html_body"] = body
def add_alternative(self, content, mimetype):
# For testing purposes, we allow all "text/*" alternatives,
# but not any other mimetypes.
if mimetype.startswith("text"):
self.params.setdefault("alternatives", []).append((content, mimetype))
else:
self.unsupported_feature("alternative part with type '%s'" % mimetype)
def add_attachment(self, attachment):
self.params.setdefault("attachments", []).append(attachment)
def set_metadata(self, metadata):
self.params["metadata"] = metadata
def set_send_at(self, send_at):
self.params["send_at"] = send_at
def set_tags(self, tags):
self.params["tags"] = tags
def set_track_clicks(self, track_clicks):
self.params["track_clicks"] = track_clicks
def set_track_opens(self, track_opens):
self.params["track_opens"] = track_opens
def set_template_id(self, template_id):
self.params["template_id"] = template_id
def set_merge_data(self, merge_data):
self.params["merge_data"] = merge_data
def set_merge_headers(self, merge_headers):
self.params["merge_headers"] = merge_headers
def set_merge_metadata(self, merge_metadata):
self.params["merge_metadata"] = merge_metadata
def set_merge_global_data(self, merge_global_data):
self.params["merge_global_data"] = merge_global_data
def set_esp_extra(self, extra):
# Merge extra into params
self.params.update(extra)