mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 11:51:05 -05:00
Allow kwargs overrides for (nearly) all settings
* Update utils.get_anymail_setting to support kwargs override of django.conf.settings values * Use the updated version everywhere * Switch from ImproperlyConfigured to AnymailConfigurationError exception (anticipates feature_wehooks change) Closes #12
This commit is contained in:
@@ -17,12 +17,15 @@ class AnymailBaseBackend(BaseEmailBackend):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(AnymailBaseBackend, self).__init__(*args, **kwargs)
|
super(AnymailBaseBackend, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.ignore_unsupported_features = get_anymail_setting("IGNORE_UNSUPPORTED_FEATURES", False)
|
self.ignore_unsupported_features = get_anymail_setting('ignore_unsupported_features',
|
||||||
self.ignore_recipient_status = get_anymail_setting("IGNORE_RECIPIENT_STATUS", False)
|
kwargs=kwargs, default=False)
|
||||||
|
self.ignore_recipient_status = get_anymail_setting('ignore_recipient_status',
|
||||||
|
kwargs=kwargs, default=False)
|
||||||
|
|
||||||
# Merge SEND_DEFAULTS and <esp_name>_SEND_DEFAULTS settings
|
# Merge SEND_DEFAULTS and <esp_name>_SEND_DEFAULTS settings
|
||||||
send_defaults = get_anymail_setting("SEND_DEFAULTS", {})
|
send_defaults = get_anymail_setting('send_defaults', default={}) # but not from kwargs
|
||||||
esp_send_defaults = get_anymail_setting("%s_SEND_DEFAULTS" % self.esp_name.upper(), None)
|
esp_send_defaults = get_anymail_setting('send_defaults', esp_name=self.esp_name,
|
||||||
|
kwargs=kwargs, default=None)
|
||||||
if esp_send_defaults is not None:
|
if esp_send_defaults is not None:
|
||||||
send_defaults = send_defaults.copy()
|
send_defaults = send_defaults.copy()
|
||||||
send_defaults.update(esp_send_defaults)
|
send_defaults.update(esp_send_defaults)
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ class MailgunBackend(AnymailRequestsBackend):
|
|||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
"""Init options from Django settings"""
|
"""Init options from Django settings"""
|
||||||
self.api_key = get_anymail_setting('MAILGUN_API_KEY', allow_bare=True)
|
esp_name = self.esp_name
|
||||||
api_url = get_anymail_setting("MAILGUN_API_URL", "https://api.mailgun.net/v3")
|
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://api.mailgun.net/v3")
|
||||||
if not api_url.endswith("/"):
|
if not api_url.endswith("/"):
|
||||||
api_url += "/"
|
api_url += "/"
|
||||||
super(MailgunBackend, self).__init__(api_url, **kwargs)
|
super(MailgunBackend, self).__init__(api_url, **kwargs)
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ class MandrillBackend(AnymailRequestsBackend):
|
|||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
"""Init options from Django settings"""
|
"""Init options from Django settings"""
|
||||||
self.api_key = get_anymail_setting('MANDRILL_API_KEY', allow_bare=True)
|
esp_name = self.esp_name
|
||||||
api_url = get_anymail_setting("MANDRILL_API_URL", "https://mandrillapp.com/api/1.0")
|
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("/"):
|
if not api_url.endswith("/"):
|
||||||
api_url += "/"
|
api_url += "/"
|
||||||
super(MandrillBackend, self).__init__(api_url, **kwargs)
|
super(MandrillBackend, self).__init__(api_url, **kwargs)
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ class PostmarkBackend(AnymailRequestsBackend):
|
|||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
"""Init options from Django settings"""
|
"""Init options from Django settings"""
|
||||||
self.server_token = get_anymail_setting('POSTMARK_SERVER_TOKEN', allow_bare=True)
|
esp_name = self.esp_name
|
||||||
api_url = get_anymail_setting("POSTMARK_API_URL", "https://api.postmarkapp.com/")
|
self.server_token = get_anymail_setting('server_token', esp_name=esp_name, kwargs=kwargs, allow_bare=True)
|
||||||
|
api_url = get_anymail_setting('api_url', esp_name=esp_name, kwargs=kwargs,
|
||||||
|
default="https://api.postmarkapp.com/")
|
||||||
if not api_url.endswith("/"):
|
if not api_url.endswith("/"):
|
||||||
api_url += "/"
|
api_url += "/"
|
||||||
super(PostmarkBackend, self).__init__(api_url, **kwargs)
|
super(PostmarkBackend, self).__init__(api_url, **kwargs)
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
from email.utils import unquote
|
from email.utils import unquote
|
||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
|
||||||
from django.core.mail import make_msgid
|
from django.core.mail import make_msgid
|
||||||
from requests.structures import CaseInsensitiveDict
|
from requests.structures import CaseInsensitiveDict
|
||||||
|
|
||||||
from ..exceptions import AnymailRequestsAPIError
|
from ..exceptions import AnymailConfigurationError, AnymailRequestsAPIError
|
||||||
from ..message import AnymailRecipientStatus
|
from ..message import AnymailRecipientStatus
|
||||||
from ..utils import get_anymail_setting, timestamp
|
from ..utils import get_anymail_setting, timestamp
|
||||||
|
|
||||||
@@ -19,19 +18,25 @@ class SendGridBackend(AnymailRequestsBackend):
|
|||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
"""Init options from Django settings"""
|
"""Init options from Django settings"""
|
||||||
# Auth requires *either* SENDGRID_API_KEY or SENDGRID_USERNAME+SENDGRID_PASSWORD
|
# Auth requires *either* SENDGRID_API_KEY or SENDGRID_USERNAME+SENDGRID_PASSWORD
|
||||||
self.api_key = get_anymail_setting('SENDGRID_API_KEY', default=None, allow_bare=True)
|
esp_name = self.esp_name
|
||||||
self.username = get_anymail_setting('SENDGRID_USERNAME', default=None, allow_bare=True)
|
self.api_key = get_anymail_setting('api_key', esp_name=esp_name, kwargs=kwargs,
|
||||||
self.password = get_anymail_setting('SENDGRID_PASSWORD', default=None, allow_bare=True)
|
default=None, allow_bare=True)
|
||||||
if self.api_key is None and self.username is None and self.password is None:
|
self.username = get_anymail_setting('username', esp_name=esp_name, kwargs=kwargs,
|
||||||
raise ImproperlyConfigured(
|
default=None, allow_bare=True)
|
||||||
|
self.password = get_anymail_setting('password', esp_name=esp_name, kwargs=kwargs,
|
||||||
|
default=None, allow_bare=True)
|
||||||
|
if self.api_key is None and (self.username is None or self.password is None):
|
||||||
|
raise AnymailConfigurationError(
|
||||||
"You must set either SENDGRID_API_KEY or both SENDGRID_USERNAME and "
|
"You must set either SENDGRID_API_KEY or both SENDGRID_USERNAME and "
|
||||||
"SENDGRID_PASSWORD in your Django ANYMAIL settings."
|
"SENDGRID_PASSWORD in your Django ANYMAIL settings."
|
||||||
)
|
)
|
||||||
|
|
||||||
self.generate_message_id = get_anymail_setting('SENDGRID_GENERATE_MESSAGE_ID', default=True)
|
self.generate_message_id = get_anymail_setting('generate_message_id', esp_name=esp_name,
|
||||||
|
kwargs=kwargs, default=True)
|
||||||
|
|
||||||
# This is SendGrid's Web API v2 (because the Web API v3 doesn't support sending)
|
# This is SendGrid's Web API v2 (because the Web API v3 doesn't support sending)
|
||||||
api_url = get_anymail_setting("SENDGRID_API_URL", "https://api.sendgrid.com/api/")
|
api_url = get_anymail_setting('api_url', esp_name=esp_name, kwargs=kwargs,
|
||||||
|
default="https://api.sendgrid.com/api/")
|
||||||
if not api_url.endswith("/"):
|
if not api_url.endswith("/"):
|
||||||
api_url += "/"
|
api_url += "/"
|
||||||
super(SendGridBackend, self).__init__(api_url, **kwargs)
|
super(SendGridBackend, self).__init__(api_url, **kwargs)
|
||||||
|
|||||||
@@ -125,8 +125,15 @@ class AnymailSerializationError(AnymailError, TypeError):
|
|||||||
super(AnymailSerializationError, self).__init__(message, *args, **kwargs)
|
super(AnymailSerializationError, self).__init__(message, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
# This deliberately doesn't inherit from AnymailError
|
class AnymailConfigurationError(ImproperlyConfigured):
|
||||||
class AnymailImproperlyInstalled(ImproperlyConfigured, ImportError):
|
"""Exception for Anymail configuration or installation issues"""
|
||||||
|
# This deliberately doesn't inherit from AnymailError,
|
||||||
|
# because we don't want it to be swallowed by backend fail_silently
|
||||||
|
|
||||||
|
|
||||||
|
class AnymailImproperlyInstalled(AnymailConfigurationError, ImportError):
|
||||||
|
"""Exception for Anymail missing package dependencies"""
|
||||||
|
|
||||||
def __init__(self, missing_package, backend="<backend>"):
|
def __init__(self, missing_package, backend="<backend>"):
|
||||||
message = "The %s package is required to use this backend, but isn't installed.\n" \
|
message = "The %s package is required to use this backend, but isn't installed.\n" \
|
||||||
"(Be sure to use `pip install django-anymail[%s]` " \
|
"(Be sure to use `pip install django-anymail[%s]` " \
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ from time import mktime
|
|||||||
|
|
||||||
import six
|
import six
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
|
||||||
from django.core.mail.message import sanitize_address, DEFAULT_ATTACHMENT_MIME_TYPE
|
from django.core.mail.message import sanitize_address, DEFAULT_ATTACHMENT_MIME_TYPE
|
||||||
from django.utils.timezone import utc
|
from django.utils.timezone import utc
|
||||||
|
|
||||||
|
from .exceptions import AnymailConfigurationError
|
||||||
|
|
||||||
UNSET = object() # Used as non-None default value
|
UNSET = object() # Used as non-None default value
|
||||||
|
|
||||||
@@ -158,25 +158,42 @@ def get_content_disposition(mimeobj):
|
|||||||
return str(value).partition(';')[0].strip().lower()
|
return str(value).partition(';')[0].strip().lower()
|
||||||
|
|
||||||
|
|
||||||
def get_anymail_setting(setting, default=UNSET, allow_bare=False):
|
def get_anymail_setting(name, default=UNSET, esp_name=None, kwargs=None, allow_bare=False):
|
||||||
"""Returns a Django Anymail setting.
|
"""Returns an Anymail option from kwargs or Django settings.
|
||||||
|
|
||||||
Returns first of:
|
Returns first of:
|
||||||
- settings.ANYMAIL[setting]
|
- kwargs[name] -- e.g., kwargs['api_key'] -- and name key will be popped from kwargs
|
||||||
- settings.ANYMAIL_<setting>
|
- settings.ANYMAIL['<ESP_NAME>_<NAME>'] -- e.g., settings.ANYMAIL['MAILGUN_API_KEY']
|
||||||
- settings.<setting> (only if allow_bare)
|
- settings.ANYMAIL_<ESP_NAME>_<NAME> -- e.g., settings.ANYMAIL_MAILGUN_API_KEY
|
||||||
- default if provided; else raises ImproperlyConfigured
|
- settings.<ESP_NAME>_<NAME> (only if allow_bare) -- e.g., settings.MAILGUN_API_KEY
|
||||||
|
- default if provided; else raises AnymailConfigurationError
|
||||||
|
|
||||||
ANYMAIL = { "MAILGUN_SEND_DEFAULTS" : { ... }, ... }
|
If allow_bare, allows settings.<ESP_NAME>_<NAME> without the ANYMAIL_ prefix:
|
||||||
ANYMAIL_MAILGUN_SEND_DEFAULTS = { ... }
|
|
||||||
|
|
||||||
If allow_bare, allows settings.<setting> without the ANYMAIL_ prefix:
|
|
||||||
ANYMAIL = { "MAILGUN_API_KEY": "xyz", ... }
|
ANYMAIL = { "MAILGUN_API_KEY": "xyz", ... }
|
||||||
ANYMAIL_MAILGUN_API_KEY = "xyz"
|
ANYMAIL_MAILGUN_API_KEY = "xyz"
|
||||||
MAILGUN_API_KEY = "xyz"
|
MAILGUN_API_KEY = "xyz"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
value = kwargs.pop(name)
|
||||||
|
if name in ['username', 'password']:
|
||||||
|
# Work around a problem in django.core.mail.send_mail, which calls
|
||||||
|
# get_connection(... username=None, password=None) by default.
|
||||||
|
# We need to ignore those None defaults (else settings like
|
||||||
|
# 'SENDGRID_USERNAME' get unintentionally overridden from kwargs).
|
||||||
|
if value is not None:
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if esp_name is not None:
|
||||||
|
setting = "{}_{}".format(esp_name.upper(), name.upper())
|
||||||
|
else:
|
||||||
|
setting = name.upper()
|
||||||
anymail_setting = "ANYMAIL_%s" % setting
|
anymail_setting = "ANYMAIL_%s" % setting
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return settings.ANYMAIL[setting]
|
return settings.ANYMAIL[setting]
|
||||||
except (AttributeError, KeyError):
|
except (AttributeError, KeyError):
|
||||||
@@ -193,7 +210,7 @@ def get_anymail_setting(setting, default=UNSET, allow_bare=False):
|
|||||||
if allow_bare:
|
if allow_bare:
|
||||||
message += " or %s" % setting
|
message += " or %s" % setting
|
||||||
message += " in your Django settings"
|
message += " in your Django settings"
|
||||||
raise ImproperlyConfigured(message)
|
raise AnymailConfigurationError(message)
|
||||||
else:
|
else:
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
|||||||
@@ -122,6 +122,14 @@ if you are using other Django apps that work with the same ESP.)
|
|||||||
# nor ANYMAIL_MAILGUN_API_KEY have been set
|
# nor ANYMAIL_MAILGUN_API_KEY have been set
|
||||||
|
|
||||||
|
|
||||||
|
Finally, for complex use cases, you can override most settings on a per-instance
|
||||||
|
basis by providing keyword args where the instance is initialized (e.g., in a
|
||||||
|
:func:`~django.core.mail.get_connection` call to create an email backend instance,
|
||||||
|
or in `View.as_view()` call to set up webhooks in a custom urls.py). To get the kwargs
|
||||||
|
parameter for a setting, drop "ANYMAIL" and the ESP name, and lowercase the rest:
|
||||||
|
e.g., you can override ANYMAIL_MAILGUN_API_KEY by passing `api_key="abc"` to
|
||||||
|
:func:`~django.core.mail.get_connection`. See :ref:`multiple-backends` for an example.
|
||||||
|
|
||||||
There are specific Anymail settings for each ESP (like API keys and urls).
|
There are specific Anymail settings for each ESP (like API keys and urls).
|
||||||
See the :ref:`supported ESPs <supported-esps>` section for details.
|
See the :ref:`supported ESPs <supported-esps>` section for details.
|
||||||
Here are the other settings Anymail supports:
|
Here are the other settings Anymail supports:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ This could be useful, for example, to deliver customer emails with the ESP,
|
|||||||
but send admin emails directly through an SMTP server:
|
but send admin emails directly through an SMTP server:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:emphasize-lines: 8,10,13,15
|
:emphasize-lines: 8,10,13,15,19-20,22
|
||||||
|
|
||||||
from django.core.mail import send_mail, get_connection
|
from django.core.mail import send_mail, get_connection
|
||||||
|
|
||||||
@@ -28,9 +28,17 @@ but send admin emails directly through an SMTP server:
|
|||||||
|
|
||||||
# You can even use multiple Anymail backends in the same app:
|
# You can even use multiple Anymail backends in the same app:
|
||||||
sendgrid_backend = get_connection('anymail.backends.sendgrid.SendGridBackend')
|
sendgrid_backend = get_connection('anymail.backends.sendgrid.SendGridBackend')
|
||||||
send_mail("Password reset", "Here you go", "user@example.com", ["noreply@example.com"],
|
send_mail("Password reset", "Here you go", "noreply@example.com", ["user@example.com"],
|
||||||
connection=sendgrid_backend)
|
connection=sendgrid_backend)
|
||||||
|
|
||||||
|
# You can override settings.py settings with kwargs to get_connection.
|
||||||
|
# This example supplies credentials to use a SendGrid subuser acccount:
|
||||||
|
alt_sendgrid_backend = get_connection('anymail.backends.sendgrid.SendGridBackend',
|
||||||
|
username='marketing_subuser', password='abc123')
|
||||||
|
send_mail("Here's that info", "you wanted", "marketing@example.com", ["prospect@example.com"],
|
||||||
|
connection=alt_sendgrid_backend)
|
||||||
|
|
||||||
|
|
||||||
You can supply a different connection to Django's
|
You can supply a different connection to Django's
|
||||||
:func:`~django.core.mail.send_mail` and :func:`~django.core.mail.send_mass_mail` helpers,
|
:func:`~django.core.mail.send_mail` and :func:`~django.core.mail.send_mass_mail` helpers,
|
||||||
and in the constructor for an
|
and in the constructor for an
|
||||||
@@ -39,6 +47,3 @@ and in the constructor for an
|
|||||||
|
|
||||||
(See the :class:`django.utils.log.AdminEmailHandler` docs for more information
|
(See the :class:`django.utils.log.AdminEmailHandler` docs for more information
|
||||||
on Django's admin error logging.)
|
on Django's admin error logging.)
|
||||||
|
|
||||||
.. _django.utils.log.AdminEmailHandler:
|
|
||||||
https://docs.djangoproject.com/en/stable/topics/logging/#django.utils.log.AdminEmailHandler
|
|
||||||
|
|||||||
35
tests/test_general_backend.py
Normal file
35
tests/test_general_backend.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from django.core.mail import get_connection
|
||||||
|
from django.test import SimpleTestCase
|
||||||
|
from django.test.utils import override_settings
|
||||||
|
|
||||||
|
from .utils import AnymailTestMixin
|
||||||
|
|
||||||
|
|
||||||
|
class BackendSettingsTests(SimpleTestCase, AnymailTestMixin):
|
||||||
|
"""Test settings initializations for Anymail EmailBackends"""
|
||||||
|
|
||||||
|
# We should add a "GenericBackend" or something basic for testing.
|
||||||
|
# For now, we just access real backends directly.
|
||||||
|
|
||||||
|
@override_settings(ANYMAIL={'MAILGUN_API_KEY': 'api_key_from_settings'})
|
||||||
|
def test_connection_kwargs_overrides_settings(self):
|
||||||
|
connection = get_connection('anymail.backends.mailgun.MailgunBackend')
|
||||||
|
self.assertEqual(connection.api_key, 'api_key_from_settings')
|
||||||
|
|
||||||
|
connection = get_connection('anymail.backends.mailgun.MailgunBackend',
|
||||||
|
api_key='api_key_from_kwargs')
|
||||||
|
self.assertEqual(connection.api_key, 'api_key_from_kwargs')
|
||||||
|
|
||||||
|
@override_settings(ANYMAIL={'SENDGRID_USERNAME': 'username_from_settings',
|
||||||
|
'SENDGRID_PASSWORD': 'password_from_settings'})
|
||||||
|
def test_username_password_kwargs_overrides(self):
|
||||||
|
# Additional checks for username and password, which are special-cased
|
||||||
|
# because of Django core mail function defaults.
|
||||||
|
connection = get_connection('anymail.backends.sendgrid.SendGridBackend')
|
||||||
|
self.assertEqual(connection.username, 'username_from_settings')
|
||||||
|
self.assertEqual(connection.password, 'password_from_settings')
|
||||||
|
|
||||||
|
connection = get_connection('anymail.backends.sendgrid.SendGridBackend',
|
||||||
|
username='username_from_kwargs', password='password_from_kwargs')
|
||||||
|
self.assertEqual(connection.username, 'username_from_kwargs')
|
||||||
|
self.assertEqual(connection.password, 'password_from_kwargs')
|
||||||
Reference in New Issue
Block a user