From 38729df93c2e78147e767b358b422239b3ea61ac Mon Sep 17 00:00:00 2001 From: medmunds Date: Fri, 4 Mar 2016 17:02:43 -0800 Subject: [PATCH] Uniform settings handling 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) --- anymail/backends/base.py | 20 +++++++--- anymail/backends/mandrill.py | 45 +++------------------- anymail/exceptions.py | 5 --- anymail/models.py | 1 + anymail/tests/test_mandrill_send.py | 2 +- anymail/tests/test_mandrill_subaccounts.py | 19 +-------- anymail/utils.py | 42 ++++++++++++++++++++ 7 files changed, 67 insertions(+), 67 deletions(-) create mode 100644 anymail/models.py diff --git a/anymail/backends/base.py b/anymail/backends/base.py index 3d912f7..255c7f1 100644 --- a/anymail/backends/base.py +++ b/anymail/backends/base.py @@ -8,7 +8,7 @@ from django.conf import settings from django.core.mail.backends.base import BaseEmailBackend from ..exceptions import AnymailError, AnymailRequestsAPIError, AnymailSerializationError, AnymailUnsupportedFeature -from ..utils import Attachment, ParsedEmail, UNSET, combine, last +from ..utils import Attachment, ParsedEmail, UNSET, combine, last, get_anymail_setting from .._version import __version__ @@ -19,7 +19,17 @@ class AnymailBaseBackend(BaseEmailBackend): def __init__(self, *args, **kwargs): super(AnymailBaseBackend, self).__init__(*args, **kwargs) - self.send_defaults = getattr(settings, "ANYMAIL_SEND_DEFAULTS", {}) + + self.unsupported_feature_errors = get_anymail_setting("UNSUPPORTED_FEATURE_ERRORS", True) + self.ignore_recipient_status = get_anymail_setting("IGNORE_RECIPIENT_STATUS", False) + + # Merge SEND_DEFAULTS and _SEND_DEFAULTS settings + send_defaults = get_anymail_setting("SEND_DEFAULTS", {}) + esp_send_defaults = get_anymail_setting("%s_SEND_DEFAULTS" % self.esp_name.upper(), None) + if esp_send_defaults is not None: + send_defaults = send_defaults.copy() + send_defaults.update(esp_send_defaults) + self.send_defaults = send_defaults def open(self): """ @@ -296,9 +306,9 @@ class BasePayload(object): setter(value) def unsupported_feature(self, feature): - # future: check settings.ANYMAIL_UNSUPPORTED_FEATURE_ERRORS - raise AnymailUnsupportedFeature("%s does not support %s" % (self.esp_name, feature), - email_message=self.message) + if self.backend.unsupported_feature_errors: + raise AnymailUnsupportedFeature("%s does not support %s" % (self.esp_name, feature), + email_message=self.message, payload=self, backend=self.backend) # # Attribute converters diff --git a/anymail/backends/mandrill.py b/anymail/backends/mandrill.py index 0685cc6..fbaa9e6 100644 --- a/anymail/backends/mandrill.py +++ b/anymail/backends/mandrill.py @@ -1,10 +1,7 @@ from datetime import date, datetime -from django.conf import settings - -from ..exceptions import (AnymailImproperlyConfigured, AnymailRequestsAPIError, - AnymailRecipientsRefused, AnymailUnsupportedFeature) -from ..utils import last, combine +from ..exceptions import AnymailRequestsAPIError, AnymailRecipientsRefused +from ..utils import last, combine, get_anymail_setting from .base import AnymailRequestsBackend, RequestsPayload @@ -16,33 +13,12 @@ class MandrillBackend(AnymailRequestsBackend): def __init__(self, **kwargs): """Init options from Django settings""" - api_url = getattr(settings, "MANDRILL_API_URL", "https://mandrillapp.com/api/1.0") + 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) - try: - self.api_key = settings.MANDRILL_API_KEY - except AttributeError: - raise AnymailImproperlyConfigured("Set MANDRILL_API_KEY in settings.py to use Anymail Mandrill backend") - - # Djrill compat! MANDRILL_SETTINGS - try: - self.send_defaults.update(settings.MANDRILL_SETTINGS) - except AttributeError: - pass # no MANDRILL_SETTINGS setting - except (TypeError, ValueError): # e.g., not enumerable - raise AnymailImproperlyConfigured("MANDRILL_SETTINGS must be a dict or mapping") - - # Djrill compat! MANDRILL_SUBACCOUNT - try: - self.send_defaults["subaccount"] = settings.MANDRILL_SUBACCOUNT - except AttributeError: - pass # no MANDRILL_SUBACCOUNT setting - - self.ignore_recipient_status = getattr(settings, "MANDRILL_IGNORE_RECIPIENT_STATUS", False) - def build_message_payload(self, message): return MandrillPayload(message, self.send_defaults, self) @@ -151,18 +127,9 @@ class MandrillPayload(RequestsPayload): def add_alternative(self, content, mimetype): if mimetype != 'text/html': - raise AnymailUnsupportedFeature( - "Invalid alternative mimetype '%s'. " - "Mandrill only accepts plain text and html emails." - % mimetype, - email_message=self.message) - + self.unsupported_feature("alternative part with mimetype '%s'" % mimetype) if "html" in self.data["message"]: - raise AnymailUnsupportedFeature( - "Too many alternatives attached to the message. " - "Mandrill only accepts plain text and html emails.", - email_message=self.message) - + self.unsupported_feature("multiple html parts") self.data["message"]["html"] = content def add_attachment(self, attachment): diff --git a/anymail/exceptions.py b/anymail/exceptions.py index d206eb4..01e9833 100644 --- a/anymail/exceptions.py +++ b/anymail/exceptions.py @@ -1,4 +1,3 @@ -from django.core.exceptions import ImproperlyConfigured import json from requests import HTTPError @@ -68,10 +67,6 @@ class AnymailError(Exception): return description -class AnymailImproperlyConfigured(AnymailError, ImproperlyConfigured): - """Exception for configuration problems""" - - class AnymailAPIError(AnymailError): """Exception for unsuccessful response from ESP's API.""" diff --git a/anymail/models.py b/anymail/models.py new file mode 100644 index 0000000..c8f74d2 --- /dev/null +++ b/anymail/models.py @@ -0,0 +1 @@ +# this empty models.py is here for Django testrunner compatibility pre Django 1.6 diff --git a/anymail/tests/test_mandrill_send.py b/anymail/tests/test_mandrill_send.py index f2d7b2a..6c53dba 100644 --- a/anymail/tests/test_mandrill_send.py +++ b/anymail/tests/test_mandrill_send.py @@ -603,7 +603,7 @@ class DjrillRecipientsRefusedTests(DjrillBackendMockAPITestCase): sent = msg.send() self.assertEqual(sent, 1) # one message sent, successfully, to 2 of 4 recipients - @override_settings(MANDRILL_IGNORE_RECIPIENT_STATUS=True) + @override_settings(ANYMAIL_IGNORE_RECIPIENT_STATUS=True) def test_settings_override(self): """Setting restores Djrill 1.x behavior""" self.mock_post.return_value = self.MockResponse(status_code=200, raw=b""" diff --git a/anymail/tests/test_mandrill_subaccounts.py b/anymail/tests/test_mandrill_subaccounts.py index b6e7430..2f1880d 100644 --- a/anymail/tests/test_mandrill_subaccounts.py +++ b/anymail/tests/test_mandrill_subaccounts.py @@ -12,31 +12,16 @@ class DjrillMandrillSubaccountTests(DjrillBackendMockAPITestCase): data = self.get_api_call_data() self.assertFalse('subaccount' in data['message']) - @override_settings(MANDRILL_SETTINGS={'subaccount': 'test_subaccount'}) + @override_settings(ANYMAIL_MANDRILL_SEND_DEFAULTS={'subaccount': 'test_subaccount'}) def test_subaccount_setting(self): mail.send_mail('Subject', 'Body', 'from@example.com', ['to@example.com']) data = self.get_api_call_data() self.assertEqual(data['message']['subaccount'], "test_subaccount") - @override_settings(MANDRILL_SETTINGS={'subaccount': 'global_setting_subaccount'}) + @override_settings(ANYMAIL_MANDRILL_SEND_DEFAULTS={'subaccount': 'global_setting_subaccount'}) def test_subaccount_message_overrides_setting(self): message = mail.EmailMessage('Subject', 'Body', 'from@example.com', ['to@example.com']) message.subaccount = "individual_message_subaccount" # should override global setting message.send() data = self.get_api_call_data() self.assertEqual(data['message']['subaccount'], "individual_message_subaccount") - - # Djrill 1.x offered dedicated MANDRILL_SUBACCOUNT setting. - # In Djrill 2.x, you should use the MANDRILL_SETTINGS dict as in the earlier tests. - # But we still support the old setting for compatibility: - @override_settings(MANDRILL_SUBACCOUNT="legacy_setting_subaccount") - def test_subaccount_legacy_setting(self): - mail.send_mail('Subject', 'Body', 'from@example.com', ['to@example.com']) - data = self.get_api_call_data() - self.assertEqual(data['message']['subaccount'], "legacy_setting_subaccount") - - message = mail.EmailMessage('Subject', 'Body', 'from@example.com', ['to@example.com']) - message.subaccount = "individual_message_subaccount" # should override legacy setting - message.send() - data = self.get_api_call_data() - self.assertEqual(data['message']['subaccount'], "individual_message_subaccount") diff --git a/anymail/utils.py b/anymail/utils.py index 317adbc..0f92d45 100644 --- a/anymail/utils.py +++ b/anymail/utils.py @@ -4,6 +4,8 @@ from email.mime.base import MIMEBase from email.utils import parseaddr import six +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured from django.core.mail.message import sanitize_address, DEFAULT_ATTACHMENT_MIME_TYPE @@ -133,3 +135,43 @@ class Attachment(object): if isinstance(content, six.text_type): content = content.encode(self.encoding) return b64encode(content).decode("ascii") + + +def get_anymail_setting(setting, default=UNSET, allow_bare=False): + """Returns a Django Anymail setting. + + Returns first of: + - settings.ANYMAIL[setting] + - settings.ANYMAIL_ + - settings. (only if allow_bare) + - default if provided; else raises ImproperlyConfigured + + ANYMAIL = { "MAILGUN_SEND_DEFAULTS" : { ... }, ... } + ANYMAIL_MAILGUN_SEND_DEFAULTS = { ... } + + If allow_bare, allows settings. without the ANYMAIL_ prefix: + ANYMAIL = { "MAILGUN_API_KEY": "xyz", ... } + ANYMAIL_MAILGUN_API_KEY = "xyz" + MAILGUN_API_KEY = "xyz" + """ + + anymail_setting = "ANYMAIL_%s" % setting + try: + return settings.ANYMAIL[setting] + except (AttributeError, KeyError): + try: + return getattr(settings, anymail_setting) + except AttributeError: + if allow_bare: + try: + return getattr(settings, setting) + except AttributeError: + pass + if default is UNSET: + message = "You must set %s or ANYMAIL = {'%s': ...}" % (anymail_setting, setting) + if allow_bare: + message += " or %s" % setting + message += " in your Django settings" + raise ImproperlyConfigured(message) + else: + return default