mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
Update (almost) all the docs
This commit is contained in:
102
README.rst
102
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:
|
.. This README is reused in multiple places:
|
||||||
* Github: project page, exactly as it appears here
|
* 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
|
.. This shared-intro section is also included in docs/index.rst
|
||||||
|
|
||||||
Djrill integrates the `Mandrill <http://mandrill.com>`_ transactional
|
Anymail integrates several transactional email service providers (ESPs) into Django,
|
||||||
email service into Django.
|
using a consistent API that makes it (relatively) easy to switch between ESPs.
|
||||||
|
|
||||||
**UPGRADING FROM DJRILL 1.x?**
|
It currently supports Mailgun and Mandrill. Postmark and SendGrid are coming soon.
|
||||||
There are some **breaking changes in Djrill 2.0**. Please see the
|
|
||||||
`upgrade instructions <http://djrill.readthedocs.org/en/latest/upgrading/>`_.
|
.. 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 <https://github.com/brack3t/Djrill>`_,
|
||||||
|
there are `notes on porting <https://anymail.readthedocs.org/en/latest/esps/mandrill/#migrating-from-djrill>`_
|
||||||
|
|
||||||
|
|
||||||
In general, Djrill "just works" with Django's built-in `django.core.mail`
|
Anymail normalizes ESP functionality so it "just works" with Django's
|
||||||
package. It includes:
|
built-in `django.core.mail` package. It includes:
|
||||||
|
|
||||||
* Support for HTML, attachments, extra headers, and other features of
|
* Support for HTML, attachments, extra headers, and other features of
|
||||||
`Django's built-in email <https://docs.djangoproject.com/en/stable/topics/email/>`_
|
`Django's built-in email <https://docs.djangoproject.com/en/stable/topics/email/>`_
|
||||||
* Mandrill-specific extensions like tags, metadata, tracking, and MailChimp templates
|
* Extensions that make it easy to use extra ESP functionality, like tags, metadata,
|
||||||
* Optional support for Mandrill inbound email and other webhook notifications,
|
and tracking, using code that's portable between ESPs
|
||||||
via Django signals
|
* 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
|
Anymail is released under the BSD license. It is tested against Django 1.8--1.9
|
||||||
(including Python 3 with Django 1.6+, and PyPy support with Django 1.5+).
|
(including Python 3 and PyPy).
|
||||||
Djrill uses `semantic versioning <http://semver.org/>`_.
|
Djrill uses `semantic versioning <http://semver.org/>`_.
|
||||||
|
|
||||||
.. END shared-intro
|
.. END shared-intro
|
||||||
|
|
||||||
.. image:: https://travis-ci.org/brack3t/Djrill.png?branch=master
|
.. image:: https://travis-ci.org/anymail/django-anymail.png?branch=master
|
||||||
:target: https://travis-ci.org/brack3t/Djrill
|
:target: https://travis-ci.org/anymail/django-anymail
|
||||||
:alt: build status on Travis-CI
|
:alt: build status on Travis-CI
|
||||||
|
|
||||||
|
|
||||||
**Resources**
|
**Resources**
|
||||||
|
|
||||||
* Full documentation: https://djrill.readthedocs.org/en/latest/
|
* Full documentation: https://anymail.readthedocs.org/en/latest/
|
||||||
* Package on PyPI: https://pypi.python.org/pypi/djrill
|
* Package on PyPI: https://pypi.python.org/pypi/django-anymail
|
||||||
* Project on Github: https://github.com/brack3t/Djrill
|
* Project on Github: https://github.com/anymail/django-anymail
|
||||||
|
|
||||||
|
|
||||||
Djrill 1-2-3
|
Anymail 1-2-3
|
||||||
------------
|
-------------
|
||||||
|
|
||||||
.. _quickstart:
|
.. _quickstart:
|
||||||
|
|
||||||
.. This quickstart section is also included in docs/quickstart.rst
|
.. 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
|
.. code-block:: console
|
||||||
|
|
||||||
$ pip install djrill
|
$ pip install anymail[mailgun] # or anymail[postmark,sendgrid] or ...
|
||||||
|
|
||||||
|
|
||||||
2. Edit your project's ``settings.py``:
|
2. Edit your project's ``settings.py``:
|
||||||
@@ -72,43 +82,53 @@ Djrill 1-2-3
|
|||||||
|
|
||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
...
|
...
|
||||||
"djrill"
|
"anymail"
|
||||||
)
|
)
|
||||||
|
|
||||||
MANDRILL_API_KEY = "<your Mandrill key>"
|
ANYMAIL = {
|
||||||
EMAIL_BACKEND = "djrill.mail.backends.djrill.DjrillBackend"
|
"MAILGUN_API_KEY": "<your Mailgun 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
|
DEFAULT_FROM_EMAIL = "you@example.com" # if you don't already have this in settings
|
||||||
|
|
||||||
|
|
||||||
3. Now the regular `Django email functions <https://docs.djangoproject.com/en/stable/topics/email/>`_
|
3. Now the regular `Django email functions <https://docs.djangoproject.com/en/stable/topics/email/>`_
|
||||||
will send through Mandrill:
|
will send through your chosen ESP:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
|
|
||||||
send_mail("It works!", "This will get sent through Mandrill",
|
send_mail("It works!", "This will get sent through Mailgun",
|
||||||
"Djrill Sender <djrill@example.com>", ["to@example.com"])
|
"Anymail Sender <from@example.com>", ["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
|
.. code-block:: python
|
||||||
|
|
||||||
from django.core.mail import EmailMultiAlternatives
|
from django.core.mail import EmailMultiAlternatives
|
||||||
|
from anymail.message import attach_inline_image
|
||||||
|
|
||||||
msg = EmailMultiAlternatives(
|
msg = EmailMultiAlternatives(
|
||||||
subject="Djrill Message",
|
subject="Please activate your account",
|
||||||
body="This is the text email body",
|
body="Click to activate your account: http://example.com/activate",
|
||||||
from_email="Djrill Sender <djrill@example.com>",
|
from_email="Example <admin@example.com>",
|
||||||
to=["Recipient One <someone@example.com>", "another.person@example.com"],
|
to=["New User <user1@example.com>", "account.manager@example.com"],
|
||||||
headers={'Reply-To': "Service <support@example.com>"} # optional extra headers
|
reply_to=["Helpdesk <support@example.com>"])
|
||||||
)
|
|
||||||
msg.attach_alternative("<p>This is the HTML email body</p>", "text/html")
|
|
||||||
|
|
||||||
# Optional Mandrill-specific extensions:
|
# Include an inline image in the html:
|
||||||
msg.tags = ["one tag", "two tag", "red tag", "blue tag"]
|
logo_cid = attach_inline_image(msg, open("logo.jpg", "rb").read())
|
||||||
msg.metadata = {'user_id': "8675309"}
|
html = """<img alt="Logo" src="cid:{logo_cid}">
|
||||||
|
<p>Please <a href="http://example.com/activate">activate</a>
|
||||||
|
your account</p>""".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:
|
# Send it:
|
||||||
msg.send()
|
msg.send()
|
||||||
@@ -116,5 +136,5 @@ Djrill 1-2-3
|
|||||||
.. END quickstart
|
.. END quickstart
|
||||||
|
|
||||||
|
|
||||||
See the `full documentation <https://djrill.readthedocs.org/en/latest/>`_
|
See the `full documentation <https://anymail.readthedocs.org/en/latest/>`_
|
||||||
for more features and options.
|
for more features and options.
|
||||||
|
|||||||
26
docs/conf.py
26
docs/conf.py
@@ -259,7 +259,7 @@ texinfo_documents = [
|
|||||||
# -- Options for Intersphinx ------------------------------------------------
|
# -- Options for Intersphinx ------------------------------------------------
|
||||||
|
|
||||||
intersphinx_mapping = {
|
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/'),
|
'django': ('http://docs.djangoproject.com/en/stable/', 'http://docs.djangoproject.com/en/stable/_objects/'),
|
||||||
'requests': ('http://docs.python-requests.org/en/latest/', None),
|
'requests': ('http://docs.python-requests.org/en/latest/', None),
|
||||||
}
|
}
|
||||||
@@ -268,22 +268,22 @@ intersphinx_mapping = {
|
|||||||
def setup(app):
|
def setup(app):
|
||||||
# Django-specific roles, from https://github.com/django/django/blob/master/docs/_ext/djangodocs.py:
|
# Django-specific roles, from https://github.com/django/django/blob/master/docs/_ext/djangodocs.py:
|
||||||
app.add_crossref_type(
|
app.add_crossref_type(
|
||||||
directivename = "setting",
|
directivename="setting",
|
||||||
rolename = "setting",
|
rolename="setting",
|
||||||
indextemplate = "pair: %s; setting",
|
indextemplate="pair: %s; setting",
|
||||||
)
|
)
|
||||||
app.add_crossref_type(
|
app.add_crossref_type(
|
||||||
directivename = "templatetag",
|
directivename="templatetag",
|
||||||
rolename = "ttag",
|
rolename="ttag",
|
||||||
indextemplate = "pair: %s; template tag"
|
indextemplate="pair: %s; template tag"
|
||||||
)
|
)
|
||||||
app.add_crossref_type(
|
app.add_crossref_type(
|
||||||
directivename = "templatefilter",
|
directivename="templatefilter",
|
||||||
rolename = "tfilter",
|
rolename="tfilter",
|
||||||
indextemplate = "pair: %s; template filter"
|
indextemplate="pair: %s; template filter"
|
||||||
)
|
)
|
||||||
app.add_crossref_type(
|
app.add_crossref_type(
|
||||||
directivename = "fieldlookup",
|
directivename="fieldlookup",
|
||||||
rolename = "lookup",
|
rolename="lookup",
|
||||||
indextemplate = "pair: %s; field lookup type",
|
indextemplate="pair: %s; field lookup type",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,31 +3,45 @@
|
|||||||
Contributing
|
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
|
The `Anymail source code`_ is on GitHub.
|
||||||
of some of the people who have helped improve Djrill.
|
|
||||||
|
|
||||||
.. _Djrill source code: https://github.com/brack3t/Djrill
|
.. _Anymail source code: https://github.com/anymail/django-anymail
|
||||||
.. _AUTHORS.txt: https://github.com/brack3t/Djrill/blob/master/AUTHORS.txt
|
|
||||||
|
|
||||||
|
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
|
Bugs
|
||||||
----
|
----
|
||||||
|
|
||||||
You can report problems or request features in
|
You can report problems or request features in `Anymail's GitHub issue tracker`_.
|
||||||
`Djrill's github issue tracker <https://github.com/brack3t/Djrill/issues>`_.
|
|
||||||
|
|
||||||
We also have some :ref:`troubleshooting` information that may be helpful.
|
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.
|
* 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
|
* By submitting a pull request, you're agreeing to release your changes under under
|
||||||
the same BSD license as the rest of this project.
|
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
|
Testing
|
||||||
-------
|
-------
|
||||||
|
|
||||||
Djrill is `tested on Travis <https://travis-ci.org/brack3t/Djrill>`_ against several
|
Anymail is `tested on Travis`_ against several combinations of Django
|
||||||
combinations of Django and Python versions. (Full list in
|
and Python versions. (Full list in `.travis.yml`_.)
|
||||||
`.travis.yml <https://github.com/brack3t/Djrill/blob/master/.travis.yml>`_.)
|
|
||||||
|
|
||||||
Most of the included tests verify that Djrill constructs the expected Mandrill API
|
Most of the included tests verify that Anymail constructs the expected ESP API
|
||||||
calls, without actually calling Mandrill or sending any email. So these tests
|
calls, without actually calling the ESP's API or sending any email. So these tests
|
||||||
don't require a Mandrill API key, but they *do* require
|
don't require API keys, but they *do* require `mock`_ (``pip install mock``).
|
||||||
`mock <http://www.voidspace.org.uk/python/mock/index.html>`_
|
|
||||||
and `six <https://pythonhosted.org/six/>`_ (``pip install mock six``).
|
|
||||||
|
|
||||||
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
|
$ python -Wall runtests.py
|
||||||
`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.)
|
|
||||||
|
|
||||||
.. _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
|
||||||
|
|||||||
23
docs/esps/index.rst
Normal file
23
docs/esps/index.rst
Normal file
@@ -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 <contributing>`
|
||||||
|
your own implementation to Anymail.
|
||||||
|
|
||||||
103
docs/esps/mailgun.rst
Normal file
103
docs/esps/mailgun.rst
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
.. _mailgun-backend:
|
||||||
|
|
||||||
|
Mailgun
|
||||||
|
-------
|
||||||
|
|
||||||
|
Anymail integrates with the `Mailgun <https://mailgun.com>`_
|
||||||
|
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": "<your 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 <mailgun-sender-domain>`
|
||||||
|
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 <base-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 <send-defaults>` 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
|
||||||
151
docs/esps/mandrill.rst
Normal file
151
docs/esps/mandrill.rst
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
.. _mandrill-backend:
|
||||||
|
|
||||||
|
Mandrill
|
||||||
|
--------
|
||||||
|
|
||||||
|
Anymail integrates with the `Mandrill <http://mandrill.com/>`_
|
||||||
|
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": "<your 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": "<your 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 <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.
|
||||||
17
docs/esps/postmark.rst
Normal file
17
docs/esps/postmark.rst
Normal file
@@ -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.)
|
||||||
17
docs/esps/sendgrid.rst
Normal file
17
docs/esps/sendgrid.rst
Normal file
@@ -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.)
|
||||||
146
docs/history.rst
146
docs/history.rst
@@ -1,146 +0,0 @@
|
|||||||
.. _history:
|
|
||||||
|
|
||||||
Release Notes
|
|
||||||
=============
|
|
||||||
|
|
||||||
Djrill practices `semantic versioning <semver>`_.
|
|
||||||
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 <upgrading>`.
|
|
||||||
* 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 <django:topic-email-backends>`.)
|
|
||||||
* 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 <message-headers>`
|
|
||||||
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 <sending-attachments>`
|
|
||||||
* 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 <https://djrill.readthedocs.org>`_
|
|
||||||
|
|
||||||
|
|
||||||
Version 0.3:
|
|
||||||
|
|
||||||
* :ref:`Attachments <sending-attachments>` are now supported
|
|
||||||
* :ref:`Mandrill templates <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
|
|
||||||
18
docs/inbound.rst
Normal file
18
docs/inbound.rst
Normal file
@@ -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
|
||||||
|
---------------------
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
.. Djrill documentation master file, created by
|
Anymail: Multi-ESP transactional email for Django
|
||||||
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
|
|
||||||
===============================================
|
|
||||||
|
|
||||||
Version |release|
|
Version |release|
|
||||||
|
|
||||||
@@ -15,6 +10,8 @@ Version |release|
|
|||||||
:end-before: END shared-intro
|
:end-before: END shared-intro
|
||||||
|
|
||||||
|
|
||||||
|
.. _main-toc:
|
||||||
|
|
||||||
Documentation
|
Documentation
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
@@ -23,21 +20,10 @@ Documentation
|
|||||||
|
|
||||||
quickstart
|
quickstart
|
||||||
installation
|
installation
|
||||||
upgrading
|
sending/index
|
||||||
usage/sending_mail
|
inbound
|
||||||
usage/templates
|
esps/index
|
||||||
usage/multiple_backends
|
tips/index
|
||||||
usage/webhooks
|
|
||||||
troubleshooting
|
troubleshooting
|
||||||
contributing
|
contributing
|
||||||
history
|
release_notes
|
||||||
|
|
||||||
|
|
||||||
Thanks
|
|
||||||
------
|
|
||||||
|
|
||||||
Thanks to the MailChimp team for asking us to build this nifty little app, and to all of Djrill's
|
|
||||||
:doc:`contributors <contributing>`.
|
|
||||||
Oh, and, of course, Kenneth Reitz for the awesome requests_ library.
|
|
||||||
|
|
||||||
.. _requests: http://docs.python-requests.org
|
|
||||||
|
|||||||
@@ -1,117 +1,167 @@
|
|||||||
Installation
|
Installation and configuration
|
||||||
============
|
==============================
|
||||||
|
|
||||||
It's easiest to install Djrill from `PyPI <https://pypi.python.org/pypi/djrill>`_:
|
.. _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
|
.. 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
|
...or with both Postmark and SendGrid support:
|
||||||
one dependency (other than Django, of course): the `requests <http://docs.python-requests.org>`_
|
|
||||||
library from Kenneth Reitz.
|
.. 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 = (
|
.. code-block:: python
|
||||||
...
|
|
||||||
"djrill"
|
|
||||||
)
|
|
||||||
|
|
||||||
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" = "<your Mailgun 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 <multiple-backends>` to send particular
|
||||||
|
messages through different ESPs.)
|
||||||
|
|
||||||
|
The exact backend name and required settings vary by ESP.
|
||||||
|
See the :ref:`supported ESPs <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,
|
Configuring status tracking webhooks
|
||||||
this is a good time to add one. (Django's default is "webmaster@localhost",
|
------------------------------------
|
||||||
which won't work with Mandrill.)
|
|
||||||
|
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 <backend-configuration>`, 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.
|
Anymail can optionally connect to your ESPs inbound webhook to notify your app
|
||||||
See the Djrill :ref:`webhooks <webhooks>` section for configuration details.
|
of inbound messages.
|
||||||
|
|
||||||
|
If you want to use inbound email with Anymail, first follow the first two
|
||||||
|
:ref:`backend configuration <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
|
There are specific Anymail settings for each ESP (like API keys and urls).
|
||||||
|
See the :ref:`supported ESPs <supported-esps>` section for details.
|
||||||
MANDRILL_IGNORE_RECIPIENT_STATUS
|
Here are the other settings Anymail supports:
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Set to ``True`` to disable :exc:`djrill.MandrillRecipientsRefused` exceptions
|
|
||||||
on invalid or rejected recipients. (Default ``False``.)
|
|
||||||
|
|
||||||
.. versionadded:: 2.0
|
|
||||||
|
|
||||||
|
|
||||||
.. 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 to `True` to disable :exc:`AnymailRecipientsRefused` exceptions
|
||||||
Set :setting:`!MANDRILL_SETTINGS` to a dict of these options. Example::
|
on invalid or rejected recipients. (Default `False`.)
|
||||||
|
See :ref:`recipients-refused`.
|
||||||
|
|
||||||
MANDRILL_SETTINGS = {
|
.. code-block:: python
|
||||||
'subaccount': 'client-347',
|
|
||||||
'tracking_domain': 'example.com',
|
|
||||||
'track_opens': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
See :ref:`mandrill-send-support` for a list of available options. (Everything
|
ANYMAIL = {
|
||||||
*except* :attr:`merge_vars`, :attr:`recipient_metadata`, and :attr:`send_at`
|
...
|
||||||
can be used with :setting:`!MANDRILL_SETTINGS`.)
|
"IGNORE_RECIPIENT_STATUS": True,
|
||||||
|
}
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
.. setting:: MANDRILL_API_URL
|
.. rubric:: SEND_DEFAULTS and *ESP*\ _SEND_DEFAULTS`
|
||||||
|
|
||||||
MANDRILL_API_URL
|
A `dict` of default options to apply to all messages sent through Anymail.
|
||||||
~~~~~~~~~~~~~~~~
|
See :ref:`send-defaults`.
|
||||||
|
|
||||||
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.)
|
|
||||||
|
|
||||||
|
|
||||||
.. setting:: MANDRILL_SUBACCOUNT
|
.. rubric:: UNSUPPORTED_FEATURE_ERRORS
|
||||||
|
|
||||||
MANDRILL_SUBACCOUNT
|
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
|
||||||
Prior to Djrill 2.0, the :setting:`!MANDRILL_SUBACCOUNT` setting could
|
:ref:`unsupported-features`. (Default `True`.)
|
||||||
be used to globally set the `Mandrill subaccount <subaccounts>`_.
|
|
||||||
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-
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Djrill 1-2-3
|
Anymail 1-2-3
|
||||||
============
|
=============
|
||||||
|
|
||||||
.. Quickstart is maintained in README.rst at the source root.
|
.. Quickstart is maintained in README.rst at the source root.
|
||||||
(Docs can include from the readme; the readme can't include anything.)
|
(Docs can include from the readme; the readme can't include anything.)
|
||||||
@@ -7,3 +7,16 @@ Djrill 1-2-3
|
|||||||
.. include:: ../README.rst
|
.. include:: ../README.rst
|
||||||
:start-after: _quickstart:
|
:start-after: _quickstart:
|
||||||
:end-before: END 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 <sending-email>`
|
||||||
|
* :ref:`Receiving inbound email <inbound>`
|
||||||
|
* :ref:`ESP-specific information <supported-esps>`
|
||||||
|
* :ref:`All the docs <main-toc>`
|
||||||
|
|||||||
23
docs/release_notes.rst
Normal file
23
docs/release_notes.rst
Normal file
@@ -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 <semver>`_.
|
||||||
|
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
|
||||||
449
docs/sending/anymail_additions.rst
Normal file
449
docs/sending/anymail_additions.rst
Normal file
@@ -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 <user1@example.com>"],
|
||||||
|
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 <django:default-current-time-zone>`.
|
||||||
|
(Careful: :meth:`datetime.now() <datetime.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 <supported-esps>` 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 <me@example.com>"],
|
||||||
|
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
|
||||||
|
`<img>` 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 = '... <img alt="Picture" src="cid:%s"> ...' % 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
|
||||||
162
docs/sending/django_email.rst
Normal file
162
docs/sending/django_email.rst
Normal file
@@ -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 <django.core.mail>`" 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 <multiple-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>html body</html>")
|
||||||
|
|
||||||
|
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>html body</html>", "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 <event-tracking>`
|
||||||
|
if you need to be notified of sends to blacklisted or invalid emails.
|
||||||
50
docs/sending/exceptions.rst
Normal file
50
docs/sending/exceptions.rst
Normal file
@@ -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.
|
||||||
13
docs/sending/index.rst
Normal file
13
docs/sending/index.rst
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
.. _sending-email:
|
||||||
|
|
||||||
|
Sending email
|
||||||
|
-------------
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
django_email
|
||||||
|
anymail_additions
|
||||||
|
templates
|
||||||
|
tracking
|
||||||
|
exceptions
|
||||||
146
docs/sending/templates.rst
Normal file
146
docs/sending/templates.rst
Normal file
@@ -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 <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': "<a href='.../*|TRACKINGNO|*'>track it</a>"
|
||||||
|
.. }
|
||||||
|
.. 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 <https://mandrillapp.com/api/docs/messages.html#method=send-template>`_,
|
||||||
|
.. and will ignore any `body` text set on the `EmailMessage`.
|
||||||
|
..
|
||||||
|
.. All of Djrill's other :ref:`Mandrill-specific options <anymail-send-features>`
|
||||||
|
.. 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.
|
||||||
|
|
||||||
178
docs/sending/tracking.rst
Normal file
178
docs/sending/tracking.rst
Normal file
@@ -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 = "<create your own random 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
|
||||||
55
docs/tips/django_templates.rst
Normal file
55
docs/tips/django_templates.rst
Normal file
@@ -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 <django.template>`.
|
||||||
|
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
|
||||||
16
docs/tips/index.rst
Normal file
16
docs/tips/index.rst
Normal file
@@ -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)
|
||||||
|
|
||||||
@@ -1,30 +1,35 @@
|
|||||||
.. _multiple-backends:
|
.. _multiple-backends:
|
||||||
|
|
||||||
Mixing Email Backends
|
Mixing email backends
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
Since you are replacing Django's global :setting:`EMAIL_BACKEND`, by default
|
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 <django.core.mail.get_connection>`
|
You can use Django mail's optional :func:`connection <django.core.mail.get_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:
|
but send admin emails directly through an SMTP server:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:emphasize-lines: 8,10
|
:emphasize-lines: 8,10,13,15
|
||||||
|
|
||||||
from django.core.mail import send_mail, get_connection
|
from django.core.mail import send_mail, get_connection
|
||||||
|
|
||||||
# send_mail connection defaults to the settings EMAIL_BACKEND, which
|
# 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"])
|
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:
|
# Get a connection to an SMTP backend, and send using that instead:
|
||||||
smtp_backend = get_connection('django.core.mail.backends.smtp.EmailBackend')
|
smtp_backend = get_connection('django.core.mail.backends.smtp.EmailBackend')
|
||||||
send_mail("Uh-Oh", "Need your attention", "admin@example.com", ["alert@example.com"],
|
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
|
You can supply a different connection to Django's
|
||||||
:func:`~django.core.mail.send_mail` and :func:`~django.core.mail.send_mass_mail` helpers,
|
:func:`~django.core.mail.send_mail` and :func:`~django.core.mail.send_mass_mail` helpers,
|
||||||
@@ -32,7 +37,8 @@ and in the constructor for an
|
|||||||
:class:`~django.core.mail.EmailMessage` or :class:`~django.core.mail.EmailMultiAlternatives`.
|
: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:
|
.. _django.utils.log.AdminEmailHandler:
|
||||||
https://docs.djangoproject.com/en/stable/topics/logging/#django.utils.log.AdminEmailHandler
|
https://docs.djangoproject.com/en/stable/topics/logging/#django.utils.log.AdminEmailHandler
|
||||||
@@ -3,68 +3,60 @@
|
|||||||
Troubleshooting
|
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
|
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
|
error logs. If you see something like "invalid API key"
|
||||||
something goes wrong. And when the error is something like "invalid API key"
|
|
||||||
or "invalid email address", that's probably 90% of what you'll need to know
|
or "invalid email address", that's probably 90% of what you'll need to know
|
||||||
to solve the problem.
|
to solve the problem.
|
||||||
|
|
||||||
* **Check the Mandrill API logs:** The Mandrill dashboard includes an
|
**Check your ESPs API logs**
|
||||||
*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.
|
|
||||||
|
|
||||||
* **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?
|
**Double-check common issues**
|
||||||
* Did you add ``'djrill'`` to the list of :setting:`INSTALLED_APPS` in settings.py?
|
|
||||||
|
* 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",
|
* 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
|
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`
|
**Try it without Anymail**
|
||||||
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
|
Try switching your :setting:`EMAIL_BACKEND` setting to
|
||||||
other than Djrill. And you can look through the :setting:`EMAIL_FILE_PATH`
|
Django's :ref:`File backend <django:topic-email-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.
|
file contents afterward to see if you're generating the email you want.
|
||||||
|
|
||||||
|
|
||||||
.. _recent API calls: https://mandrillapp.com/settings/api
|
Getting help
|
||||||
.. _File backend: https://docs.djangoproject.com/en/stable/topics/email/#file-backend
|
|
||||||
|
|
||||||
|
|
||||||
Getting Help
|
|
||||||
------------
|
------------
|
||||||
|
|
||||||
If you've gone through the suggestions above and still aren't sure what's wrong,
|
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
|
the Anymail community is happy to help. Anymail is supported and maintained by the
|
||||||
people who use it -- like you! (We're not Mandrill employees.)
|
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 <contributing>` a fix!)
|
||||||
|
|
||||||
Ask on `StackOverflow`_
|
Whenever you open an issue, it's always helpful to mention which ESP you're using,
|
||||||
Tag your question with **both** ``Django`` and ``Mandrill`` to get our attention.
|
include the relevant portions of your code and settings, the text of any error messages,
|
||||||
Bonus: a lot of questions about Djrill are actually questions about Django
|
and any exception stack traces.
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
.. _StackOverflow: http://stackoverflow.com/questions/tagged/django+mandrill
|
.. _GitHub issue: https://github.com/anymail/django-anymail/issues
|
||||||
.. _GitHub issue: https://github.com/brack3t/Djrill/issues
|
|
||||||
|
|||||||
@@ -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 <history>` 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 <EMAIL_BACKEND>`,
|
|
||||||
which raises :exc:`SMTPRecipientsRefused <smtplib.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.
|
|
||||||
@@ -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 <multiple-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 <email\@example.com>").
|
|
||||||
|
|
||||||
**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>html body</html>", "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 <https://mandrillapp.com/api/docs/messages.html#method=send>`_
|
|
||||||
`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 <https://mandrillapp.com/api/docs/messages.html#method=send>`_ 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 <https://mandrillapp.com/settings/api>`_ 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.
|
|
||||||
@@ -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': "<a href='.../*|TRACKINGNO|*'>track it</a>"
|
|
||||||
}
|
|
||||||
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 <https://mandrillapp.com/api/docs/messages.html#method=send-template>`_,
|
|
||||||
and will ignore any `body` text set on the `EmailMessage`.
|
|
||||||
|
|
||||||
All of Djrill's other :ref:`Mandrill-specific options <mandrill-send-support>`
|
|
||||||
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()
|
|
||||||
|
|
||||||
@@ -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 = "<create your own random 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
|
|
||||||
Reference in New Issue
Block a user