diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 815dbf4..92807a1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -50,6 +50,11 @@ Breaking changes Be sure to update any dependencies specification (pip install, requirements.txt, 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 `__. + * Require Python 3.7 or later. * Require urllib3 1.25 or later (released 2019-04-29). diff --git a/anymail/backends/mandrill.py b/anymail/backends/mandrill.py index 6aba287..1816b28 100644 --- a/anymail/backends/mandrill.py +++ b/anymail/backends/mandrill.py @@ -1,9 +1,8 @@ -import warnings from datetime import datetime -from ..exceptions import AnymailRequestsAPIError, AnymailWarning +from ..exceptions import AnymailRequestsAPIError 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 @@ -60,10 +59,6 @@ class EmailBackend(AnymailRequestsBackend): 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): """Format a datetime for use as a Mandrill API date field @@ -107,14 +102,9 @@ class MandrillPayload(RequestsPayload): } def set_from_email(self, email): - if getattr(self.message, "use_template_from", False): - self.deprecation_warning( - "message.use_template_from", "message.from_email = None" - ) - else: - self.data["message"]["from_email"] = email.addr_spec - if email.display_name: - self.data["message"]["from_name"] = email.display_name + 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"] @@ -125,12 +115,7 @@ class MandrillPayload(RequestsPayload): to_list.append(recipient_data) def set_subject(self, subject): - if getattr(self.message, "use_template_subject", False): - self.deprecation_warning( - "message.use_template_subject", "message.subject = None" - ) - else: - self.data["message"]["subject"] = subject + self.data["message"]["subject"] = subject def set_reply_to(self, emails): if emails: @@ -259,109 +244,3 @@ class MandrillPayload(RequestsPayload): self.data["message"].update(esp_extra["message"]) except KeyError: 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': }}" % attr - else: - replacement = "message.esp_extra = {'%s': }" % 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_ functions for any missing esp_message_attrs attrs - # (avoids dozens of simple `self.data["message"][] = 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() diff --git a/docs/esps/mandrill.rst b/docs/esps/mandrill.rst index 63bc0e8..2fb325f 100644 --- a/docs/esps/mandrill.rst +++ b/docs/esps/mandrill.rst @@ -383,16 +383,19 @@ Changes to EmailMessage attributes 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 @. +.. _djrill-message-attributes: + **Other Mandrill-specific attributes** Djrill allowed nearly all Mandrill API parameters to be set as attributes directly on an EmailMessage. With Anymail, you should instead set these in the message's :ref:`esp_extra ` dict as described above. - Although the Djrill style attributes are still supported (for now), - Anymail will issue a :exc:`DeprecationWarning` if you try to use them. - These warnings are visible during tests (with Django's default test - runner), and will explain how to update your code. + .. versionchanged:: 10.0 + + These Djrill-specific attributes are no longer supported, + 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 problems: diff --git a/tests/test_mandrill_djrill_features.py b/tests/test_mandrill_djrill_features.py deleted file mode 100644 index 1dc1e55..0000000 --- a/tests/test_mandrill_djrill_features.py +++ /dev/null @@ -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": "

Specials Just For *|FNAME|*

", - "OFFER_BLOCK": "

Half off all fruit

", - } - 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": "

Specials Just For *|FNAME|*

"}, - { - "name": "OFFER_BLOCK", - "content": "

Half off all fruit

", - }, - ], - ) - - 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")