Mandrill: drop Djrill compatibility

This commit is contained in:
Mike Edmunds
2023-05-04 13:08:05 -07:00
parent 41754d9813
commit 746cf0e24e
4 changed files with 18 additions and 390 deletions

View File

@@ -50,6 +50,11 @@ Breaking changes
Be sure to update any dependencies specification (pip install, requirements.txt, Be sure to update any dependencies specification (pip install, requirements.txt,
etc.) that had been using ``[amazon_ses]``. etc.) that had been using ``[amazon_ses]``.
* **Mandrill:** Remove support for Mandrill-specific message attributes left over
from Djrill. These attributes have raised DeprecationWarnings since Anymail 0.3
(in 2016), but are now silently ignored. See
`Migrating from Djrill <https://anymail.dev/en/latest/esps/mandrill/#djrill-message-attributes>`__.
* Require Python 3.7 or later. * Require Python 3.7 or later.
* Require urllib3 1.25 or later (released 2019-04-29). * Require urllib3 1.25 or later (released 2019-04-29).

View File

@@ -1,9 +1,8 @@
import warnings
from datetime import datetime from datetime import datetime
from ..exceptions import AnymailRequestsAPIError, AnymailWarning from ..exceptions import AnymailRequestsAPIError
from ..message import ANYMAIL_STATUSES, AnymailRecipientStatus from ..message import ANYMAIL_STATUSES, AnymailRecipientStatus
from ..utils import combine, get_anymail_setting, last from ..utils import get_anymail_setting
from .base_requests import AnymailRequestsBackend, RequestsPayload from .base_requests import AnymailRequestsBackend, RequestsPayload
@@ -60,10 +59,6 @@ class EmailBackend(AnymailRequestsBackend):
return recipient_status return recipient_status
class DjrillDeprecationWarning(AnymailWarning, DeprecationWarning):
"""Warning for features carried over from Djrill that will be removed soon"""
def encode_date_for_mandrill(dt): def encode_date_for_mandrill(dt):
"""Format a datetime for use as a Mandrill API date field """Format a datetime for use as a Mandrill API date field
@@ -107,14 +102,9 @@ class MandrillPayload(RequestsPayload):
} }
def set_from_email(self, email): def set_from_email(self, email):
if getattr(self.message, "use_template_from", False): self.data["message"]["from_email"] = email.addr_spec
self.deprecation_warning( if email.display_name:
"message.use_template_from", "message.from_email = None" self.data["message"]["from_name"] = email.display_name
)
else:
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): def add_recipient(self, recipient_type, email):
assert recipient_type in ["to", "cc", "bcc"] assert recipient_type in ["to", "cc", "bcc"]
@@ -125,12 +115,7 @@ class MandrillPayload(RequestsPayload):
to_list.append(recipient_data) to_list.append(recipient_data)
def set_subject(self, subject): def set_subject(self, subject):
if getattr(self.message, "use_template_subject", False): self.data["message"]["subject"] = subject
self.deprecation_warning(
"message.use_template_subject", "message.subject = None"
)
else:
self.data["message"]["subject"] = subject
def set_reply_to(self, emails): def set_reply_to(self, emails):
if emails: if emails:
@@ -259,109 +244,3 @@ class MandrillPayload(RequestsPayload):
self.data["message"].update(esp_extra["message"]) self.data["message"].update(esp_extra["message"])
except KeyError: except KeyError:
pass pass
# Djrill deprecated message attrs
def deprecation_warning(self, feature, replacement=None):
msg = "Djrill's `%s` will be removed in an upcoming Anymail release." % feature
if replacement:
msg += " Use `%s` instead." % replacement
warnings.warn(msg, DjrillDeprecationWarning)
def deprecated_to_esp_extra(self, attr, in_message_dict=False):
feature = "message.%s" % attr
if in_message_dict:
replacement = "message.esp_extra = {'message': {'%s': <value>}}" % attr
else:
replacement = "message.esp_extra = {'%s': <value>}" % attr
self.deprecation_warning(feature, replacement)
esp_message_attrs = (
("async", last, None),
("ip_pool", last, None),
("from_name", last, None), # overrides display name parsed from from_email
("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, None),
("merge_vars", combine, None),
("recipient_metadata", combine, None),
("template_name", last, None),
("template_content", combine, None),
)
def set_async(self, is_async):
self.deprecated_to_esp_extra("async")
self.esp_extra["async"] = is_async
def set_ip_pool(self, ip_pool):
self.deprecated_to_esp_extra("ip_pool")
self.esp_extra["ip_pool"] = ip_pool
def set_global_merge_vars(self, global_merge_vars):
self.deprecation_warning(
"message.global_merge_vars", "message.merge_global_data"
)
self.set_merge_global_data(global_merge_vars)
def set_merge_vars(self, merge_vars):
self.deprecation_warning("message.merge_vars", "message.merge_data")
self.set_merge_data(merge_vars)
def set_return_path_domain(self, domain):
self.deprecation_warning(
"message.return_path_domain", "message.envelope_sender"
)
self.esp_extra.setdefault("message", {})["return_path_domain"] = domain
def set_template_name(self, template_name):
self.deprecation_warning("message.template_name", "message.template_id")
self.set_template_id(template_name)
def set_template_content(self, template_content):
self.deprecated_to_esp_extra("template_content")
self.esp_extra["template_content"] = template_content
def set_recipient_metadata(self, recipient_metadata):
self.deprecated_to_esp_extra("recipient_metadata", in_message_dict=True)
self.esp_extra.setdefault("message", {})[
"recipient_metadata"
] = recipient_metadata
# 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.deprecated_to_esp_extra(attr, in_message_dict=True)
self.esp_extra.setdefault("message", {})[attr] = value
setter.__name__ = setter_name
return setter
MandrillPayload.define_message_attr_setters()

View File

@@ -383,16 +383,19 @@ Changes to EmailMessage attributes
instead. You'll need to pass a valid email address (not just a domain), instead. You'll need to pass a valid email address (not just a domain),
but Anymail will use only the domain, and will ignore anything before the @. but Anymail will use only the domain, and will ignore anything before the @.
.. _djrill-message-attributes:
**Other Mandrill-specific attributes** **Other Mandrill-specific attributes**
Djrill allowed nearly all Mandrill API parameters to be set Djrill allowed nearly all Mandrill API parameters to be set
as attributes directly on an EmailMessage. With Anymail, you as attributes directly on an EmailMessage. With Anymail, you
should instead set these in the message's should instead set these in the message's
:ref:`esp_extra <mandrill-esp-extra>` dict as described above. :ref:`esp_extra <mandrill-esp-extra>` dict as described above.
Although the Djrill style attributes are still supported (for now), .. versionchanged:: 10.0
Anymail will issue a :exc:`DeprecationWarning` if you try to use them.
These warnings are visible during tests (with Django's default test These Djrill-specific attributes are no longer supported,
runner), and will explain how to update your code. and will be silently ignored. (Earlier versions raised a
:exc:`DeprecationWarning` but still worked.)
You can also use the following git grep expression to find potential You can also use the following git grep expression to find potential
problems: problems:

View File

@@ -1,259 +0,0 @@
from datetime import date
from django.core import mail
from django.test import override_settings, tag
from anymail.exceptions import AnymailSerializationError
from .test_mandrill_backend import MandrillBackendMockAPITestCase
@tag("mandrill")
class MandrillBackendDjrillFeatureTests(MandrillBackendMockAPITestCase):
"""Test backend support for deprecated features leftover from Djrill"""
# These features should now be accessed through esp_extra
def test_async(self):
# async becomes a keyword in Python 3.7. If you have code like this:
# self.message.async = True
# it should be changed to:
# self.message.esp_extra = {"async": True}
# (The setattr below keeps these tests compatible,
# but isn't recommended for your code.)
setattr(self.message, "async", True) # don't do this; use esp_extra instead
with self.assertWarnsRegex(DeprecationWarning, "async"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data["async"], True)
def test_auto_html(self):
self.message.auto_html = True
with self.assertWarnsRegex(DeprecationWarning, "auto_html"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data["message"]["auto_html"], True)
def test_auto_text(self):
self.message.auto_text = True
with self.assertWarnsRegex(DeprecationWarning, "auto_text"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data["message"]["auto_text"], True)
def test_google_analytics_campaign(self):
self.message.google_analytics_campaign = "Email Receipts"
with self.assertWarnsRegex(DeprecationWarning, "google_analytics_campaign"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data["message"]["google_analytics_campaign"], "Email Receipts")
def test_google_analytics_domains(self):
self.message.google_analytics_domains = ["example.com"]
with self.assertWarnsRegex(DeprecationWarning, "google_analytics_domains"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data["message"]["google_analytics_domains"], ["example.com"])
def test_important(self):
self.message.important = True
with self.assertWarnsRegex(DeprecationWarning, "important"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data["message"]["important"], True)
def test_inline_css(self):
self.message.inline_css = True
with self.assertWarnsRegex(DeprecationWarning, "inline_css"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data["message"]["inline_css"], True)
def test_ip_pool(self):
self.message.ip_pool = "Bulk Pool"
with self.assertWarnsRegex(DeprecationWarning, "ip_pool"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data["ip_pool"], "Bulk Pool")
def test_merge_language(self):
self.message.merge_language = "mailchimp"
with self.assertWarnsRegex(DeprecationWarning, "merge_language"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data["message"]["merge_language"], "mailchimp")
def test_preserve_recipients(self):
self.message.preserve_recipients = True
with self.assertWarnsRegex(DeprecationWarning, "preserve_recipients"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data["message"]["preserve_recipients"], True)
def test_recipient_metadata(self):
self.message.recipient_metadata = {
# Anymail expands simple python dicts into the more-verbose
# rcpt/values structures the Mandrill API uses
"customer@example.com": {"cust_id": "67890", "order_id": "54321"},
"guest@example.com": {"cust_id": "94107", "order_id": "43215"},
}
with self.assertWarnsRegex(DeprecationWarning, "recipient_metadata"):
self.message.send()
data = self.get_api_call_json()
self.assertCountEqual(
data["message"]["recipient_metadata"],
[
{
"rcpt": "customer@example.com",
"values": {"cust_id": "67890", "order_id": "54321"},
},
{
"rcpt": "guest@example.com",
"values": {"cust_id": "94107", "order_id": "43215"},
},
],
)
def test_return_path_domain(self):
self.message.return_path_domain = "support.example.com"
with self.assertWarnsRegex(DeprecationWarning, "return_path_domain"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data["message"]["return_path_domain"], "support.example.com")
def test_signing_domain(self):
self.message.signing_domain = "example.com"
with self.assertWarnsRegex(DeprecationWarning, "signing_domain"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data["message"]["signing_domain"], "example.com")
def test_subaccount(self):
self.message.subaccount = "marketing-dept"
with self.assertWarnsRegex(DeprecationWarning, "subaccount"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data["message"]["subaccount"], "marketing-dept")
def test_template_content(self):
self.message.template_content = {
"HEADLINE": "<h1>Specials Just For *|FNAME|*</h1>",
"OFFER_BLOCK": "<p><em>Half off</em> all fruit</p>",
}
with self.assertWarnsRegex(DeprecationWarning, "template_content"):
self.message.send()
data = self.get_api_call_json()
# Anymail expands simple python dicts into the more-verbose name/content
# structures the Mandrill API uses
self.assertCountEqual(
data["template_content"],
[
{"name": "HEADLINE", "content": "<h1>Specials Just For *|FNAME|*</h1>"},
{
"name": "OFFER_BLOCK",
"content": "<p><em>Half off</em> all fruit</p>",
},
],
)
def test_tracking_domain(self):
self.message.tracking_domain = "click.example.com"
with self.assertWarnsRegex(DeprecationWarning, "tracking_domain"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data["message"]["tracking_domain"], "click.example.com")
def test_url_strip_qs(self):
self.message.url_strip_qs = True
with self.assertWarnsRegex(DeprecationWarning, "url_strip_qs"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data["message"]["url_strip_qs"], True)
def test_use_template_from(self):
self.message.template_id = "PERSONALIZED_SPECIALS" # forces send-template api
self.message.use_template_from = True
with self.assertWarnsRegex(DeprecationWarning, "use_template_from"):
self.message.send()
data = self.get_api_call_json()
self.assertNotIn("from_email", data["message"])
self.assertNotIn("from_name", data["message"])
def test_use_template_subject(self):
self.message.template_id = "PERSONALIZED_SPECIALS" # force send-template API
self.message.use_template_subject = True
with self.assertWarnsRegex(DeprecationWarning, "use_template_subject"):
self.message.send()
data = self.get_api_call_json()
self.assertNotIn("subject", data["message"])
def test_view_content_link(self):
self.message.view_content_link = True
with self.assertWarnsRegex(DeprecationWarning, "view_content_link"):
self.message.send()
data = self.get_api_call_json()
self.assertEqual(data["message"]["view_content_link"], True)
def test_default_omits_options(self):
"""Make sure by default we don't send any Mandrill-specific options.
Options not specified by the caller should be omitted entirely from
the Mandrill API call (*not* sent as False or empty). This ensures
that your Mandrill account settings apply by default.
"""
self.message.send()
self.assert_esp_called("/messages/send.json")
data = self.get_api_call_json()
self.assertFalse("auto_html" in data["message"])
self.assertFalse("auto_text" in data["message"])
self.assertFalse("bcc_address" in data["message"])
self.assertFalse("from_name" in data["message"])
self.assertFalse("global_merge_vars" in data["message"])
self.assertFalse("google_analytics_campaign" in data["message"])
self.assertFalse("google_analytics_domains" in data["message"])
self.assertFalse("important" in data["message"])
self.assertFalse("inline_css" in data["message"])
self.assertFalse("merge_language" in data["message"])
self.assertFalse("merge_vars" in data["message"])
self.assertFalse("preserve_recipients" in data["message"])
self.assertFalse("recipient_metadata" in data["message"])
self.assertFalse("return_path_domain" in data["message"])
self.assertFalse("signing_domain" in data["message"])
self.assertFalse("subaccount" in data["message"])
self.assertFalse("tracking_domain" in data["message"])
self.assertFalse("url_strip_qs" in data["message"])
self.assertFalse("view_content_link" in data["message"])
# Options at top level of api params (not in message dict):
self.assertFalse("async" in data)
self.assertFalse("ip_pool" in data)
def test_dates_not_serialized(self):
"""
Old versions of predecessor package Djrill accidentally serialized dates to ISO
"""
self.message.metadata = {"SHIP_DATE": date(2015, 12, 2)}
with self.assertRaises(AnymailSerializationError):
self.message.send()
@override_settings(ANYMAIL_MANDRILL_SEND_DEFAULTS={"subaccount": "test_subaccount"})
def test_subaccount_setting(self):
"""Global, non-esp_extra version of subaccount default"""
with self.assertWarnsRegex(DeprecationWarning, "subaccount"):
mail.send_mail("Subject", "Body", "from@example.com", ["to@example.com"])
data = self.get_api_call_json()
self.assertEqual(data["message"]["subaccount"], "test_subaccount")
@override_settings(
ANYMAIL_MANDRILL_SEND_DEFAULTS={"subaccount": "global_setting_subaccount"}
)
def test_subaccount_message_overrides_setting(self):
"""Global, non-esp_extra version of subaccount default"""
message = mail.EmailMessage(
"Subject", "Body", "from@example.com", ["to@example.com"]
)
# subaccount should override global setting:
message.subaccount = "individual_message_subaccount"
with self.assertWarnsRegex(DeprecationWarning, "subaccount"):
message.send()
data = self.get_api_call_json()
self.assertEqual(data["message"]["subaccount"], "individual_message_subaccount")