From 20c63501406d7093683f1269a985914177f8cab7 Mon Sep 17 00:00:00 2001 From: medmunds Date: Wed, 9 Mar 2016 18:37:11 -0800 Subject: [PATCH] Update (almost) all the docs --- README.rst | 102 +++-- docs/conf.py | 26 +- docs/contributing.rst | 70 ++-- docs/esps/index.rst | 23 ++ docs/esps/mailgun.rst | 103 +++++ docs/esps/mandrill.rst | 151 +++++++ docs/esps/postmark.rst | 17 + docs/esps/sendgrid.rst | 17 + docs/history.rst | 146 ------- docs/inbound.rst | 18 + docs/index.rst | 32 +- docs/installation.rst | 210 ++++++---- docs/quickstart.rst | 17 +- docs/release_notes.rst | 23 ++ docs/sending/anymail_additions.rst | 449 +++++++++++++++++++++ docs/sending/django_email.rst | 162 ++++++++ docs/sending/exceptions.rst | 50 +++ docs/sending/index.rst | 13 + docs/sending/templates.rst | 146 +++++++ docs/sending/tracking.rst | 178 ++++++++ docs/tips/django_templates.rst | 55 +++ docs/tips/index.rst | 16 + docs/{usage => tips}/multiple_backends.rst | 22 +- docs/troubleshooting.rst | 76 ++-- docs/upgrading.rst | 121 ------ docs/usage/sending_mail.rst | 387 ------------------ docs/usage/templates.rst | 131 ------ docs/usage/webhooks.rst | 172 -------- 28 files changed, 1741 insertions(+), 1192 deletions(-) create mode 100644 docs/esps/index.rst create mode 100644 docs/esps/mailgun.rst create mode 100644 docs/esps/mandrill.rst create mode 100644 docs/esps/postmark.rst create mode 100644 docs/esps/sendgrid.rst delete mode 100644 docs/history.rst create mode 100644 docs/inbound.rst create mode 100644 docs/release_notes.rst create mode 100644 docs/sending/anymail_additions.rst create mode 100644 docs/sending/django_email.rst create mode 100644 docs/sending/exceptions.rst create mode 100644 docs/sending/index.rst create mode 100644 docs/sending/templates.rst create mode 100644 docs/sending/tracking.rst create mode 100644 docs/tips/django_templates.rst create mode 100644 docs/tips/index.rst rename docs/{usage => tips}/multiple_backends.rst (63%) delete mode 100644 docs/upgrading.rst delete mode 100644 docs/usage/sending_mail.rst delete mode 100644 docs/usage/templates.rst delete mode 100644 docs/usage/webhooks.rst diff --git a/README.rst b/README.rst index d9a9dac..adbe882 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ -Djrill: Mandrill Transactional Email for Django -=============================================== +Anymail: Multi-ESP transactional email for Django +================================================= .. This README is reused in multiple places: * Github: project page, exactly as it appears here @@ -17,53 +17,63 @@ Djrill: Mandrill Transactional Email for Django .. This shared-intro section is also included in docs/index.rst -Djrill integrates the `Mandrill `_ transactional -email service into Django. +Anymail integrates several transactional email service providers (ESPs) into Django, +using a consistent API that makes it (relatively) easy to switch between ESPs. -**UPGRADING FROM DJRILL 1.x?** -There are some **breaking changes in Djrill 2.0**. Please see the -`upgrade instructions `_. +It currently supports Mailgun and Mandrill. Postmark and SendGrid are coming soon. + +.. attention:: **EARLY DEVELOPMENT** + + This project is undergoing rapid development to get to a 1.0 release. + You should expect frequent, possibly-breaking changes until 1.0 alpha. + + If you are migrating to Anymail from `Djrill `_, + there are `notes on porting `_ -In general, Djrill "just works" with Django's built-in `django.core.mail` -package. It includes: +Anymail normalizes ESP functionality so it "just works" with Django's +built-in `django.core.mail` package. It includes: * Support for HTML, attachments, extra headers, and other features of `Django's built-in email `_ -* Mandrill-specific extensions like tags, metadata, tracking, and MailChimp templates -* Optional support for Mandrill inbound email and other webhook notifications, - via Django signals +* Extensions that make it easy to use extra ESP functionality, like tags, metadata, + and tracking, using code that's portable between ESPs +* Optional support for ESP delivery status notification via webhooks and Django signals +* Optional support for inbound email -Djrill is released under the BSD license. It is tested against Django 1.4--1.9 -(including Python 3 with Django 1.6+, and PyPy support with Django 1.5+). +Anymail is released under the BSD license. It is tested against Django 1.8--1.9 +(including Python 3 and PyPy). Djrill uses `semantic versioning `_. .. END shared-intro -.. image:: https://travis-ci.org/brack3t/Djrill.png?branch=master - :target: https://travis-ci.org/brack3t/Djrill +.. image:: https://travis-ci.org/anymail/django-anymail.png?branch=master + :target: https://travis-ci.org/anymail/django-anymail :alt: build status on Travis-CI **Resources** -* Full documentation: https://djrill.readthedocs.org/en/latest/ -* Package on PyPI: https://pypi.python.org/pypi/djrill -* Project on Github: https://github.com/brack3t/Djrill +* Full documentation: https://anymail.readthedocs.org/en/latest/ +* Package on PyPI: https://pypi.python.org/pypi/django-anymail +* Project on Github: https://github.com/anymail/django-anymail -Djrill 1-2-3 ------------- +Anymail 1-2-3 +------------- .. _quickstart: .. This quickstart section is also included in docs/quickstart.rst -1. Install Djrill from PyPI: +This example uses Mailgun, but you can substitute Postmark or SendGrid +or any other supported ESP where you see "mailgun": + +1. Install Anymail from PyPI, including the ESP(s) you want to use: .. code-block:: console - $ pip install djrill + $ pip install anymail[mailgun] # or anymail[postmark,sendgrid] or ... 2. Edit your project's ``settings.py``: @@ -72,43 +82,53 @@ Djrill 1-2-3 INSTALLED_APPS = ( ... - "djrill" + "anymail" ) - MANDRILL_API_KEY = "" - EMAIL_BACKEND = "djrill.mail.backends.djrill.DjrillBackend" + ANYMAIL = { + "MAILGUN_API_KEY": "", + } + EMAIL_BACKEND = "anymail.backends.mailgun.MailgunBackend" # or sendgrid.SendGridBackend, or... DEFAULT_FROM_EMAIL = "you@example.com" # if you don't already have this in settings 3. Now the regular `Django email functions `_ - will send through Mandrill: + will send through your chosen ESP: .. code-block:: python from django.core.mail import send_mail - send_mail("It works!", "This will get sent through Mandrill", - "Djrill Sender ", ["to@example.com"]) + send_mail("It works!", "This will get sent through Mailgun", + "Anymail Sender ", ["to@example.com"]) - You could send an HTML message, complete with custom Mandrill tags and metadata: + You could send an HTML message, complete with an inline image, + custom tags and metadata: .. code-block:: python from django.core.mail import EmailMultiAlternatives + from anymail.message import attach_inline_image msg = EmailMultiAlternatives( - subject="Djrill Message", - body="This is the text email body", - from_email="Djrill Sender ", - to=["Recipient One ", "another.person@example.com"], - headers={'Reply-To': "Service "} # optional extra headers - ) - msg.attach_alternative("

This is the HTML email body

", "text/html") + subject="Please activate your account", + body="Click to activate your account: http://example.com/activate", + from_email="Example ", + to=["New User ", "account.manager@example.com"], + reply_to=["Helpdesk "]) - # Optional Mandrill-specific extensions: - msg.tags = ["one tag", "two tag", "red tag", "blue tag"] - msg.metadata = {'user_id': "8675309"} + # Include an inline image in the html: + logo_cid = attach_inline_image(msg, open("logo.jpg", "rb").read()) + html = """Logo +

Please activate + your account

""".format(logo_cid=logo_cid) + msg.attach_alternative(html, "text/html") + + # Optional Anymail extensions: + msg.metadata = {"user_id": "8675309", "experiment_variation": 1} + msg.tags = ["activation", "onboarding"] + msg.track_clicks = True # Send it: msg.send() @@ -116,5 +136,5 @@ Djrill 1-2-3 .. END quickstart -See the `full documentation `_ +See the `full documentation `_ for more features and options. diff --git a/docs/conf.py b/docs/conf.py index d20a255..af92543 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -259,7 +259,7 @@ texinfo_documents = [ # -- Options for Intersphinx ------------------------------------------------ intersphinx_mapping = { - 'python': ('http://docs.python.org/2.7', None), + 'python': ('http://docs.python.org/3.5', None), 'django': ('http://docs.djangoproject.com/en/stable/', 'http://docs.djangoproject.com/en/stable/_objects/'), 'requests': ('http://docs.python-requests.org/en/latest/', None), } @@ -268,22 +268,22 @@ intersphinx_mapping = { def setup(app): # Django-specific roles, from https://github.com/django/django/blob/master/docs/_ext/djangodocs.py: app.add_crossref_type( - directivename = "setting", - rolename = "setting", - indextemplate = "pair: %s; setting", + directivename="setting", + rolename="setting", + indextemplate="pair: %s; setting", ) app.add_crossref_type( - directivename = "templatetag", - rolename = "ttag", - indextemplate = "pair: %s; template tag" + directivename="templatetag", + rolename="ttag", + indextemplate="pair: %s; template tag" ) app.add_crossref_type( - directivename = "templatefilter", - rolename = "tfilter", - indextemplate = "pair: %s; template filter" + directivename="templatefilter", + rolename="tfilter", + indextemplate="pair: %s; template filter" ) app.add_crossref_type( - directivename = "fieldlookup", - rolename = "lookup", - indextemplate = "pair: %s; field lookup type", + directivename="fieldlookup", + rolename="lookup", + indextemplate="pair: %s; field lookup type", ) diff --git a/docs/contributing.rst b/docs/contributing.rst index b7fe7eb..59eb13a 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -3,31 +3,45 @@ Contributing ============ -Djrill is maintained by its users. Your contributions are encouraged! +Anymail is maintained by its users. Your contributions are encouraged! -The `Djrill source code`_ is on github. See `AUTHORS.txt`_ for a list -of some of the people who have helped improve Djrill. +The `Anymail source code`_ is on GitHub. -.. _Djrill source code: https://github.com/brack3t/Djrill -.. _AUTHORS.txt: https://github.com/brack3t/Djrill/blob/master/AUTHORS.txt +.. _Anymail source code: https://github.com/anymail/django-anymail + + +Contributors +------------ + +See `AUTHORS.txt`_ for a list of some of the people who have helped +improve Anymail. + +Anymail evolved from the `Djrill`_ project. Special thanks to the +folks from `brack3t`_ who developed the original version of Djrill. + +.. _AUTHORS.txt: https://github.com/anymail/django-anymail/blob/master/AUTHORS.txt +.. _brack3t: http://brack3t.com/ +.. _Djrill: https://github.com/brack3t/Djrill Bugs ---- -You can report problems or request features in -`Djrill's github issue tracker `_. +You can report problems or request features in `Anymail's GitHub issue tracker`_. We also have some :ref:`troubleshooting` information that may be helpful. +.. _Anymail's GitHub issue tracker: https://github.com/anymail/django-anymail/issues -Pull Requests + +Pull requests ------------- -Pull requests are always welcome to fix bugs and improve support for Mandrill and Django features. +Pull requests are always welcome to fix bugs and improve support for ESP and Django features. * Please include test cases. -* We try to follow the `Django coding style`_ (basically, PEP 8 with longer lines OK). +* We try to follow the `Django coding style`_ + (basically, :pep:`8` with longer lines OK). * By submitting a pull request, you're agreeing to release your changes under under the same BSD license as the rest of this project. @@ -39,27 +53,31 @@ Pull requests are always welcome to fix bugs and improve support for Mandrill an Testing ------- -Djrill is `tested on Travis `_ against several -combinations of Django and Python versions. (Full list in -`.travis.yml `_.) +Anymail is `tested on Travis`_ against several combinations of Django +and Python versions. (Full list in `.travis.yml`_.) -Most of the included tests verify that Djrill constructs the expected Mandrill API -calls, without actually calling Mandrill or sending any email. So these tests -don't require a Mandrill API key, but they *do* require -`mock `_ -and `six `_ (``pip install mock six``). +Most of the included tests verify that Anymail constructs the expected ESP API +calls, without actually calling the ESP's API or sending any email. So these tests +don't require API keys, but they *do* require `mock`_ (``pip install mock``). -To run the tests, either:: +To run the tests, either: - python -Wall setup.py test + .. code-block:: console -or:: + $ python -Wall setup.py test - python -Wall runtests.py +or: + .. code-block:: console -If you set the environment variable `MANDRILL_TEST_API_KEY` to a valid Mandrill -`test API key`_, there are also a handful of integration tests which will run against -the live Mandrill API. (Otherwise these live API tests are skipped.) + $ python -Wall runtests.py -.. _test API key: https://mandrill.zendesk.com/hc/en-us/articles/205582447#test_key +Anymail also includes some integration tests, which do call the live ESP APIs. +These integration tests require API keys (and sometimes other settings) they +get from from environment variables. Look in the ``*_integration_tests.py`` +files in the `tests source`_ for specific requirements. + +.. _.travis.yml: https://github.com/anymail/django-anymail/blob/master/.travis.yml +.. _tests source: https://github.com/anymail/django-anymail/blob/master/anymail/tests +.. _mock: http://www.voidspace.org.uk/python/mock/index.html +.. _tested on Travis: https://travis-ci.org/anymail/django-anymail diff --git a/docs/esps/index.rst b/docs/esps/index.rst new file mode 100644 index 0000000..b0b7e8b --- /dev/null +++ b/docs/esps/index.rst @@ -0,0 +1,23 @@ +.. _supported-esps: + +Supported ESPs +-------------- + +Anymail supports these ESPs. Click in for the specific +settings required and notes about any quirks or limitations: + +.. these are listed in alphabetical order + +.. toctree:: + :maxdepth: 1 + + mailgun + mandrill + postmark + sendgrid + + +Don't see your favorite ESP here? You can suggest that +Anymail add it, or even :ref:`contribute ` +your own implementation to Anymail. + diff --git a/docs/esps/mailgun.rst b/docs/esps/mailgun.rst new file mode 100644 index 0000000..a70ab06 --- /dev/null +++ b/docs/esps/mailgun.rst @@ -0,0 +1,103 @@ +.. _mailgun-backend: + +Mailgun +------- + +Anymail integrates with the `Mailgun `_ +transactional email service from Rackspace, using their +REST API. + + +Settings +======== + +.. rubric:: EMAIL_BACKEND + +To use Anymail's Mailgun backend, set: + + .. code-block:: python + + EMAIL_BACKEND = "anymail.backends.mailgun.MailgunBackend" + +in your settings.py. (Watch your capitalization: Mailgun spells their name with a +lowercase "g", so Anymail does too.) + + +.. setting:: ANYMAIL_MAILGUN_API_KEY + +.. rubric:: MAILGUN_API_KEY + +Required. Your Mailgun API key: + + .. code-block:: python + + ANYMAIL = { + ... + "MAILGUN_API_KEY": "", + } + +Anymail will also look for ``MAILGUN_API_KEY`` at the +root of the settings file if neither ``ANYMAIL["MAILGUN_API_KEY"]`` +nor ``ANYMAIL_MAILGUN_API_KEY`` is set. + + +.. setting:: ANYMAIL_MAILGUN_API_URL + +.. rubric:: MAILGUN_API_URL + +The base url for calling the Mailgun API. It does not include +the sender domain. (Anymail :ref:`figures this out ` +for you.) + +The default is ``MAILGUN_API_URL = "https://api.mailgun.net/v3"`` +(It's unlikely you would need to change this.) + + +.. _mailgun-sender-domain: + +Email sender domain +=================== + +Mailgun's API requires a sender domain `in the API url `_. +By default, Anymail will use the domain of each email's from address +as the domain for the Mailgun API. + +If you need to override this default, you can use Anymail's +:attr:`esp_extra` dict, either on an individual message: + + .. code-block:: python + + message = EmailMessage(from_email="sales@europe.example.com", ...) + message.esp_extra = {"sender_domain": "example.com"} + + +... or as a global :ref:`send default ` setting that applies +to all messages: + + .. code-block:: python + + ANYMAIL = { + ... + "MAILGUN_SEND_DEFAULTS": { + "esp_extra": {"sender_domain": "example.com"} + } + } + +.. _base-url: https://documentation.mailgun.com/api-intro.html#base-url + + +Mailgun esp_extra +================= + +Anymail's Mailgun backend will pass all :attr:`~anymail.message.AnymailMessage.esp_extra` +values directly to Mailgun. You can use any of the (non-file) parameters listed in the +`Mailgun sending docs`_. Example: + + .. code-block:: python + + message = AnymailMessage(...) + message.esp_extra = { + 'o:testmode': 'yes', # use Mailgun's test mode + } + +.. _Mailgun sending docs: https://documentation.mailgun.com/api-sending.html#sending diff --git a/docs/esps/mandrill.rst b/docs/esps/mandrill.rst new file mode 100644 index 0000000..1d08a6c --- /dev/null +++ b/docs/esps/mandrill.rst @@ -0,0 +1,151 @@ +.. _mandrill-backend: + +Mandrill +-------- + +Anymail integrates with the `Mandrill `_ +transactional email service from MailChimp. + + +Settings +======== + +.. rubric:: EMAIL_BACKEND + +To use Anymail's Mandrill backend, set: + + .. code-block:: python + + EMAIL_BACKEND = "anymail.backends.mandrill.MandrillBackend" + +in your settings.py. + + +.. setting:: ANYMAIL_MANDRILL_API_KEY + +.. rubric:: MANDRILL_API_KEY + +Required. Your Mandrill API key: + + .. code-block:: python + + ANYMAIL = { + ... + "MANDRILL_API_KEY": "", + } + +Anymail will also look for ``MANDRILL_API_KEY`` at the +root of the settings file if neither ``ANYMAIL["MANDRILL_API_KEY"]`` +nor ``ANYMAIL_MANDRILL_API_KEY`` is set. + + +.. setting:: ANYMAIL_MANDRILL_API_URL + +.. rubric:: MANDRILL_API_URL + +The base url for calling the Mandrill API. The default is +``MANDRILL_API_URL = "https://mandrillapp.com/api/1.0"``, +which is the secure, production version of Mandrill's 1.0 API. + +(It's unlikely you would need to change this.) + + +Mandrill esp_extra +================== + +Anymail's Mandrill backend does not yet implement the +:attr:`~anymail.message.AnymailMessage.esp_extra` feature. + + +.. _migrating-from-djrill: + +Migrating from Djrill +===================== + +Anymail has its origins as a fork of the `Djrill`_ +package, which supported only Mandrill. If you are migrating +from Djrill to Anymail -- e.g., because you are thinking +of switching ESPs -- you'll need to make a few changes +to your code. + +.. _Djrill: https://github.com/brack3t/Djrill + +Changes to settings +~~~~~~~~~~~~~~~~~~~ + +``MANDRILL_API_KEY`` + Will still work, but consider moving it into the :setting:`ANYMAIL` + settings dict, or changing it to :setting:`ANYMAIL_MANDRILL_API_KEY`. + +``MANDRILL_SETTINGS`` + Use :setting:`ANYMAIL_SEND_DEFAULTS` and/or :setting:`ANYMAIL_MANDRILL_SEND_DEFAULTS` + (see :ref:`send-defaults`). + + There is one slight behavioral difference between :setting:`ANYMAIL_SEND_DEFAULTS` + and Djrill's ``MANDRILL_SETTINGS``: in Djrill, setting :attr:`tags` or + :attr:`merge_vars` on a message would completely override any global + settings defaults. In Anymail, those message attributes are merged with + the values from :setting:`ANYMAIL_SEND_DEFAULTS`. + +``MANDRILL_SUBACCOUNT`` + Use :attr:`esp_extra` in :setting:`ANYMAIL_MANDRILL_SEND_DEFAULTS`: + + .. code-block:: python + + ANYMAIL = { + ... + "MANDRILL_SEND_DEFAULTS": { + "esp_extra": {"subaccount": ""} + } + } + +``MANDRILL_IGNORE_RECIPIENT_STATUS`` + Renamed to :setting:`ANYMAIL_IGNORE_RECIPIENT_STATUS` + (or just `IGNORE_RECIPIENT_STATUS` in the :setting:`ANYMAIL` + settings dict). + + +Changes to EmailMessage attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``message.send_at`` + If you are using an aware datetime for :attr:`send_at`, + it will keep working unchanged with Anymail. + + If you are using a date (without a time), or a naive datetime, + be aware that these now default to Django's current_timezone, + rather than UTC as in Djrill. + + (As with Djrill, it's best to use an aware datetime + that says exactly when you want the message sent.) + + +``message.mandrill_response`` + Anymail normalizes ESP responses, so you don't have to be familiar + with the format of Mandrill's JSON. See :attr:`anymail_status`. + + The *raw* ESP response is attached to a sent message as + ``anymail_status.esp_response``, so the direct replacement + for message.mandrill_response is: + + .. code-block:: python + + mandrill_response = message.anymail_status.esp_response.json() + +**Templates and merge variables** + Coming to Anymail soon. + + However, no other ESPs support MailChimp's templating language, so + you'll need to rewrite your templates as you switch ESPs. + + Consider converting to :ref:`Django templates ` + instead, as these can be used with any email backend. + +**Other Mandrill-specific attributes** + Are currently still supported by Anymail's Mandrill backend, + but will be ignored by other Anymail backends. + + It's best to eliminate them if they're not essential + to your code. In the future, the Mandrill-only attributes + will be moved into the + :attr:`~anymail.message.AnymailMessage.esp_extra` dict. diff --git a/docs/esps/postmark.rst b/docs/esps/postmark.rst new file mode 100644 index 0000000..815b417 --- /dev/null +++ b/docs/esps/postmark.rst @@ -0,0 +1,17 @@ +.. _postmark: + +Postmark +-------- + +.. note:: + + Postmark support coming soon + + +Settings +======== + + EMAIL_BACKEND = "anymail.backends.postmark.PostmarkBackend" + +(Watch your capitalization: Postmark spells their name with a +lowercase "m", so Anymail does too.) diff --git a/docs/esps/sendgrid.rst b/docs/esps/sendgrid.rst new file mode 100644 index 0000000..1e727a1 --- /dev/null +++ b/docs/esps/sendgrid.rst @@ -0,0 +1,17 @@ +.. _sendgrid: + +SendGrid +-------- + +.. note:: + + SendGrid support is being developed now + + +Settings +======== + + EMAIL_BACKEND = "anymail.backends.sendgrid.SendGridBackend" + +(Watch your capitalization: SendGrid spells their name with an +uppercase "G", so Anymail does too.) diff --git a/docs/history.rst b/docs/history.rst deleted file mode 100644 index 89e98ce..0000000 --- a/docs/history.rst +++ /dev/null @@ -1,146 +0,0 @@ -.. _history: - -Release Notes -============= - -Djrill practices `semantic versioning `_. -Among other things, this means that minor updates -(1.x to 1.y) should always be backwards-compatible, -and breaking changes will always increment the -major version number (1.x to 2.0). - - -Djrill 2.x ----------- - -Version 2.1 (in development): - -* Handle Mandrill rejection whitelist/blacklist sync event webhooks - - -Version 2.0: - -* **Breaking Changes:** please see the :ref:`upgrade guide `. -* Add Django 1.9 support; drop Django 1.3, Python 2.6, and Python 3.2 support -* Add global :setting:`MANDRILL_SETTINGS` dict that can provide defaults - for most Djrill message options -* Add :exc:`djrill.NotSerializableForMandrillError` -* Use a single HTTP connection to the Mandrill API to improve performance - when sending multiple messages at once using :func:`~django.core.mail.send_mass_mail`. - (You can also directly manage your own long-lived Djrill connection across multiple sends, - by calling open and close on :ref:`Django's email backend `.) -* Add Djrill version to user-agent header when calling Mandrill API -* Improve diagnostics in exceptions from Djrill -* Remove DjrillAdminSite -* Remove unintended date-to-string conversion in JSON encoding -* Remove obsolete DjrillMessage class and DjrillBackendHTTPError -* Refactor Djrill backend and exceptions - - -Djrill 1.x and Earlier ----------------------- - -Version 1.4: - -* Django 1.8 support -* Support new Django 1.8 EmailMessage reply_to param. - (Specifying a :ref:`Reply-To header ` - still works, with any version of Django, - and will override the reply_to param if you use both.) -* Include Mandrill error response in str(MandrillAPIError), - to make errors easier to understand. -* More-helpful exception when using a non-JSON-serializable - type in merge_vars and other Djrill message attributes -* Deprecation warnings for upcoming 2.0 changes (see above) - - -Version 1.3: - -* Use Mandrill secure https API endpoint (rather than http). -* Support :attr:`merge_language` option (for choosing between - Handlebars and Mailchimp templates). - - -Version 1.2: - -* Support Django 1.7; add testing on Python 3.3, 3.4, and PyPy -* Bug fixes - - -Version 1.1: - -* Allow use of Mandrill template default "from" and "subject" fields, - via :attr:`use_template_from` and :attr:`use_template_subject`. -* Fix `UnicodeEncodeError` with unicode attachments - - -Version 1.0: - -* Global :setting:`MANDRILL_SUBACCOUNT` setting - - -Version 0.9: - -* Better handling for "cc" and "bcc" recipients. -* Allow all extra message headers in send. - (Mandrill has relaxed previous API restrictions on headers.) - - -Version 0.8: - -* Expose :ref:`mandrill-response` on sent messages - - -Version 0.7: - -* Support for Mandrill send options :attr:`async`, :attr:`important`, - :attr:`ip_pool`, :attr:`return_path_domain`, :attr:`send_at`, - :attr:`subaccount`, and :attr:`view_content_link` - - -Version 0.6: - -* Support for signed webhooks - - -Version 0.5: - -* Support for incoming mail and other Mandrill webhooks -* Support for Mandrill send options :attr:`auto_html`, :attr:`tracking_domain` - and :attr:`signing_domain`. - - -Version 0.4: - -* Attachments with a Content-ID are now treated as - :ref:`embedded images ` -* New Mandrill :attr:`inline_css` option is supported -* Remove limitations on attachment types, to track Mandrill change -* Documentation is now available on - `djrill.readthedocs.org `_ - - -Version 0.3: - -* :ref:`Attachments ` are now supported -* :ref:`Mandrill templates ` are now supported -* A bcc address is now passed to Mandrill as bcc, rather than being lumped in - with the "to" recipients. Multiple bcc recipients will now raise an exception, - as Mandrill only allows one. -* Python 3 support (with Django 1.5) -* Exceptions should be more useful: - :exc:`djrill.NotSupportedByMandrillError` replaces generic ValueError; - :exc:`djrill.MandrillAPIError` replaces DjrillBackendHTTPError, and is now - derived from requests.HTTPError. - (New exceptions are backwards compatible with old ones for existing code.) - - -Version 0.2: - -* ``MANDRILL_API_URL`` is no longer required in settings.py -* Earlier versions of Djrill required use of a ``DjrillMessage`` class to - specify Mandrill-specific options. This is no longer needed -- Mandrill - options can now be set directly on a Django ``EmailMessage`` object or any - subclass. (Existing code can continue to use ``DjrillMessage``.) - -.. _semver: http://semver.org diff --git a/docs/inbound.rst b/docs/inbound.rst new file mode 100644 index 0000000..7eda9ce --- /dev/null +++ b/docs/inbound.rst @@ -0,0 +1,18 @@ +.. _inbound: + +Receiving inbound email +======================= + +.. note:: + + Normalized inbound email handling is coming soon to Anymail. + + +.. _inbound-webhooks: + +Configuring inbound webhooks +---------------------------- + + +Inbound email signals +--------------------- diff --git a/docs/index.rst b/docs/index.rst index 782a358..a86cca4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,10 +1,5 @@ -.. Djrill documentation master file, created by - sphinx-quickstart on Sat Mar 2 13:07:34 2013. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Djrill: Mandrill Transactional Email for Django -=============================================== +Anymail: Multi-ESP transactional email for Django +================================================= Version |release| @@ -15,6 +10,8 @@ Version |release| :end-before: END shared-intro +.. _main-toc: + Documentation ------------- @@ -23,21 +20,10 @@ Documentation quickstart installation - upgrading - usage/sending_mail - usage/templates - usage/multiple_backends - usage/webhooks + sending/index + inbound + esps/index + tips/index troubleshooting contributing - history - - -Thanks ------- - -Thanks to the MailChimp team for asking us to build this nifty little app, and to all of Djrill's -:doc:`contributors `. -Oh, and, of course, Kenneth Reitz for the awesome requests_ library. - -.. _requests: http://docs.python-requests.org + release_notes diff --git a/docs/installation.rst b/docs/installation.rst index ec0b0f2..22987ab 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -1,117 +1,167 @@ -Installation -============ +Installation and configuration +============================== -It's easiest to install Djrill from `PyPI `_: +.. _installation: + +Installing Anymail +------------------ + +Install Anymail from PyPI using pip. + +Anymail uses python setuptools' "extra features" to pull in dependencies +for specific ESPs. (This avoids installing packages needed by ESPs +you aren't using.) + +You'll want to include at least one ESP as an extra in your pip command. +E.g., for Anymail with Mailgun support: .. code-block:: console - $ pip install djrill + $ pip install anymail[mailgun] -If you decide to install Djrill some other way, you'll also need to install its -one dependency (other than Django, of course): the `requests `_ -library from Kenneth Reitz. +...or with both Postmark and SendGrid support: + + .. code-block:: console + + $ pip install anymail[postmark,sendgrid] -Configuration -------------- +.. _backend-configuration: -.. setting:: MANDRILL_API_KEY +Configuring Django's email backend +---------------------------------- -In your project's :file:`settings.py`: +To use Anymail for sending email, edit your Django project's :file:`settings.py`: -1. Add :mod:`djrill` to your :setting:`INSTALLED_APPS`:: +1. Add :mod:`anymail` to your :setting:`INSTALLED_APPS`: - INSTALLED_APPS = ( - ... - "djrill" - ) + .. code-block:: python -2. Add the following line, substituting your own :setting:`MANDRILL_API_KEY`:: + INSTALLED_APPS = ( + ... + "anymail", + ) - MANDRILL_API_KEY = "brack3t-is-awesome" +2. Add an :setting:`ANYMAIL` settings dict, substituting the appropriate settings for + your ESP: -3. Override your existing :setting:`EMAIL_BACKEND` with the following line:: + .. code-block:: python - EMAIL_BACKEND = "djrill.mail.backends.djrill.DjrillBackend" + ANYMAIL = { + "MAILGUN_API_KEY" = "", + } + +3. Change your existing Django :setting:`EMAIL_BACKEND` to the Anymail backend + for your ESP. For example, to send using Mailgun by default: + + .. code-block:: python + + EMAIL_BACKEND = "anymail.backends.mailgun.MailgunBackend" + + (:setting:`EMAIL_BACKEND` sets Django's default for sending emails; you can also + use :ref:`multiple Anymail backends ` to send particular + messages through different ESPs.) + + The exact backend name and required settings vary by ESP. + See the :ref:`supported ESPs ` section for specifics. + +Also, if you don't already have a :setting:`DEFAULT_FROM_EMAIL` in your settings, +this is a good time to add one. (Django's default is "webmaster\@localhost", +which some ESPs will reject.) -Also, if you don't already have a :setting:`DEFAULT_FROM_EMAIL` in settings, -this is a good time to add one. (Django's default is "webmaster@localhost", -which won't work with Mandrill.) +Configuring status tracking webhooks +------------------------------------ + +Anymail can optionally connect to your ESPs event webhooks to notify your app +of status like bounced and rejected emails, successful delivery, message opens +and clicks, and other tracking. + +If you want to use Anymail's status tracking webhooks, follow the steps above +to :ref:`configure an Anymail backend `, and then +follow the instructions in the :ref:`event-tracking` section to set up +the delivery webhooks. -Mandrill Webhooks (Optional) ----------------------------- +Configuring inbound email +------------------------- -Djrill includes optional support for Mandrill webhooks, including inbound email. -See the Djrill :ref:`webhooks ` section for configuration details. +Anymail can optionally connect to your ESPs inbound webhook to notify your app +of inbound messages. + +If you want to use inbound email with Anymail, first follow the first two +:ref:`backend configuration ` steps above. (You can +skip changing your :setting:`EMAIL_BACKEND` if you don't want to us Anymail +for *sending* messages.) Then follow the instructions in the +:ref:`inbound-webhooks` section to set up the inbound webhooks. -Other Optional Settings ------------------------ -You can optionally add any of these Djrill settings to your :file:`settings.py`. +.. setting:: ANYMAIL + +Anymail settings reference +-------------------------- + +You can add Anymail settings to your project's :file:`settings.py` either as +a single ``ANYMAIL`` dict, or by breaking out individual settings prefixed with +``ANYMAIL_``. So this settings dict: + + .. code-block:: python + + ANYMAIL = { + "MAILGUN_API_KEY": "12345", + "SEND_DEFAULTS": { + "tags": ["myapp"] + }, + } + +...is equivalent to these individual settings: + + .. code-block:: python + + ANYMAIL_MAILGUN_API_KEY = "12345" + ANYMAIL_SEND_DEFAULTS = {"tags": ["myapp"]} + +In addition, for some ESP settings like API keys, Anymail will look for a setting +without the ``ANYMAIL_`` prefix if it can't find the Anymail one. (This can be helpful +if you are using other Django apps that work with the same ESP.) + + .. code-block:: python + + MAILGUN_API_KEY = "12345" # used only if neither ANYMAIL["MAILGUN_API_KEY"] + # nor ANYMAIL_MAILGUN_API_KEY have been set -.. setting:: MANDRILL_IGNORE_RECIPIENT_STATUS - -MANDRILL_IGNORE_RECIPIENT_STATUS -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Set to ``True`` to disable :exc:`djrill.MandrillRecipientsRefused` exceptions -on invalid or rejected recipients. (Default ``False``.) - -.. versionadded:: 2.0 +There are specific Anymail settings for each ESP (like API keys and urls). +See the :ref:`supported ESPs ` section for details. +Here are the other settings Anymail supports: -.. setting:: MANDRILL_SETTINGS +.. setting:: ANYMAIL_IGNORE_RECIPIENT_STATUS -MANDRILL_SETTINGS -~~~~~~~~~~~~~~~~~ +.. rubric:: IGNORE_RECIPIENT_STATUS -You can supply global default options to apply to all messages sent through Djrill. -Set :setting:`!MANDRILL_SETTINGS` to a dict of these options. Example:: +Set to `True` to disable :exc:`AnymailRecipientsRefused` exceptions +on invalid or rejected recipients. (Default `False`.) +See :ref:`recipients-refused`. - MANDRILL_SETTINGS = { - 'subaccount': 'client-347', - 'tracking_domain': 'example.com', - 'track_opens': True, - } + .. code-block:: python -See :ref:`mandrill-send-support` for a list of available options. (Everything -*except* :attr:`merge_vars`, :attr:`recipient_metadata`, and :attr:`send_at` -can be used with :setting:`!MANDRILL_SETTINGS`.) - -Attributes set on individual EmailMessage objects will override the global -:setting:`!MANDRILL_SETTINGS` for that message. :attr:`global_merge_vars` -on an EmailMessage will be merged with any ``global_merge_vars`` in -:setting:`!MANDRILL_SETTINGS` (with the ones on the EmailMessage taking -precedence if there are conflicting var names). - -.. versionadded:: 2.0 + ANYMAIL = { + ... + "IGNORE_RECIPIENT_STATUS": True, + } -.. setting:: MANDRILL_API_URL +.. rubric:: SEND_DEFAULTS and *ESP*\ _SEND_DEFAULTS` -MANDRILL_API_URL -~~~~~~~~~~~~~~~~ - -The base url for calling the Mandrill API. The default is -``MANDRILL_API_URL = "https://mandrillapp.com/api/1.0"``, -which is the secure, production version of Mandrill's 1.0 API. - -(It's unlikely you would need to change this.) +A `dict` of default options to apply to all messages sent through Anymail. +See :ref:`send-defaults`. -.. setting:: MANDRILL_SUBACCOUNT +.. rubric:: UNSUPPORTED_FEATURE_ERRORS -MANDRILL_SUBACCOUNT -~~~~~~~~~~~~~~~~~~~ - -Prior to Djrill 2.0, the :setting:`!MANDRILL_SUBACCOUNT` setting could -be used to globally set the `Mandrill subaccount `_. -Although this is still supported for compatibility with existing code, -new code should set a global subaccount in :setting:`MANDRILL_SETTINGS` -as shown above. - -.. _subaccounts: http://help.mandrill.com/entries/25523278-What-are-subaccounts- +Whether Anymail should raise :exc:`~anymail.exceptions.AnymailUnsupportedFeature` +errors for email with features that can't be accurately communicated to the ESP. +Set to `False` to ignore these problems and send the email anyway. See +:ref:`unsupported-features`. (Default `True`.) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 06be696..3053166 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -1,5 +1,5 @@ -Djrill 1-2-3 -============ +Anymail 1-2-3 +============= .. Quickstart is maintained in README.rst at the source root. (Docs can include from the readme; the readme can't include anything.) @@ -7,3 +7,16 @@ Djrill 1-2-3 .. include:: ../README.rst :start-after: _quickstart: :end-before: END quickstart + + +Problems? We have some :ref:`troubleshooting` info that may help. + + +.. rubric:: Now what? + +Now that you've got Anymail working, you might be interested in: + +* :ref:`Sending email with Anymail ` +* :ref:`Receiving inbound email ` +* :ref:`ESP-specific information ` +* :ref:`All the docs ` diff --git a/docs/release_notes.rst b/docs/release_notes.rst new file mode 100644 index 0000000..d6c68f1 --- /dev/null +++ b/docs/release_notes.rst @@ -0,0 +1,23 @@ +.. _release_notes: + +Release notes +============= + +Complete release notes can be found in the project's +`GitHub releases page`_. + +Anymail practices `semantic versioning `_. +Among other things, this means that minor updates +(1.x to 1.y) should always be backwards-compatible, +and breaking changes will always increment the +major version number (1.x to 2.0). + +.. rubric:: PRE-1.0 DEVELOPMENT VERSIONS + +Anymail is under active, early development right now. +Prior to a 1.0 alpha, features and APIs may change +rapidly. (Per semver, the 0.x minor version will get +bumped for any breaking changes before 1.0.) + +.. _GitHub releases page: https://github.com/anymail/django-anymail/releases +.. _semver: http://semver.org diff --git a/docs/sending/anymail_additions.rst b/docs/sending/anymail_additions.rst new file mode 100644 index 0000000..90e1e1b --- /dev/null +++ b/docs/sending/anymail_additions.rst @@ -0,0 +1,449 @@ +.. _anymail-send-features: + +.. module:: anymail.message + +Anymail additions +================= + +Anymail normalizes several common ESP features, like adding +metadata or tags to a message. It also normalizes the response +from the ESP's send API. + +There are three ways you can use Anymail's ESP features with +your Django email: + +* Just use Anymail's added attributes directly on *any* Django + :class:`~django.core.mail.EmailMessage` object (or any subclass). + +* Create your email message using the :class:`AnymailMessage` class, + which exposes extra attributes for the ESP features. + +* Use the :class:`AnymailMessageMixin` to add the Anymail extras + to some other EmailMessage-derived class (your own or from + another Django package). + +The first approach is usually the simplest. The other two can be +helpful if you are working with Python development tools that +offer type checking or other static code analysis. + + +ESP send options (AnymailMessage) +--------------------------------- + +.. class:: AnymailMessage + + A subclass of Django's :class:`~django.core.mail.EmailMultiAlternatives` + that exposes additional ESP functionality. + + The constructor accepts any of the attributes below, or you can set + them directly on the message at any time before sending: + + .. code-block:: python + + from anymail.message import AnymailMessage + + message = AnymailMessage( + subject="Welcome", + body="Welcome to our site", + to=["New User "], + tags=["Onboarding"], # Anymail extra in constructor + ) + # Anymail extra attributes: + message.metadata = {"onboarding_experiment": "variation 1"} + message.track_clicks = True + + message.send() + status = message.anymail_status # available after sending + status.message_id # e.g., '<12345.67890@example.com>' + status.recipients["user1@example.com"].status # e.g., 'queued' + + + .. rubric:: Attributes you can add to messages + + .. attribute:: metadata + + Set this to a `dict` of metadata values the ESP should store + with the message, for later search and retrieval. + + .. code-block:: python + + message.metadata = {"customer": customer.id, + "order": order.reference_number} + + ESPs have differing restrictions on metadata content. + For portability, it's best to stick to alphanumeric keys, and values + that are numbers or strings. + + You should format any non-string data into a string before setting it + as metadata. See :ref:`formatting-merge-data`. + + + .. attribute:: tags + + Set this to a `list` of `str` tags to apply to the message (usually + for segmenting ESP reporting). + + .. code-block:: python + + message.tags = ["Order Confirmation", "Test Variant A"] + + ESPs have differing restrictions on tags. For portability, + it's best to stick with strings that start with an alphanumeric + character. (Also, PostMark only allows a single tag per message.) + + + .. caution:: + + Some ESPs put :attr:`metadata` and :attr:`tags` in email headers, + which are included with the email when it is delivered. Anything you + put in them **could be exposed to the recipients,** so don't + include sensitive data. + + + .. attribute:: track_opens + + Set this to `True` or `False` to override your ESP account default + setting for tracking when users open a message. + + .. code-block:: python + + message.track_opens = True + + + .. attribute:: track_clicks + + Set this to `True` or `False` to override your ESP account default + setting for tracking when users click on a link in a message. + + .. code-block:: python + + message.track_clicks = False + + + .. attribute:: send_at + + Set this to a `~datetime.datetime`, `~datetime.date` to + have the ESP wait until the specified time to send the message. + (You can also use a `float` or `int`, which will be treated + as a POSIX timestamp as in :func:`time.time`.) + + .. code-block:: python + + from datetime import datetime, timedelta + from django.utils.timezone import utc + + message.send_at = datetime.now(utc) + timedelta(hours=1) + + To avoid confusion, it's best to provide either an *aware* + `~datetime.datetime` (one that has its tzinfo set), or an + `int` or `float` seconds-since-the-epoch timestamp. + + If you set :attr:`!send_at` to a `~datetime.date` or a *naive* + `~datetime.datetime` (without a timezone), Anymail will interpret it in + Django's :ref:`current timezone `. + (Careful: :meth:`datetime.now() ` returns a *naive* + datetime, unless you call it with a timezone like in the example above.) + + The sent message will be held for delivery by your ESP -- not locally by Anymail. + + + .. attribute:: esp_extra + + Set this to a `dict` of additional, ESP-specific settings for the message. + + Using this attribute is inherently non-portable between ESPs, and is + intended as an "escape hatch" for accessing functionality that Anymail + doesn't (or doesn't yet) support. + + See the notes for each :ref:`specific ESP ` for information + on its :attr:`!esp_extra` handling. + + + .. rubric:: Status response from the ESP + + .. attribute:: anymail_status + + Normalized response from the ESP API's send call. Anymail adds this + to each :class:`~django.core.mail.EmailMessage` as it is sent. + + The value is an :class:`AnymailStatus`. + See :ref:`esp-send-status` for details. + + + .. rubric:: Convenience methods + + (These methods are only available on :class:`AnymailMessage` or + :class:`AnymailMessageMixin` objects. Unlike the attributes above, + they can't be used on an arbitrary :class:`~django.core.mail.EmailMessage`.) + + .. method:: attach_inline_image(content, subtype=None, idstring="img", domain=None) + + Attach an inline (embedded) image to the message and return its :mailheader:`Content-ID`. + + This calls :func:`attach_inline_image` on the message. See :ref:`inline-images` + for details and an example. + + +.. _esp-send-status: + +ESP send status +--------------- + +.. class:: AnymailStatus + + When you send a message through an Anymail backend, Anymail adds + an :attr:`~AnymailMessage.anymail_status` attribute to the + :class:`~django.core.mail.EmailMessage`, with a normalized version + of the ESP's response. + + :attr:`~AnymailMessage.anymail_status` will be an object with these attributes: + + .. attribute:: message_id + + The message id assigned by the ESP, or `None` if the send call failed. + + The exact format varies by ESP. Some use a UUID or similar; + some use an :rfc:`2822` :mailheader:`Message-ID` as the id: + + .. code-block:: python + + message.anymail_status.message_id + # '<20160306015544.116301.25145@example.org>' + + Some ESPs assign a unique message ID for *each recipient* (to, cc, bcc) + of a single message. In that case, :attr:`!message_id` will be a + `set` of all the message IDs across all recipients: + + .. code-block:: python + + message.anymail_status.message_id + # set(['16fd2706-8baf-433b-82eb-8c7fada847da', + # '886313e1-3b8a-5372-9b90-0c9aee199e5d']) + + + .. attribute:: status + + A `set` of send statuses, across all recipients (to, cc, bcc) of the + message, or `None` if the send call failed. + + .. code-block:: python + + message1.anymail_status.status + # set(['queued']) # all recipients were queued + message2.anymail_status.status + # set(['rejected', 'sent']) # at least one recipient was sent, + # and at least one rejected + + # This is an easy way to check there weren't any problems: + if message3.anymail_status.status.issubset({'queued', 'sent'}): + print("ok!") + + Anymail normalizes ESP sent status to one of these values: + + * `'sent'` the ESP has sent the message + (though it may or may not end up delivered) + * `'queued'` the ESP has accepted the message + and will try to send it asynchronously + * `'invalid'` the ESP considers the sender or recipient email invalid + * `'rejected'` the recipient is on an ESP blacklist + (unsubscribe, previous bounces, etc.) + * `'failed'` the attempt to send failed for some other reason + * `'unknown'` anything else + + Not all ESPs check recipient emails during the send API call -- some + simply queue the message, and report problems later. In that case, + you can use Anymail's :ref:`event-tracking` features to be notified + of delivery status events. + + + .. attribute:: recipients + + A `dict` of per-recipient message ID and status values. + + The dict is keyed by each recipient's base email address + (ignoring any display name). Each value in the dict is + an object with `status` and `message_id` properties: + + .. code-block:: python + + message = EmailMultiAlternatives( + to=["you@example.com", "Me "], + subject="Re: The apocalypse") + message.send() + + message.anymail_status.recipient["you@example.com"].status + # 'sent' + message.anymail_status.recipient["me@example.com"].status + # 'queued' + message.anymail_status.recipient["me@example.com"].message_id + # '886313e1-3b8a-5372-9b90-0c9aee199e5d' + + Will be an empty dict if the send call failed. + + + .. attribute:: esp_response + + The raw response from the ESP API call. The exact type varies by + backend. Accessing this is inherently non-portable. + + .. code-block:: python + + # This will work with a requests-based backend: + message.anymail_status.esp_response.json() + + +.. _inline-images: + +Inline images +------------- + +Anymail includes a convenience function to simplify attaching inline images to email. + +.. function:: attach_inline_image(message, content, subtype=None, idstring="img", domain=None) + + Attach an inline (embedded) image to the message and return its :mailheader:`Content-ID`. + + In your HTML message body, prefix the returned id with `cid:` to make an + `` src attribute: + + .. code-block:: python + + from django.core.mail import EmailMultiAlternatives + from anymail.message import attach_inline_image + + # read image content -- be sure to open the file in binary mode: + with f = open("path/to/picture.jpg", "rb"): + raw_image_data = f.read() + + message = EmailMultiAlternatives( ... ) + cid = attach_inline_image(message, raw_image_data) + html = '... Picture ...' % cid + message.attach_alternative(html, "text/html") + + message.send() + + + `message` must be an :class:`~django.core.mail.EmailMessage` (or subclass) object. + + `content` must be the binary image data (e.g., read from a file). + + `subtype` is an optional MIME :mimetype:`image` subtype, e.g., `"png"` or `"jpg"`. + By default, this is determined automatically from the content. + + `idstring` and `domain` are optional, and are passed to Python's + :func:`~email.utils.make_msgid` to generate the :mailheader:`Content-ID`. + Generally the defaults should be fine. + (But be aware the default `domain` can leak your server's local hostname + in the resulting email.) + + This function works with *any* Django :class:`~django.core.mail.EmailMessage` -- + it's not specific to Anymail email backends. You can use it with messages sent + through Django's SMTP backend or any other that properly supports MIME attachments. + + (This function is also available as the + :meth:`~anymail.message.AnymailMessage.attach_inline_image` method + on Anymail's :class:`~anymail.message.AnymailMessage` and + :class:`~anymail.message.AnymailMessageMixin` classes.) + + +.. _send-defaults: + +Global send defaults +-------------------- + +.. setting:: ANYMAIL_SEND_DEFAULTS + +In your :file:`settings.py`, you can set :setting:`!ANYMAIL_SEND_DEFAULTS` +to a `dict` of default options that will apply to all messages sent through Anymail: + + .. code-block:: python + + ANYMAIL = { + ... + "SEND_DEFAULTS": { + "metadata": {"district": "North", "source": "unknown"}, + "tags": ["myapp", "version3"], + "track_clicks": True, + "track_opens": True, + }, + } + +At send time, the attributes on each :class:`~django.core.mail.EmailMessage` +get merged with the global send defaults. For example, with the +settings above: + + .. code-block:: python + + message = AnymailMessage(...) + message.tags = ["welcome"] + message.metadata = {"source": "Ads", "user_id": 12345} + message.track_clicks = False + + message.send() + # will send with: + # tags: ["myapp", "version3", "welcome"] (merged with defaults) + # metadata: {"district": "North", "source": "Ads", "user_id": 12345} (merged) + # track_clicks: False (message overrides defaults) + # track_opens: True (from the defaults) + +To prevent a message from using a particular global default, set that attribute +to `None`. (E.g., ``message.tags = None`` will send the message with no tags, +ignoring the global default.) + +Anymail's send defaults actually work for all :class:`!django.core.mail.EmailMessage` +attributes. So you could set ``"bcc": ["always-copy@example.com"]`` to add a bcc +to every message. (You could even attach a file to every message -- though +your recipients would probably find that annoying!) + +You can also set ESP-specific global defaults. If there are conflicts, +the ESP-specific value will override the main `SEND_DEFAULTS`: + + .. code-block:: python + + ANYMAIL = { + ... + "SEND_DEFAULTS": { + "tags": ["myapp", "version3"], + }, + "POSTMARK_SEND_DEFAULTS": { + # Postmark only supports a single tag + "tags": ["version3"], # overrides SEND_DEFAULTS['tags'] (not merged!) + }, + "MAILGUN_SEND_DEFAULTS": { + "esp_extra": {"o:dkim": "no"}, # Disable Mailgun DKIM signatures + }, + } + + +AnymailMessageMixin +------------------- + +.. class:: AnymailMessageMixin + + Mixin class that adds Anymail's ESP extra attributes and convenience methods + to other :class:`~django.core.mail.EmailMessage` subclasses. + + For example, with the `django-mail-templated`_ package's custom EmailMessage: + + .. code-block:: python + + from anymail.message import AnymailMessageMixin + from mail_templated import EmailMessage + + class TemplatedAnymailMessage(AnymailMessageMixin, EmailMessage): + """ + An EmailMessage that supports both Mail-Templated + and Anymail features + """ + pass + + msg = TemplatedAnymailMessage( + template_name="order_confirmation.tpl", # Mail-Templated arg + track_opens=True, # Anymail arg + ... + ) + msg.context = {"order_num": "12345"} # Mail-Templated attribute + msg.tags = ["templated"] # Anymail attribute + + +.. _django-mail-templated: https://pypi.python.org/pypi/django-mail-templated diff --git a/docs/sending/django_email.rst b/docs/sending/django_email.rst new file mode 100644 index 0000000..2609188 --- /dev/null +++ b/docs/sending/django_email.rst @@ -0,0 +1,162 @@ +.. currentmodule:: anymail + +.. _sending-django-email: + +Django email support +==================== + +Anymail builds on Django's core email functionality. If you are already sending +email using Django's default SMTP :class:`~django.core.mail.backends.smtp.EmailBackend`, +switching to Anymail will be easy. Anymail is designed to "just work" with Django. + +If you're not familiar with Django's email functions, please take a look at +":mod:`sending email `" in the Django docs first. + +Anymail supports most of the functionality of Django's :class:`~django.core.mail.EmailMessage` +and :class:`~django.core.mail.EmailMultiAlternatives` classes. + +Anymail handles **all** outgoing email sent through Django's +:mod:`django.core.mail` package, including :func:`~django.core.mail.send_mail`, +:func:`~django.core.mail.send_mass_mail`, the :class:`~django.core.mail.EmailMessage` class, +and even :func:`~django.core.mail.mail_admins`. +If you'd like to selectively send only some messages through Anymail, +or you'd like to use different ESPs for particular messages, +there are ways to use :ref:`multiple email backends `. + + +.. _sending-html: + +HTML email +---------- + +To send an HTML message, you can simply use Django's :func:`~django.core.mail.send_mail` +function with the ``html_message`` parameter: + + .. code-block:: python + + from django.core.mail import send_mail + + send_mail("Subject", "text body", "from@example.com", + ["to@example.com"], html_message="html body") + +However, many Django email capabilities -- and additional Anymail features -- +are only available when working with an :class:`~django.core.mail.EmailMultiAlternatives` +object. Use its :meth:`~django.core.mail.EmailMultiAlternatives.attach_alternative` +method to send HTML: + + .. code-block:: python + + from django.core.mail import EmailMultiAlternatives + + msg = EmailMultiAlternatives("Subject", "text body", + "from@example.com", ["to@example.com"]) + msg.attach_alternative("html body", "text/html") + # you can set any other options on msg here, then... + msg.send() + +It's good practice to send equivalent content in your plain-text body +and the html version. + + +.. _sending-attachments: + +Attachments +----------- + +Anymail will send a message's attachments to your ESP. You can add attachments +with the :meth:`~django.core.mail.EmailMessage.attach` or +:meth:`~django.core.mail.EmailMessage.attach_file` methods +of Django's :class:`~django.core.mail.EmailMessage`. + +Note that some ESPs impose limits on the size and type of attachments they +will send. + +.. rubric:: Inline images + +If your message has any image attachments with :mailheader:`Content-ID` headers, +Anymail will tell your ESP to treat them as inline images rather than ordinary +attached files. + +You can construct an inline image attachment yourself with Python's +:class:`python:email.mime.image.MIMEImage`, or you can use the convenience +function :func:`~message.attach_inline_image` included with +Anymail. See :ref:`inline-images` in the "Anymail additions" section. + + +.. _message-headers: + +Additional headers +------------------ + +Anymail passes additional headers to your ESP. (Some ESPs may limit +which headers they'll allow.) + + .. code-block:: python + + msg = EmailMessage( ... + headers={ + "List-Unsubscribe": unsubscribe_url, + "X-Example-Header": "myapp", + } + ) + + +.. _unsupported-features: + +Unsupported features +-------------------- + +Some email capabilities aren't supported by all ESPs. When you try to send a +message using features Anymail can't communicate to the current ESP, you'll get an +:exc:`~exceptions.AnymailUnsupportedFeature` error, and the message won't be sent. + +For example, very few ESPs support alternative message parts added with +:meth:`~django.core.mail.EmailMultiAlternatives.attach_alternative` +(other than a single :mimetype:`text/html` part that becomes the HTML body). +If you try to send a message with other alternative parts, Anymail will +raise :exc:`~exceptions.AnymailUnsupportedFeature`. + +.. setting:: ANYMAIL_UNSUPPORTED_FEATURE_ERRORS + +If you'd like to silently ignore :exc:`~exceptions.AnymailUnsupportedFeature` +errors and send the messages anyway, set :setting:`!ANYMAIL_UNSUPPORTED_FEATURE_ERRORS` +to `False` in your settings.py: + + .. code-block:: python + + ANYMAIL = { + ... + "UNSUPPORTED_FEATURE_ERRORS": False, + } + + +.. _recipients-refused: + +Refused recipients +------------------ + +If *all* recipients (to, cc, bcc) of a message are invalid or rejected by +your ESP *at send time,* the send call will raise an +:exc:`~exceptions.AnymailRecipientsRefused` error. + +You can examine the message's :attr:`~message.AnymailMessage.anymail_status` +attribute to determine the cause of the error. (See :ref:`esp-send-status`.) + +If a single message is sent to multiple recipients, and *any* recipient is valid +(or the message is queued by your ESP because of rate limiting or +:attr:`~message.AnymailMessage.send_at`), then this exception will not be raised. +You can still examine the message's :attr:`~message.AnymailMessage.anymail_status` +property after the send to determine the status of each recipient. + +You can disable this exception by setting :setting:`ANYMAIL_IGNORE_RECIPIENT_STATUS` +to `True` in your settings.py, which will cause Anymail to treat any non-API-error response +from your ESP as a successful send. + +.. note:: + + Many ESPs don't check recipient status during the send API call. For example, + Mailgun always queues sent messages, so you'll never catch + :exc:`AnymailRecipientsRefused` with the Mailgun backend. + + For those ESPs, use Anymail's :ref:`delivery event tracking ` + if you need to be notified of sends to blacklisted or invalid emails. diff --git a/docs/sending/exceptions.rst b/docs/sending/exceptions.rst new file mode 100644 index 0000000..d38f1a7 --- /dev/null +++ b/docs/sending/exceptions.rst @@ -0,0 +1,50 @@ +.. _anymail-exceptions: + +Exceptions +---------- + +.. module:: anymail.exceptions + +.. exception:: AnymailUnsupportedFeature + + If the email tries to use features that aren't supported by the ESP, the send + call will raise an :exc:`!AnymailUnsupportedFeature` error (a subclass + of :exc:`ValueError`), and the message won't be sent. + + You can disable this exception (ignoring the unsupported features and + sending the message anyway, without them) by setting + :setting:`ANYMAIL_UNSUPPORTED_FEATURE_ERRORS` to ``False``. + + +.. exception:: AnymailRecipientsRefused + + Raised when *all* recipients (to, cc, bcc) of a message are invalid or rejected by + your ESP *at send time.* See :ref:`recipients-refused`. + + You can disable this exception by setting :setting:`ANYMAIL_IGNORE_RECIPIENT_STATUS` + to `True` in your settings.py, which will cause Anymail to treat any + non-:exc:`AnymailAPIError` response from your ESP as a successful send. + + +.. exception:: AnymailAPIError + + If the ESP's API fails or returns an error response, the send call will + raise an :exc:`!AnymailAPIError`. + + The exception's :attr:`status_code` and :attr:`response` attributes may + help explain what went wrong. (Tip: you may also be able to check the API log in + your ESP's dashboard. See :ref:`troubleshooting`.) + + +.. exception:: AnymailSerializationError + + The send call will raise a :exc:`!AnymailSerializationError` + if there are message attributes Anymail doesn't know how to represent + to your ESP. + + The most common cause of this error is including values other than + strings and numbers in your :attr:`merge_data` or :attr:`metadata`. + (E.g., you need to format `Decimal` and `date` data to + strings before setting them into :attr:`merge_data`.) + + See :ref:`formatting-merge-data` for more information. diff --git a/docs/sending/index.rst b/docs/sending/index.rst new file mode 100644 index 0000000..9243446 --- /dev/null +++ b/docs/sending/index.rst @@ -0,0 +1,13 @@ +.. _sending-email: + +Sending email +------------- + +.. toctree:: + :maxdepth: 2 + + django_email + anymail_additions + templates + tracking + exceptions diff --git a/docs/sending/templates.rst b/docs/sending/templates.rst new file mode 100644 index 0000000..f50071c --- /dev/null +++ b/docs/sending/templates.rst @@ -0,0 +1,146 @@ +.. _merge-vars: + +Mail merge and ESP templates +============================ + +Anymail has some features to simplify using your ESP's email +templates and merge-variable features in a portable way. + +However, ESP templating languages are generally proprietary, +which makes them inherently non-portable. Although Anymail +can normalize the Django code you write to supply merge +variables to your ESP, it can't help you avoid needing +to rewrite your email templates if you switch ESPs. + +:ref:`Using Django templates ` can be a +better, portable and maintainable option. + + +.. note:: + + Normalized merge variables and template identification + are coming to Anymail soon. + + +.. currentmodule:: anymail.message + +.. _esp-templates: + +ESP templates +------------- + +.. To use a *Mandrill* (MailChimp) template stored in your Mandrill account, +.. set a :attr:`template_name` and (optionally) :attr:`template_content` +.. on your :class:`~django.core.mail.EmailMessage` object:: +.. +.. from django.core.mail import EmailMessage +.. +.. msg = EmailMessage(subject="Shipped!", from_email="store@example.com", +.. to=["customer@example.com", "accounting@example.com"]) +.. msg.template_name = "SHIPPING_NOTICE" # A Mandrill template name +.. msg.template_content = { # Content blocks to fill in +.. 'TRACKING_BLOCK': "track it" +.. } +.. msg.global_merge_vars = { # Merge tags in your template +.. 'ORDERNO': "12345", 'TRACKINGNO': "1Z987" +.. } +.. msg.merge_vars = { # Per-recipient merge tags +.. 'accounting@example.com': {'NAME': "Pat"}, +.. 'customer@example.com': {'NAME': "Kim"} +.. } +.. msg.send() +.. +.. If :attr:`template_name` is set, Djrill will use Mandrill's +.. `messages/send-template API `_, +.. and will ignore any `body` text set on the `EmailMessage`. +.. +.. All of Djrill's other :ref:`Mandrill-specific options ` +.. can be used with templates. + + +.. attribute:: AnymailMessage.template_name + + +.. attribute:: AnymailMessage.global_merge_vars + +.. ``dict``: merge variables to use for all recipients (most useful with :ref:`mandrill-templates`). :: +.. +.. message.global_merge_vars = {'company': "ACME", 'offer': "10% off"} +.. +.. Merge data must be strings or other JSON-serializable types. +.. (See :ref:`formatting-merge-data` for details.) + +.. attribute:: AnymailMessage.merge_vars + +.. ``dict``: per-recipient merge variables (most useful with :ref:`mandrill-templates`). The keys +.. in the dict are the recipient email addresses, and the values are dicts of merge vars for +.. each recipient:: +.. +.. message.merge_vars = { +.. 'wiley@example.com': {'offer': "15% off anvils"}, +.. 'rr@example.com': {'offer': "instant tunnel paint"} +.. } +.. +.. Merge data must be strings or other JSON-serializable types. +.. (See :ref:`formatting-merge-data` for details.) + + +.. _formatting-merge-data: + +Formatting merge data +~~~~~~~~~~~~~~~~~~~~~ + +If you're using a `date`, `datetime`, `Decimal`, or anything other +than strings and integers, +you'll need to format them into strings for use as merge data:: + + product = Product.objects.get(123) # A Django model + total_cost = Decimal('19.99') + ship_date = date(2015, 11, 18) + + # Won't work -- you'll get "not JSON serializable" exceptions: + msg.global_merge_vars = { + 'PRODUCT': product, + 'TOTAL_COST': total_cost, + 'SHIP_DATE': ship_date + } + + # Do something this instead: + msg.global_merge_vars = { + 'PRODUCT': product.name, # assuming name is a CharField + 'TOTAL_COST': "%.2f" % total_cost, + 'SHIP_DATE': ship_date.strftime('%B %d, %Y') # US-style "March 15, 2015" + } + +These are just examples. You'll need to determine the best way to format +your merge data as strings. + +Although floats are allowed in merge vars, you'll generally want to format them +into strings yourself to avoid surprises with floating-point precision. + +Anymail will raise :exc:`~anymail.exceptions.AnymailSerializationError` if you attempt +to send a message with non-json-serializable data. + + +.. How To Use Default Mandrill Subject and From fields +.. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. +.. To use default Mandrill "subject" or "from" field from your template definition +.. (overriding your EmailMessage and Django defaults), set the following attrs: +.. :attr:`use_template_subject` and/or :attr:`use_template_from` on +.. your :class:`~django.core.mail.EmailMessage` object:: +.. +.. msg.use_template_subject = True +.. msg.use_template_from = True +.. msg.send() +.. +.. .. attribute:: use_template_subject +.. +.. If `True`, Djrill will omit the subject, and Mandrill will +.. use the default subject from the template. +.. +.. .. attribute:: use_template_from +.. +.. If `True`, Djrill will omit the "from" field, and Mandrill will +.. use the default "from" from the template. + diff --git a/docs/sending/tracking.rst b/docs/sending/tracking.rst new file mode 100644 index 0000000..d1f25f6 --- /dev/null +++ b/docs/sending/tracking.rst @@ -0,0 +1,178 @@ +.. _event-tracking: + +Tracking sent mail status +========================= + +.. note:: + + Normalized event-tracking webhooks and signals are coming + to Anymail soon. + + +.. `Mandrill webhooks`_ are used for notification about outbound messages +.. (bounces, clicks, etc.), and also for delivering inbound email +.. processed through Mandrill. +.. +.. Djrill includes optional support for Mandrill's webhook notifications. +.. If enabled, it will send a Django signal for each event in a webhook. +.. Your code can connect to this signal for further processing. + +.. _Mandrill webhooks: http://help.mandrill.com/entries/21738186-Introduction-to-Webhooks + +.. _webhooks-config: + +Configuring tracking webhooks +----------------------------- + +.. warning:: Webhook Security + + Webhooks are ordinary urls---they're wide open to the internet. + You must take steps to secure webhooks, or anyone could submit + random (or malicious) data to your app simply by invoking your + webhook URL. For security: + + * Your webhook should only be accessible over SSL (https). + (This is beyond the scope of Anymail.) + + * Your webhook must include a random, secret key, known only to your + app and your ESP. Anymail will verify calls to your webhook, and will + reject calls without the correct key. + +.. * You can, optionally include the two settings :setting:`DJRILL_WEBHOOK_SIGNATURE_KEY` +.. and :setting:`DJRILL_WEBHOOK_URL` to enforce `webhook signature`_ checking + +.. _webhook signature: http://help.mandrill.com/entries/23704122-Authenticating-webhook-requests + + +.. To enable Djrill webhook processing you need to create and set a webhook +.. secret in your project settings, include the Djrill url routing, and +.. then add the webhook in the Mandrill control panel. +.. +.. 1. In your project's :file:`settings.py`, add a :setting:`DJRILL_WEBHOOK_SECRET`: +.. +.. .. code-block:: python +.. +.. DJRILL_WEBHOOK_SECRET = "" +.. +.. substituting a secret you've generated just for Mandrill webhooks. +.. (Do *not* use your Mandrill API key or Django SECRET_KEY for this!) +.. +.. An easy way to generate a random secret is to run the command below in a shell: +.. +.. .. code-block:: console +.. +.. $ python -c "from django.utils import crypto; print crypto.get_random_string(16)" +.. +.. +.. 2. In your base :file:`urls.py`, add routing for the Djrill urls: +.. +.. .. code-block:: python +.. +.. urlpatterns = patterns('', +.. ... +.. url(r'^djrill/', include(djrill.urls)), +.. ) +.. +.. +.. 3. Now you need to tell Mandrill about your webhook: +.. +.. * For receiving events on sent messages (e.g., bounces or clickthroughs), +.. you'll do this in Mandrill's `webhooks control panel`_. +.. * For setting up inbound email through Mandrill, you'll add your webhook +.. to Mandrill's `inbound settings`_ under "Routes" for your domain. +.. * And if you want both, you'll need to add the webhook in both places. +.. +.. In all cases, the "Post to URL" is +.. :samp:`{https://yoursite.example.com}/djrill/webhook/?secret={your-secret}` +.. substituting your app's own domain, and changing *your-secret* to the secret +.. you created in step 1. +.. +.. (For sent-message webhooks, don't forget to tick the "Trigger on Events" +.. checkboxes for the events you want to receive.) +.. +.. +.. Once you've completed these steps and your Django app is live on your site, +.. you can use the Mandrill "Test" commands to verify your webhook configuration. +.. Then see the next section for setting up Django signal handlers to process +.. the webhooks. +.. +.. Incidentally, you have some control over the webhook url. +.. If you'd like to change the "djrill" prefix, that comes from +.. the url config in step 2. And if you'd like to change +.. the *name* of the "secret" query string parameter, you can set +.. :setting:`DJRILL_WEBHOOK_SECRET_NAME` in your :file:`settings.py`. +.. +.. For extra security, Mandrill provides a signature in the request header +.. X-Mandrill-Signature. If you want to verify this signature, you need to provide +.. the settings :setting:`DJRILL_WEBHOOK_SIGNATURE_KEY` with the webhook-specific +.. signature key that can be found in the Mandrill admin panel and +.. :setting:`DJRILL_WEBHOOK_URL` where you should enter the exact URL, including +.. that you entered in Mandrill when creating the webhook. + +.. _webhooks control panel: https://mandrillapp.com/settings/webhooks +.. _inbound settings: https://mandrillapp.com/inbound + + +.. _webhook-usage: + +Tracking event signals +---------------------- + +.. Once you've enabled webhooks, Djrill will send a ``djrill.signals.webhook_event`` +.. custom `Django signal`_ for each Mandrill event it receives. +.. You can connect your own receiver function to this signal for further processing. +.. +.. Be sure to read Django's `listening to signals`_ docs for information on defining +.. and connecting signal receivers. +.. +.. Examples: +.. +.. .. code-block:: python +.. +.. from djrill.signals import webhook_event +.. from django.dispatch import receiver +.. +.. @receiver(webhook_event) +.. def handle_bounce(sender, event_type, data, **kwargs): +.. if event_type == 'hard_bounce' or event_type == 'soft_bounce': +.. print "Message to %s bounced: %s" % ( +.. data['msg']['email'], +.. data['msg']['bounce_description'] +.. ) +.. +.. @receiver(webhook_event) +.. def handle_inbound(sender, event_type, data, **kwargs): +.. if event_type == 'inbound': +.. print "Inbound message from %s: %s" % ( +.. data['msg']['from_email'], +.. data['msg']['subject'] +.. ) +.. +.. @receiver(webhook_event) +.. def handle_whitelist_sync(sender, event_type, data, **kwargs): +.. if event_type == 'whitelist_add' or event_type == 'whitelist_remove': +.. print "Rejection whitelist update: %s email %s (%s)" % ( +.. data['action'], +.. data['reject']['email'], +.. data['reject']['reason'] +.. ) +.. +.. +.. Note that your webhook_event signal handlers will be called for all Mandrill +.. webhook callbacks, so you should always check the `event_type` param as shown +.. in the examples above to ensure you're processing the expected events. +.. +.. Mandrill batches up multiple events into a single webhook call. +.. Djrill will invoke your signal handler once for each event in the batch. +.. +.. The available fields in the `data` param are described in Mandrill's documentation: +.. `sent-message webhooks`_, `inbound webhooks`_, and `whitelist/blacklist sync webooks`_. + +.. _Django signal: https://docs.djangoproject.com/en/stable/topics/signals/ +.. _inbound webhooks: + http://help.mandrill.com/entries/22092308-What-is-the-format-of-inbound-email-webhooks- +.. _listening to signals: + https://docs.djangoproject.com/en/stable/topics/signals/#listening-to-signals +.. _sent-message webhooks: http://help.mandrill.com/entries/21738186-Introduction-to-Webhooks +.. _whitelist/blacklist sync webooks: + https://mandrill.zendesk.com/hc/en-us/articles/205583297-Sync-Event-Webhook-format diff --git a/docs/tips/django_templates.rst b/docs/tips/django_templates.rst new file mode 100644 index 0000000..285e47d --- /dev/null +++ b/docs/tips/django_templates.rst @@ -0,0 +1,55 @@ +.. _django-templates: + +Using Django templates for email +================================ + +ESP's templating languages and merge capabilities are generally not compatible +with each other, which can make it hard to move email templates between them. + +But since you're working in Django, you already have access to the +extremely-full-featured :mod:`Django templating system `. +You don't even have to use Django's template syntax: it supports other +template languages (like Jinja2). + +You're probably already using Django's templating system for your HTML pages, +so it can be an easy decision to use it for your email, too. + +To compose email using *Django* templates, you can use Django's +:func:`~django.template.loaders.django.template.loader.render_to_string` +template shortcut to build the body and html. + +Example that builds an email from the templates ``message_subject.txt``, +``message_body.txt`` and ``message_body.html``: + +.. code-block:: python + + from django.core.mail import EmailMultiAlternatives + from django.template import Context + from django.template.loader import render_to_string + + template_data = { + 'ORDERNO': "12345", 'TRACKINGNO': "1Z987" + } + + plaintext_context = Context(autoescape=False) # HTML escaping not appropriate in plaintext + subject = render_to_string("message_subject.txt", template_data, plaintext_context) + text_body = render_to_string("message_body.txt", template_data, plaintext_context) + html_body = render_to_string("message_body.html", template_data) + + msg = EmailMultiAlternatives(subject=subject, from_email="store@example.com", + to=["customer@example.com"], body=text_body) + msg.attach_alternative(html_body, "text/html") + msg.send() + + +Helpful add-ons +--------------- + +These (third-party) packages can be helpful for building your email +in Django: + +.. TODO: flesh this out + +* django-templated-mail +* Premailer, for inlining css +* BeautifulSoup, lxml, or html2text, for auto-generating plaintext from your html diff --git a/docs/tips/index.rst b/docs/tips/index.rst new file mode 100644 index 0000000..05e00d6 --- /dev/null +++ b/docs/tips/index.rst @@ -0,0 +1,16 @@ +Tips, tricks, and advanced usage +-------------------------------- + +Some suggestions and recipes for getting things +done with Anymail: + +.. toctree:: + :maxdepth: 1 + + multiple_backends + django_templates + +.. TODO: +.. Working with django-mailer(2) +.. Sharing backend connections (sessions) + diff --git a/docs/usage/multiple_backends.rst b/docs/tips/multiple_backends.rst similarity index 63% rename from docs/usage/multiple_backends.rst rename to docs/tips/multiple_backends.rst index 83c96de..b54e945 100644 --- a/docs/usage/multiple_backends.rst +++ b/docs/tips/multiple_backends.rst @@ -1,30 +1,35 @@ .. _multiple-backends: -Mixing Email Backends +Mixing email backends ===================== Since you are replacing Django's global :setting:`EMAIL_BACKEND`, by default -Djrill will handle all outgoing mail, sending everything through Mandrill. +Anymail will handle **all** outgoing mail, sending everything through your ESP. You can use Django mail's optional :func:`connection ` -argument to send some mail through Mandrill and others through a different system. +argument to send some mail through your ESP and others through a different system. -This could be useful, for example, to deliver customer emails with Mandrill, +This could be useful, for example, to deliver customer emails with the ESP, but send admin emails directly through an SMTP server: .. code-block:: python - :emphasize-lines: 8,10 + :emphasize-lines: 8,10,13,15 from django.core.mail import send_mail, get_connection # send_mail connection defaults to the settings EMAIL_BACKEND, which - # we've set to DjrillBackend. This will be sent using Mandrill: + # we've set to Anymail's MailgunBackend. This will be sent using Mailgun: send_mail("Thanks", "We sent your order", "sales@example.com", ["customer@example.com"]) # Get a connection to an SMTP backend, and send using that instead: smtp_backend = get_connection('django.core.mail.backends.smtp.EmailBackend') send_mail("Uh-Oh", "Need your attention", "admin@example.com", ["alert@example.com"], - connection=smtp_backend) + connection=smtp_backend) + + # You can even use multiple Anymail backends in the same app: + sendgrid_backend = get_connection('anymail.backends.sendgrid.SendGridBackend') + send_mail("Password reset", "Here you go", "user@example.com", ["noreply@example.com"], + connection=sendgrid_backend) You can supply a different connection to Django's :func:`~django.core.mail.send_mail` and :func:`~django.core.mail.send_mass_mail` helpers, @@ -32,7 +37,8 @@ and in the constructor for an :class:`~django.core.mail.EmailMessage` or :class:`~django.core.mail.EmailMultiAlternatives`. -(See the `django.utils.log.AdminEmailHandler`_ docs for more information on Django's admin error logging.) +(See the :class:`django.utils.log.AdminEmailHandler` docs for more information +on Django's admin error logging.) .. _django.utils.log.AdminEmailHandler: https://docs.djangoproject.com/en/stable/topics/logging/#django.utils.log.AdminEmailHandler diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index dec4fe4..cc8e35d 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -3,68 +3,60 @@ Troubleshooting =============== -Djrill throwing errors? Not sending what you want? Here are some tips... +Anymail throwing errors? Not sending what you want? Here are some tips... -Figuring Out What's Wrong +Figuring out what's wrong ------------------------- -* **Check the error message:** Look for a Mandrill error message in your +**Check the error message** + + Look for an Anymail error message in your web browser or console (running Django in dev mode) or in your server - error logs. As of v1.4, Djrill reports the detailed Mandrill error when - something goes wrong. And when the error is something like "invalid API key" + error logs. If you see something like "invalid API key" or "invalid email address", that's probably 90% of what you'll need to know to solve the problem. -* **Check the Mandrill API logs:** The Mandrill dashboard includes an - *incredibly-helpful* list of your `recent API calls`_ -- and you can click - into each one to see the full request and response. Check to see if the - data you thought you were sending actually made it into the request, and - if Mandrill has any complaints in the response. +**Check your ESPs API logs** -* **Double-check common issues:** + Many ESPs offer an incredibly-helpful log + of your recent API calls in their dashboards. Check the logs to see if the + data you thought you were sending actually made it to your ESP, and + if they recorded any errors there. - * Did you set your :setting:`MANDRILL_API_KEY` in settings.py? - * Did you add ``'djrill'`` to the list of :setting:`INSTALLED_APPS` in settings.py? +**Double-check common issues** + + * Did you install Anymail with the ESPs you want available? + (E.g., `pip install anymail[mailgun,sendgrid]` -- *not* just `pip install anymail`.) + * Did you add any required settings for those ESPs to your settings.py? + (E.g., `ANYMAIL_MANDRILL_API_KEY`.) + * Did you add ``'anymail'`` to the list of :setting:`INSTALLED_APPS` in settings.py? * Are you using a valid from address? Django's default is "webmaster@localhost", which won't cut it. Either specify the ``from_email`` explicitly on every message - you send through Djrill, or add :setting:`DEFAULT_FROM_EMAIL` to your settings.py. + you send through Anymail, or add :setting:`DEFAULT_FROM_EMAIL` to your settings.py. -* **Try it without Djrill:** Try switching your :setting:`EMAIL_BACKEND` - setting to Django's `File backend`_ and then running your email-sending - code again. If that causes errors, you'll know the issue is somewhere - other than Djrill. And you can look through the :setting:`EMAIL_FILE_PATH` +**Try it without Anymail** + + Try switching your :setting:`EMAIL_BACKEND` setting to + Django's :ref:`File backend ` and then running your + email-sending code again. If that causes errors, you'll know the issue is somewhere + other than Anymail. And you can look through the :setting:`EMAIL_FILE_PATH` file contents afterward to see if you're generating the email you want. -.. _recent API calls: https://mandrillapp.com/settings/api -.. _File backend: https://docs.djangoproject.com/en/stable/topics/email/#file-backend - - -Getting Help +Getting help ------------ If you've gone through the suggestions above and still aren't sure what's wrong, -the Djrill community is happy to help. Djrill is supported and maintained by the -people who use it -- like you! (We're not Mandrill employees.) +the Anymail community is happy to help. Anymail is supported and maintained by the +people who use it -- like you! (We're not employees of any ESP.) -You can ask in either of these places (but please pick only one per question!): +For questions or problems with Anymail, you can open a `GitHub issue`_. +(And if you've found a bug, you're welcome to :ref:`contribute ` a fix!) -Ask on `StackOverflow`_ - Tag your question with **both** ``Django`` and ``Mandrill`` to get our attention. - Bonus: a lot of questions about Djrill are actually questions about Django - itself, so by asking on StackOverflow you'll also get the benefit of the - thousands of Django experts there. - -Open a `GitHub issue`_ - We do our best to answer questions in GitHub issues. And if you've found - a Djrill bug, that's definitely the place to report it. (Or even fix it -- - see :ref:`contributing`.) - -Wherever you ask, it's always helpful to include the relevant portions of your -code, the text of any error messages, and any exception stack traces in your -question. +Whenever you open an issue, it's always helpful to mention which ESP you're using, +include the relevant portions of your code and settings, the text of any error messages, +and any exception stack traces. -.. _StackOverflow: http://stackoverflow.com/questions/tagged/django+mandrill -.. _GitHub issue: https://github.com/brack3t/Djrill/issues +.. _GitHub issue: https://github.com/anymail/django-anymail/issues diff --git a/docs/upgrading.rst b/docs/upgrading.rst deleted file mode 100644 index ce03b85..0000000 --- a/docs/upgrading.rst +++ /dev/null @@ -1,121 +0,0 @@ -.. _upgrading: - -Upgrading from 1.x -================== - -Djrill 2.0 includes some breaking changes from 1.x. -These changes should have minimal (or no) impact on most Djrill users, -but if you are upgrading please review the major topics below -to see if they apply to you. - -Djrill 1.4 tried to warn you if you were using Djrill features -expected to change in 2.0. If you are seeing any deprecation warnings -with Djrill 1.4, you should fix them before upgrading to 2.0. -(Warnings appear in the console when running Django in debug mode.) - -Please see the :ref:`release notes ` for a list of new features -and improvements in Djrill 2.0. - - -Dropped support for Django 1.3, Python 2.6, and Python 3.2 ----------------------------------------------------------- - -Although Djrill may still work with these older configurations, -we no longer test against them. Djrill now requires Django 1.4 -or later and Python 2.7, 3.3, or 3.4. - -If you require support for these earlier versions, you should -not upgrade to Djrill 2.0. Djrill 1.4 remains available on -pypi, and will continue to receive security fixes. - - -Removed DjrillAdminSite ------------------------ - -Earlier versions of Djrill included a custom Django admin site. -The equivalent functionality is available in Mandrill's dashboard, -and Djrill 2.0 drops support for it. - -Although most Djrill users were unaware the admin site existed, -many did follow the earlier versions' instructions to enable it. - -If you have added DjrillAdminSite, you will need to remove it for Djrill 2.0. - -In your :file:`urls.py`: - - .. code-block:: python - - from djrill import DjrillAdminSite # REMOVE this - admin.site = DjrillAdminSite() # REMOVE this - - admin.autodiscover() # REMOVE this if you added it only for Djrill - -In your :file:`settings.py`: - - .. code-block:: python - - INSTALLED_APPS = ( - ... - # If you added SimpleAdminConfig only for Djrill: - 'django.contrib.admin.apps.SimpleAdminConfig', # REMOVE this - 'django.contrib.admin', # ADD this default back - ... - ) - -(These instructions assume you had changed to SimpleAdminConfig -solely for DjrillAdminSite. If you are using it for custom admin -sites with any other Django apps you use, you should leave it -SimpleAdminConfig in place, but still remove the references to -DjrillAdminSite.) - - -Added exception for invalid or rejected recipients --------------------------------------------------- - -Djrill 2.0 raises a new :exc:`djrill.MandrillRecipientsRefused` exception when -all recipients of a message are invalid or rejected by Mandrill. (This parallels -the behavior of Django's default :setting:`SMTP email backend `, -which raises :exc:`SMTPRecipientsRefused ` when -all recipients are refused.) - -Your email-sending code should handle this exception (along with other -exceptions that could occur during a send). However, if you want to retain the -Djrill 1.x behavior and treat invalid or rejected recipients as successful sends, -you can set :setting:`MANDRILL_IGNORE_RECIPIENT_STATUS` to ``True`` in your settings.py. - - -Other 2.0 breaking changes --------------------------- - -Code that will be affected by these changes is far less common than -for the changes listed above, but they may impact some uses: - -Removed unintended date-to-string conversion - If your code was inadvertently relying on Djrill to automatically - convert date or datetime values to strings in :attr:`merge_vars`, - :attr:`metadata`, or other Mandrill message attributes, you must - now explicitly do the string conversion yourself. - See :ref:`formatting-merge-data` for an explanation. - (Djrill 1.4 reported a DeprecationWarning for this case.) - - (This does not affect :attr:`send_at`, where Djrill specifically - allows date or datetime values.) - -Removed DjrillMessage class - The ``DjrillMessage`` class has not been needed since Djrill 0.2. - You should replace any uses of it with the standard - :class:`~django.core.mail.EmailMessage` class. - (Djrill 1.4 reported a DeprecationWarning for this case.) - -Removed DjrillBackendHTTPError - This exception was deprecated in Djrill 0.3. Replace uses of it - with :exc:`djrill.MandrillAPIError`. - (Djrill 1.4 reported a DeprecationWarning for this case.) - -Refactored Djrill backend and exceptions - Several internal details of ``djrill.mail.backends.DjrillBackend`` - and Djrill's exception classes have been significantly updated for 2.0. - The intent is to make it easier to maintain and extend the backend - (including creating your own subclasses to override Djrill's default - behavior). As a result, though, any existing code that depended on - undocumented Djrill internals may need to be updated. diff --git a/docs/usage/sending_mail.rst b/docs/usage/sending_mail.rst deleted file mode 100644 index cc57f6e..0000000 --- a/docs/usage/sending_mail.rst +++ /dev/null @@ -1,387 +0,0 @@ -Sending Mail -============ - -Djrill handles **all** outgoing email sent through Django's standard -:mod:`django.core.mail` package, including :func:`~django.core.mail.send_mail`, -:func:`~django.core.mail.send_mass_mail`, the :class:`~django.core.mail.EmailMessage` class, -and even :func:`~django.core.mail.mail_admins`. - -If you'd like to selectively send only some messages through Mandrill, -there is a way to :ref:`use multiple email backends `. - - -.. _django-send-support: - -Django Email Support --------------------- - -Djrill supports most of the functionality of Django's :class:`~django.core.mail.EmailMessage` -and :class:`~django.core.mail.EmailMultiAlternatives` classes. - -Some notes and limitations: - -**Display Names** - All email addresses (from, to, cc, bcc) can be simple - ("email\@example.com") or can include a display name - ("Real Name "). - -**CC and BCC Recipients** - Djrill properly identifies "cc" and "bcc" recipients to Mandrill. - - Note that you may need to set the Mandrill option :attr:`preserve_recipients` - to `!True` if you want recipients to be able to see who else was included - in the "to" list. - - -.. _sending-html: - -**HTML/Alternative Parts** - To include an HTML version of a message, use - :meth:`~django.core.mail.EmailMultiAlternatives.attach_alternative`: - - .. code-block:: python - - from django.core.mail import EmailMultiAlternatives - - msg = EmailMultiAlternatives("Subject", "text body", - "from@example.com", ["to@example.com"]) - msg.attach_alternative("html body", "text/html") - - Djrill allows a maximum of one - :meth:`~django.core.mail.EmailMultiAlternatives.attach_alternative` - on a message, and it must be ``mimetype="text/html"``. - Otherwise, Djrill will raise :exc:`~djrill.NotSupportedByMandrillError` when you - attempt to send the message. (Mandrill doesn't support sending multiple html - alternative parts, or any non-html alternatives.) - - -.. _sending-attachments: - -**Attachments** - Djrill will send a message's attachments. (Note that Mandrill may impose limits - on size and type of attachments.) - - Also, if an image attachment has a Content-ID header, Djrill will tell Mandrill - to treat that as an embedded image rather than an ordinary attachment. - (For an example, see :meth:`~DjrillBackendTests.test_embedded_images` - in :file:`tests/test_mandrill_send.py`.) - -.. _message-headers: - -**Headers** - Djrill accepts additional headers and passes them along to Mandrill: - - .. code-block:: python - - msg = EmailMessage( ... - headers={'Reply-To': "reply@example.com", 'List-Unsubscribe': "..."} - ) - - .. note:: - - Djrill also supports the `reply_to` param added to - :class:`~django.core.mail.EmailMessage` in Django 1.8. - (If you provide *both* a 'Reply-To' header and the `reply_to` param, - the header will take precedence.) - - -.. _mandrill-send-support: - -Mandrill-Specific Options -------------------------- - -Most of the options from the Mandrill -`messages/send API `_ -`message` struct can be set directly on an :class:`~django.core.mail.EmailMessage` -(or subclass) object. - -.. note:: - - You can set global defaults for common options with the - :setting:`MANDRILL_SETTINGS` setting, to avoid having to - set them on every message. - - -.. These attributes are in the same order as they appear in the Mandrill API docs... - -.. attribute:: important - - ``Boolean``: whether Mandrill should send this message ahead of non-important ones. - -.. attribute:: track_opens - - ``Boolean``: whether Mandrill should enable open-tracking for this message. - Default from your Mandrill account settings. :: - - message.track_opens = True - -.. attribute:: track_clicks - - ``Boolean``: whether Mandrill should enable click-tracking for this message. - Default from your Mandrill account settings. - - .. note:: - - Mandrill has an option to track clicks in HTML email but not plaintext, but - it's *only* available in your Mandrill account settings. If you want to use that - option, set it at Mandrill, and *don't* set the ``track_clicks`` attribute here. - -.. attribute:: auto_text - - ``Boolean``: whether Mandrill should automatically generate a text body from the HTML. - Default from your Mandrill account settings. - -.. attribute:: auto_html - - ``Boolean``: whether Mandrill should automatically generate an HTML body from the plaintext. - Default from your Mandrill account settings. - -.. attribute:: inline_css - - ``Boolean``: whether Mandrill should inline CSS styles in the HTML. - Default from your Mandrill account settings. - -.. attribute:: url_strip_qs - - ``Boolean``: whether Mandrill should ignore any query parameters when aggregating - URL tracking data. Default from your Mandrill account settings. - -.. attribute:: preserve_recipients - - ``Boolean``: whether Mandrill should include all recipients in the "to" message header. - Default from your Mandrill account settings. - -.. attribute:: view_content_link - - ``Boolean``: set False on sensitive messages to instruct Mandrill not to log the content. - -.. attribute:: tracking_domain - - ``str``: domain Mandrill should use to rewrite tracked links and host tracking pixels - for this message. Useful if you send email from multiple domains. - Default from your Mandrill account settings. - -.. attribute:: signing_domain - - ``str``: domain Mandrill should use for DKIM signing and SPF on this message. - Useful if you send email from multiple domains. - Default from your Mandrill account settings. - -.. attribute:: return_path_domain - - ``str``: domain Mandrill should use for the message's return-path. - -.. attribute:: merge_language - - ``str``: the merge tag language if using merge tags -- e.g., "mailchimp" or "handlebars". - Default from your Mandrill account settings. - -.. attribute:: global_merge_vars - - ``dict``: merge variables to use for all recipients (most useful with :ref:`mandrill-templates`). :: - - message.global_merge_vars = {'company': "ACME", 'offer': "10% off"} - - Merge data must be strings or other JSON-serializable types. - (See :ref:`formatting-merge-data` for details.) - -.. attribute:: merge_vars - - ``dict``: per-recipient merge variables (most useful with :ref:`mandrill-templates`). The keys - in the dict are the recipient email addresses, and the values are dicts of merge vars for - each recipient:: - - message.merge_vars = { - 'wiley@example.com': {'offer': "15% off anvils"}, - 'rr@example.com': {'offer': "instant tunnel paint"} - } - - Merge data must be strings or other JSON-serializable types. - (See :ref:`formatting-merge-data` for details.) - -.. attribute:: tags - - ``list`` of ``str``: tags to apply to the message, for filtering reports in the Mandrill - dashboard. (Note that Mandrill prohibits tags longer than 50 characters or starting with - underscores.) :: - - message.tags = ["Order Confirmation", "Test Variant A"] - -.. attribute:: subaccount - - ``str``: the ID of one of your subaccounts to use for sending this message. - -.. attribute:: google_analytics_domains - - ``list`` of ``str``: domain names for links where Mandrill should add Google Analytics - tracking parameters. :: - - message.google_analytics_domains = ["example.com"] - -.. attribute:: google_analytics_campaign - - ``str`` or ``list`` of ``str``: the utm_campaign tracking parameter to attach to links - when adding Google Analytics tracking. (Mandrill defaults to the message's from_email as - the campaign name.) - -.. attribute:: metadata - - ``dict``: metadata values Mandrill should store with the message for later search and - retrieval. :: - - message.metadata = {'customer': customer.id, 'order': order.reference_number} - - Mandrill restricts metadata keys to alphanumeric characters and underscore, and - metadata values to numbers, strings, boolean values, and None (null). - -.. attribute:: recipient_metadata - - ``dict``: per-recipient metadata values. Keys are the recipient email addresses, - and values are dicts of metadata for each recipient (similar to - :attr:`merge_vars`) - - Mandrill restricts metadata keys to alphanumeric characters and underscore, and - metadata values to numbers, strings, boolean values, and None (null). - -.. attribute:: async - - ``Boolean``: whether Mandrill should use an async mode optimized for bulk sending. - -.. attribute:: ip_pool - - ``str``: name of one of your Mandrill dedicated IP pools to use for sending this message. - -.. attribute:: send_at - - `datetime` or `date` or ``str``: instructs Mandrill to delay sending this message - until the specified time. Example:: - - msg.send_at = datetime.utcnow() + timedelta(hours=1) - - Mandrill requires a UTC string in the form ``YYYY-MM-DD HH:MM:SS``. - Djrill will convert python dates and datetimes to this form. - (Dates will be given a time of 00:00:00.) - - .. note:: Timezones - - Mandrill assumes :attr:`!send_at` is in the UTC timezone, - which is likely *not* the same as your local time. - - Djrill will convert timezone-*aware* datetimes to UTC for you. - But if you format your own string, supply a date, or a - *naive* datetime, you must make sure it is in UTC. - See the python `datetime` docs for more information. - - For example, ``msg.send_at = datetime.now() + timedelta(hours=1)`` - will try to schedule the message for an hour from the current time, - but *interpreted in the UTC timezone* (which isn't what you want). - If you're more than an hour west of the prime meridian, that will - be in the past (and the message will get sent immediately). If - you're east of there, the message might get sent quite a bit later - than you intended. One solution is to use `utcnow` as shown in - the earlier example. - - .. note:: - - Scheduled sending is a paid Mandrill feature. If you are using - a free Mandrill account, :attr:`!send_at` won't work. - - -All the Mandrill-specific attributes listed above work with *any* -:class:`~django.core.mail.EmailMessage`-derived object, so you can use them with -many other apps that add Django mail functionality. - -If you have questions about the python syntax for any of these properties, -see :class:`DjrillMandrillFeatureTests` in :file:`tests/test_mandrill_send.py` for examples. - - - -.. _mandrill-response: - -Response from Mandrill ----------------------- - -.. attribute:: mandrill_response - -Djrill adds a :attr:`!mandrill_response` attribute to each :class:`~django.core.mail.EmailMessage` -as it sends it. This allows you to retrieve message ids, initial status information and more. - -For an EmailMessage that is successfully sent to one or more email addresses, :attr:`!mandrill_response` will -be set to a ``list`` of ``dict``, where each entry has info for one email address. See the Mandrill docs for the -`messages/send API `_ for full details. - -For example, to get the Mandrill message id for a sent email you might do this:: - - msg = EmailMultiAlternatives(subject="subject", body="body", - from_email="sender@example.com",to=["someone@example.com"]) - msg.send() - response = msg.mandrill_response[0] - mandrill_id = response['_id'] - -For this example, msg.mandrill_response might look like this:: - - msg.mandrill_response = [ - { - "email": "someone@example.com", - "status": "sent", - "_id": "abc123abc123abc123abc123abc123" - } - ] - -If an error is returned by Mandrill while sending the message then :attr:`!mandrill_response` will be set to None. - - -.. _djrill-exceptions: - -Exceptions ----------- - -.. exception:: djrill.NotSupportedByMandrillError - - If the email tries to use features that aren't supported by Mandrill, the send - call will raise a :exc:`~!djrill.NotSupportedByMandrillError` exception (a subclass - of :exc:`ValueError`). - - -.. exception:: djrill.MandrillRecipientsRefused - - If *all* recipients (to, cc, bcc) of a message are invalid or rejected by Mandrill - (e.g., because they are your Mandrill blacklist), the send call will raise a - :exc:`~!djrill.MandrillRecipientsRefused` exception. - You can examine the message's :attr:`mandrill_response` attribute - to determine the cause of the error. - - If a single message is sent to multiple recipients, and *any* recipient is valid - (or the message is queued by Mandrill because of rate limiting or :attr:`send_at`), then - this exception will not be raised. You can still examine the mandrill_response - property after the send to determine the status of each recipient. - - You can disable this exception by setting :setting:`MANDRILL_IGNORE_RECIPIENT_STATUS` - to True in your settings.py, which will cause Djrill to treat any non-API-error response - from Mandrill as a successful send. - - .. versionadded:: 2.0 - Djrill 1.x behaved as if ``MANDRILL_IGNORE_RECIPIENT_STATUS = True``. - - -.. exception:: djrill.MandrillAPIError - - If the Mandrill API fails or returns an error response, the send call will - raise a :exc:`~!djrill.MandrillAPIError` exception (a subclass of :exc:`requests.HTTPError`). - The exception's :attr:`status_code` and :attr:`response` attributes may - help explain what went wrong. (Tip: you can also check Mandrill's - `API error log `_ to view the full API - request and error response.) - - -.. exception:: djrill.NotSerializableForMandrillError - - The send call will raise a :exc:`~!djrill.NotSerializableForMandrillError` exception - if the message has attached data which cannot be serialized to JSON for the Mandrill API. - - See :ref:`formatting-merge-data` for more information. - - .. versionadded:: 2.0 - Djrill 1.x raised a generic `TypeError` in this case. - :exc:`~!djrill.NotSerializableForMandrillError` is a subclass of `TypeError` - for compatibility with existing code. diff --git a/docs/usage/templates.rst b/docs/usage/templates.rst deleted file mode 100644 index ddc3f39..0000000 --- a/docs/usage/templates.rst +++ /dev/null @@ -1,131 +0,0 @@ -Sending Template Mail -===================== - -.. _mandrill-templates: - -Mandrill Templates ------------------- - -To use a *Mandrill* (MailChimp) template stored in your Mandrill account, -set a :attr:`template_name` and (optionally) :attr:`template_content` -on your :class:`~django.core.mail.EmailMessage` object:: - - from django.core.mail import EmailMessage - - msg = EmailMessage(subject="Shipped!", from_email="store@example.com", - to=["customer@example.com", "accounting@example.com"]) - msg.template_name = "SHIPPING_NOTICE" # A Mandrill template name - msg.template_content = { # Content blocks to fill in - 'TRACKING_BLOCK': "track it" - } - msg.global_merge_vars = { # Merge tags in your template - 'ORDERNO': "12345", 'TRACKINGNO': "1Z987" - } - msg.merge_vars = { # Per-recipient merge tags - 'accounting@example.com': {'NAME': "Pat"}, - 'customer@example.com': {'NAME': "Kim"} - } - msg.send() - -If :attr:`template_name` is set, Djrill will use Mandrill's -`messages/send-template API `_, -and will ignore any `body` text set on the `EmailMessage`. - -All of Djrill's other :ref:`Mandrill-specific options ` -can be used with templates. - - -.. _formatting-merge-data: - -Formatting Merge Data -~~~~~~~~~~~~~~~~~~~~~ - -If you're using dates, datetimes, Decimals, or anything other than strings and integers, -you'll need to format them into strings for use as merge data:: - - product = Product.objects.get(123) # A Django model - total_cost = Decimal('19.99') - ship_date = date(2015, 11, 18) - - # Won't work -- you'll get "not JSON serializable" exceptions: - msg.global_merge_vars = { - 'PRODUCT': product, - 'TOTAL_COST': total_cost, - 'SHIP_DATE': ship_date - } - - # Do something this instead: - msg.global_merge_vars = { - 'PRODUCT': product.name, # assuming name is a CharField - 'TOTAL_COST': "%.2f" % total_cost, - 'SHIP_DATE': ship_date.strftime('%B %d, %Y') # US-style "March 15, 2015" - } - -These are just examples. You'll need to determine the best way to format -your merge data as strings. - -Although floats are allowed in merge vars, you'll generally want to format them -into strings yourself to avoid surprises with floating-point precision. - -Technically, Djrill will accept anything serializable by the Python json package -- -which means advanced template users can include dicts and lists as merge vars -(for templates designed to handle objects and arrays). -See the Python :class:`json.JSONEncoder` docs for a list of allowable types. - -Djrill will raise :exc:`djrill.NotSerializableForMandrillError` if you attempt -to send a message with non-json-serializable data. - - -How To Use Default Mandrill Subject and From fields -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To use default Mandrill "subject" or "from" field from your template definition -(overriding your EmailMessage and Django defaults), set the following attrs: -:attr:`use_template_subject` and/or :attr:`use_template_from` on -your :class:`~django.core.mail.EmailMessage` object:: - - msg.use_template_subject = True - msg.use_template_from = True - msg.send() - -.. attribute:: use_template_subject - - If `True`, Djrill will omit the subject, and Mandrill will - use the default subject from the template. - -.. attribute:: use_template_from - - If `True`, Djrill will omit the "from" field, and Mandrill will - use the default "from" from the template. - - -.. _django-templates: - -Django Templates ----------------- - -To compose email using *Django* templates, you can use Django's -:func:`~django.template.loaders.django.template.loader.render_to_string` -template shortcut to build the body and html. - -Example that builds an email from the templates ``message_subject.txt``, -``message_body.txt`` and ``message_body.html``:: - - from django.core.mail import EmailMultiAlternatives - from django.template import Context - from django.template.loader import render_to_string - - template_data = { - 'ORDERNO': "12345", 'TRACKINGNO': "1Z987" - } - - plaintext_context = Context(autoescape=False) # HTML escaping not appropriate in plaintext - subject = render_to_string("message_subject.txt", template_data, plaintext_context) - text_body = render_to_string("message_body.txt", template_data, plaintext_context) - html_body = render_to_string("message_body.html", template_data) - - msg = EmailMultiAlternatives(subject=subject, from_email="store@example.com", - to=["customer@example.com"], body=text_body) - msg.attach_alternative(html_body, "text/html") - msg.send() - diff --git a/docs/usage/webhooks.rst b/docs/usage/webhooks.rst deleted file mode 100644 index 382f768..0000000 --- a/docs/usage/webhooks.rst +++ /dev/null @@ -1,172 +0,0 @@ -.. _webhooks: - -Mandrill Webhooks and Inbound Email -=================================== - -`Mandrill webhooks`_ are used for notification about outbound messages -(bounces, clicks, etc.), and also for delivering inbound email -processed through Mandrill. - -Djrill includes optional support for Mandrill's webhook notifications. -If enabled, it will send a Django signal for each event in a webhook. -Your code can connect to this signal for further processing. - -.. warning:: Webhook Security - - Webhooks are ordinary urls---they're wide open to the internet. - You must take steps to secure webhooks, or anyone could submit - random (or malicious) data to your app simply by invoking your - webhook URL. For security: - - * Your webhook should only be accessible over SSL (https). - (This is beyond the scope of Djrill.) - - * Your webhook must include a random, secret key, known only to your - app and Mandrill. Djrill will verify calls to your webhook, and will - reject calls without the correct key. - - * You can, optionally include the two settings :setting:`DJRILL_WEBHOOK_SIGNATURE_KEY` - and :setting:`DJRILL_WEBHOOK_URL` to enforce `webhook signature`_ checking - - -.. _Mandrill webhooks: http://help.mandrill.com/entries/21738186-Introduction-to-Webhooks -.. _securing webhooks: http://apidocs.mailchimp.com/webhooks/#securing-webhooks -.. _webhook signature: http://help.mandrill.com/entries/23704122-Authenticating-webhook-requests - -.. _webhooks-config: - -Configuration -------------- - -To enable Djrill webhook processing you need to create and set a webhook -secret in your project settings, include the Djrill url routing, and -then add the webhook in the Mandrill control panel. - -1. In your project's :file:`settings.py`, add a :setting:`DJRILL_WEBHOOK_SECRET`: - - .. code-block:: python - - DJRILL_WEBHOOK_SECRET = "" - - substituting a secret you've generated just for Mandrill webhooks. - (Do *not* use your Mandrill API key or Django SECRET_KEY for this!) - - An easy way to generate a random secret is to run the command below in a shell: - - .. code-block:: console - - $ python -c "from django.utils import crypto; print crypto.get_random_string(16)" - - -2. In your base :file:`urls.py`, add routing for the Djrill urls: - - .. code-block:: python - - urlpatterns = patterns('', - ... - url(r'^djrill/', include(djrill.urls)), - ) - - -3. Now you need to tell Mandrill about your webhook: - - * For receiving events on sent messages (e.g., bounces or clickthroughs), - you'll do this in Mandrill's `webhooks control panel`_. - * For setting up inbound email through Mandrill, you'll add your webhook - to Mandrill's `inbound settings`_ under "Routes" for your domain. - * And if you want both, you'll need to add the webhook in both places. - - In all cases, the "Post to URL" is - :samp:`{https://yoursite.example.com}/djrill/webhook/?secret={your-secret}` - substituting your app's own domain, and changing *your-secret* to the secret - you created in step 1. - - (For sent-message webhooks, don't forget to tick the "Trigger on Events" - checkboxes for the events you want to receive.) - - -Once you've completed these steps and your Django app is live on your site, -you can use the Mandrill "Test" commands to verify your webhook configuration. -Then see the next section for setting up Django signal handlers to process -the webhooks. - -Incidentally, you have some control over the webhook url. -If you'd like to change the "djrill" prefix, that comes from -the url config in step 2. And if you'd like to change -the *name* of the "secret" query string parameter, you can set -:setting:`DJRILL_WEBHOOK_SECRET_NAME` in your :file:`settings.py`. - -For extra security, Mandrill provides a signature in the request header -X-Mandrill-Signature. If you want to verify this signature, you need to provide -the settings :setting:`DJRILL_WEBHOOK_SIGNATURE_KEY` with the webhook-specific -signature key that can be found in the Mandrill admin panel and -:setting:`DJRILL_WEBHOOK_URL` where you should enter the exact URL, including -that you entered in Mandrill when creating the webhook. - -.. _webhooks control panel: https://mandrillapp.com/settings/webhooks -.. _inbound settings: https://mandrillapp.com/inbound - - -.. _webhook-usage: - -Webhook Notifications ---------------------- - -Once you've enabled webhooks, Djrill will send a ``djrill.signals.webhook_event`` -custom `Django signal`_ for each Mandrill event it receives. -You can connect your own receiver function to this signal for further processing. - -Be sure to read Django's `listening to signals`_ docs for information on defining -and connecting signal receivers. - -Examples: - -.. code-block:: python - - from djrill.signals import webhook_event - from django.dispatch import receiver - - @receiver(webhook_event) - def handle_bounce(sender, event_type, data, **kwargs): - if event_type == 'hard_bounce' or event_type == 'soft_bounce': - print "Message to %s bounced: %s" % ( - data['msg']['email'], - data['msg']['bounce_description'] - ) - - @receiver(webhook_event) - def handle_inbound(sender, event_type, data, **kwargs): - if event_type == 'inbound': - print "Inbound message from %s: %s" % ( - data['msg']['from_email'], - data['msg']['subject'] - ) - - @receiver(webhook_event) - def handle_whitelist_sync(sender, event_type, data, **kwargs): - if event_type == 'whitelist_add' or event_type == 'whitelist_remove': - print "Rejection whitelist update: %s email %s (%s)" % ( - data['action'], - data['reject']['email'], - data['reject']['reason'] - ) - - -Note that your webhook_event signal handlers will be called for all Mandrill -webhook callbacks, so you should always check the `event_type` param as shown -in the examples above to ensure you're processing the expected events. - -Mandrill batches up multiple events into a single webhook call. -Djrill will invoke your signal handler once for each event in the batch. - -The available fields in the `data` param are described in Mandrill's documentation: -`sent-message webhooks`_, `inbound webhooks`_, and `whitelist/blacklist sync webooks`_. - -.. _Django signal: https://docs.djangoproject.com/en/stable/topics/signals/ -.. _inbound webhooks: - http://help.mandrill.com/entries/22092308-What-is-the-format-of-inbound-email-webhooks- -.. _listening to signals: - https://docs.djangoproject.com/en/stable/topics/signals/#listening-to-signals -.. _sent-message webhooks: http://help.mandrill.com/entries/21738186-Introduction-to-Webhooks -.. _whitelist/blacklist sync webooks: - https://mandrill.zendesk.com/hc/en-us/articles/205583297-Sync-Event-Webhook-format