mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
Add inbound mail handling
Add normalized event, signal, and webhooks for inbound mail. Closes #43 Closes #86
This commit is contained in:
@@ -48,6 +48,10 @@ Email Service Provider |Mailgun| |Mailjet| |Mandrill|
|
||||
---------------------------------------------------------------------------------------------------------------------
|
||||
:attr:`~AnymailMessage.anymail_status` Yes Yes Yes Yes Yes Yes
|
||||
|AnymailTrackingEvent| from webhooks Yes Yes Yes Yes Yes Yes
|
||||
|
||||
.. rubric:: :ref:`Inbound handling <inbound>`
|
||||
---------------------------------------------------------------------------------------------------------------------
|
||||
|AnymailInboundEvent| from webhooks Yes Yes Yes Yes Yes Yes
|
||||
============================================ ========== ========== ========== ========== ========== ===========
|
||||
|
||||
|
||||
@@ -63,6 +67,7 @@ meaningless. (And even specific features don't matter if you don't plan to use t
|
||||
.. |SendGrid| replace:: :ref:`sendgrid-backend`
|
||||
.. |SparkPost| replace:: :ref:`sparkpost-backend`
|
||||
.. |AnymailTrackingEvent| replace:: :class:`~anymail.signals.AnymailTrackingEvent`
|
||||
.. |AnymailInboundEvent| replace:: :class:`~anymail.signals.AnymailInboundEvent`
|
||||
|
||||
|
||||
Other ESPs
|
||||
|
||||
@@ -215,3 +215,36 @@ a Django :class:`~django.http.QueryDict` object of `Mailgun event fields`_.
|
||||
|
||||
.. _Mailgun dashboard: https://mailgun.com/app/dashboard
|
||||
.. _Mailgun event fields: https://documentation.mailgun.com/user_manual.html#webhooks
|
||||
|
||||
|
||||
.. _mailgun-inbound:
|
||||
|
||||
Inbound webhook
|
||||
---------------
|
||||
|
||||
If you want to receive email from Mailgun through Anymail's normalized :ref:`inbound <inbound>`
|
||||
handling, follow Mailgun's `Receiving, Storing and Fowarding Messages`_ guide to set up
|
||||
an inbound route that forwards to Anymail's inbound webhook. (You can configure routes
|
||||
using Mailgun's API, or simply using the "Routes" tab in your `Mailgun dashboard`_.)
|
||||
|
||||
The *action* for your route will be either:
|
||||
|
||||
:samp:`forward("https://{random}:{random}@{yoursite.example.com}/anymail/mailgun/inbound/")`
|
||||
:samp:`forward("https://{random}:{random}@{yoursite.example.com}/anymail/mailgun/inbound_mime/")`
|
||||
|
||||
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
|
||||
* *yoursite.example.com* is your Django site
|
||||
|
||||
Anymail accepts either of Mailgun's "fully-parsed" (.../inbound/) and "raw MIME" (.../inbound_mime/)
|
||||
formats; the URL tells Mailgun which you want. Because Anymail handles parsing and normalizing the data,
|
||||
both are equally easy to use. The raw MIME option will give the most accurate representation of *any*
|
||||
received email (including complex forms like multi-message mailing list digests). The fully-parsed option
|
||||
*may* use less memory while processing messages with many large attachments.
|
||||
|
||||
If you want to use Anymail's normalized :attr:`~anymail.inbound.AnymailInboundMessage.spam_detected` and
|
||||
:attr:`~anymail.inbound.AnymailInboundMessage.spam_score` attributes, you'll need to set your Mailgun
|
||||
domain's inbound spam filter to "Deliver spam, but add X-Mailgun-SFlag and X-Mailgun-SScore headers"
|
||||
(in the `Mailgun dashboard`_ on the "Domains" tab).
|
||||
|
||||
.. _Receiving, Storing and Fowarding Messages:
|
||||
https://documentation.mailgun.com/en/latest/user_manual.html#receiving-forwarding-and-storing-messages
|
||||
|
||||
@@ -249,3 +249,26 @@ for each event in the batch.)
|
||||
|
||||
.. _Event tracking (triggers): https://app.mailjet.com/account/triggers
|
||||
.. _Mailjet event: https://dev.mailjet.com/guides/#events
|
||||
|
||||
|
||||
.. _mailjet-inbound:
|
||||
|
||||
Inbound webhook
|
||||
---------------
|
||||
|
||||
If you want to receive email from Mailjet through Anymail's normalized :ref:`inbound <inbound>`
|
||||
handling, follow Mailjet's `Parse API inbound emails`_ guide to set up Anymail's inbound webhook.
|
||||
|
||||
The parseroute Url parameter will be:
|
||||
|
||||
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/mailjet/inbound/`
|
||||
|
||||
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
|
||||
* *yoursite.example.com* is your Django site
|
||||
|
||||
Once you've done Mailjet's "basic setup" to configure the Parse API webhook, you can skip
|
||||
ahead to the "use your own domain" section of their guide. (Anymail normalizes the inbound
|
||||
event for you, so you won't need to worry about Mailjet's event and attachment formats.)
|
||||
|
||||
.. _Parse API inbound emails:
|
||||
https://dev.mailjet.com/guides/#parse-api-inbound-emails
|
||||
|
||||
@@ -185,27 +185,31 @@ See the `Mandrill's template docs`_ for more information.
|
||||
|
||||
|
||||
.. _mandrill-webhooks:
|
||||
.. _mandrill-inbound:
|
||||
|
||||
Status tracking webhooks
|
||||
------------------------
|
||||
Status tracking and inbound webhooks
|
||||
------------------------------------
|
||||
|
||||
If you are using Anymail's normalized :ref:`status tracking <event-tracking>`,
|
||||
setting up Anymail's webhook URL requires deploying your Django project twice:
|
||||
If you are using Anymail's normalized :ref:`status tracking <event-tracking>`
|
||||
and/or :ref:`inbound <inbound>` handling, setting up Anymail's webhook URL
|
||||
requires deploying your Django project twice:
|
||||
|
||||
1. First, follow the instructions to
|
||||
:ref:`configure Anymail's webhooks <webhooks-configuration>`. You *must*
|
||||
deploy before adding the webhook URL to Mandrill, because it will attempt
|
||||
:ref:`configure Anymail's webhooks <webhooks-configuration>`. You *must deploy*
|
||||
before adding the webhook URL to Mandrill, because Mandrill will attempt
|
||||
to verify the URL against your production server.
|
||||
|
||||
Follow `Mandrill's instructions`_ to add Anymail's webhook URL in their settings:
|
||||
Once you've deployed, then set Anymail's webhook URL in Mandrill, following their
|
||||
instructions for `tracking event webhooks`_ (be sure to check the boxes for the
|
||||
events you want to receive) and/or `inbound route webhooks`_.
|
||||
In either case, the webhook url is:
|
||||
|
||||
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/mandrill/tracking/`
|
||||
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/mandrill/`
|
||||
|
||||
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
|
||||
* *yoursite.example.com* is your Django site
|
||||
|
||||
Be sure to check the boxes in the Mandrill settings for the event types you want to receive.
|
||||
The same Anymail tracking URL can handle all Mandrill "message" and "change" events.
|
||||
* (Note: Unlike Anymail's other supported ESPs, the Mandrill webhook uses this
|
||||
single url for both tracking and inbound events.)
|
||||
|
||||
2. Mandrill will provide you a "webhook authentication key" once it verifies the URL
|
||||
is working. Add this to your Django project's Anymail settings under
|
||||
@@ -226,7 +230,7 @@ else fails, you can set Anymail's :setting:`MANDRILL_WEBHOOK_URL <ANYMAIL_MANDRI
|
||||
to the same public webhook URL you gave Mandrill.
|
||||
|
||||
Mandrill will report these Anymail :attr:`~anymail.signals.AnymailTrackingEvent.event_type`\s:
|
||||
sent, rejected, deferred, bounced, opened, clicked, complained, unsubscribed. Mandrill does
|
||||
sent, rejected, deferred, bounced, opened, clicked, complained, unsubscribed, inbound. Mandrill does
|
||||
not support delivered events. Mandrill "whitelist" and "blacklist" change events will show up
|
||||
as Anymail's unknown event_type.
|
||||
|
||||
@@ -235,8 +239,18 @@ a `dict` of Mandrill event fields, for a single event. (Although Mandrill calls
|
||||
webhooks with batches of events, Anymail will invoke your signal receiver separately
|
||||
for each event in the batch.)
|
||||
|
||||
.. _Mandrill's instructions:
|
||||
.. _tracking event webhooks:
|
||||
https://mandrill.zendesk.com/hc/en-us/articles/205583217-Introduction-to-Webhooks
|
||||
.. _inbound route webhooks:
|
||||
https://mandrill.zendesk.com/hc/en-us/articles/205583197-Inbound-Email-Processing-Overview
|
||||
|
||||
|
||||
.. versionchanged:: 1.3
|
||||
Earlier Anymail releases used :samp:`.../anymail/mandrill/{tracking}/` as the tracking
|
||||
webhook url. With the addition of inbound handling, Anymail has dropped "tracking"
|
||||
from the recommended url for new installations. But the older url is still
|
||||
supported. Existing installations can continue to use it---and can even install it
|
||||
on a Mandrill *inbound* route to avoid issuing a new webhook key.
|
||||
|
||||
|
||||
.. _migrating-from-djrill:
|
||||
@@ -298,8 +312,15 @@ Changes to settings
|
||||
Use :setting:`ANYMAIL_MANDRILL_WEBHOOK_KEY` instead.
|
||||
|
||||
``DJRILL_WEBHOOK_URL``
|
||||
Use :setting:`ANYMAIL_MANDRILL_WEBHOOK_URL`, or eliminate if
|
||||
your Django server is not behind a proxy that changes hostnames.
|
||||
Often no longer required: Anymail can normally use Django's
|
||||
:meth:`HttpRequest.build_absolute_uri <django.http.HttpRequest.build_absolute_uri>`
|
||||
to figure out the complete webhook url that Mandrill called.
|
||||
|
||||
If you are experiencing webhook authorization errors, the best solution is to adjust
|
||||
your Django :setting:`SECURE_PROXY_SSL_HEADER`, :setting:`USE_X_FORWARDED_HOST`, and/or
|
||||
:setting:`USE_X_FORWARDED_PORT` settings to work with your proxy server.
|
||||
If that's not possible, you can set :setting:`ANYMAIL_MANDRILL_WEBHOOK_URL` to explicitly
|
||||
declare the webhook url.
|
||||
|
||||
|
||||
Changes to EmailMessage attributes
|
||||
@@ -393,14 +414,17 @@ parameters is that most logging and analytics systems are aware of the
|
||||
need to keep auth secret.)
|
||||
|
||||
Anymail replaces `djrill.signals.webhook_event` with
|
||||
`anymail.signals.tracking` for delivery tracking events.
|
||||
(It does not currently handle inbound message webhooks.)
|
||||
`anymail.signals.tracking` for delivery tracking events,
|
||||
and `anymail.signals.inbound` for inbound events.
|
||||
Anymail parses and normalizes
|
||||
the event data passed to the signal receiver: see :ref:`event-tracking`.
|
||||
the event data passed to the signal receiver: see :ref:`event-tracking`
|
||||
and :ref:`inbound`.
|
||||
|
||||
The equivalent of Djrill's ``data`` parameter is available
|
||||
to your signal receiver as
|
||||
:attr:`event.esp_event <anymail.signals.AnymailTrackingEvent.esp_event>`,
|
||||
and for most events, the equivalent of Djrill's ``event_type`` parameter
|
||||
is `event.esp_event['event']`. But consider working with Anymail's
|
||||
normalized :class:`~anymail.signals.AnymailTrackingEvent` instead.
|
||||
normalized :class:`~anymail.signals.AnymailTrackingEvent` and
|
||||
:class:`~anymail.signals.AnymailInboundEvent` instead for easy portability
|
||||
to other ESPs.
|
||||
|
||||
@@ -201,3 +201,26 @@ a `dict` of Postmark `delivery <http://developer.postmarkapp.com/developer-deliv
|
||||
or `open <http://developer.postmarkapp.com/developer-open-webhook.html>`_ webhook data.
|
||||
|
||||
.. _Postmark account settings: https://account.postmarkapp.com/servers
|
||||
|
||||
|
||||
.. _postmark-inbound:
|
||||
|
||||
Inbound webhook
|
||||
---------------
|
||||
|
||||
If you want to receive email from Postmark through Anymail's normalized :ref:`inbound <inbound>`
|
||||
handling, follow Postmark's `Inbound Processing`_ guide to configure
|
||||
an inbound server pointing to Anymail's inbound webhook.
|
||||
|
||||
The InboundHookUrl setting will be:
|
||||
|
||||
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/postmark/inbound/`
|
||||
|
||||
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
|
||||
* *yoursite.example.com* is your Django site
|
||||
|
||||
Anymail handles the "parse an email" part of Postmark's instructions for you, but you'll
|
||||
likely want to work through the other sections to set up a custom inbound domain, and
|
||||
perhaps configure inbound spam blocking.
|
||||
|
||||
.. _Inbound Processing: https://postmarkapp.com/developer/user-guide/inbound
|
||||
|
||||
@@ -302,6 +302,37 @@ for each event in the batch.)
|
||||
.. _Sendgrid event: https://sendgrid.com/docs/API_Reference/Webhooks/event.html
|
||||
|
||||
|
||||
.. _sendgrid-inbound:
|
||||
|
||||
Inbound webhook
|
||||
---------------
|
||||
|
||||
If you want to receive email from SendGrid through Anymail's normalized :ref:`inbound <inbound>`
|
||||
handling, follow SendGrid's `Inbound Parse Webhook`_ guide to set up
|
||||
Anymail's inbound webhook.
|
||||
|
||||
The Destination URL setting will be:
|
||||
|
||||
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/sendgrid/inbound/`
|
||||
|
||||
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
|
||||
* *yoursite.example.com* is your Django site
|
||||
|
||||
Be sure the URL has a trailing slash. (SendGrid's inbound processing won't follow Django's
|
||||
:setting:`APPEND_SLASH` redirect.)
|
||||
|
||||
If you want to use Anymail's normalized :attr:`~anymail.inbound.AnymailInboundMessage.spam_detected` and
|
||||
:attr:`~anymail.inbound.AnymailInboundMessage.spam_score` attributes, be sure to enable the "Check
|
||||
incoming emails for spam" checkbox.
|
||||
|
||||
You have a choice for SendGrid's "POST the raw, full MIME message" checkbox. Anymail will handle
|
||||
either option (and you can change it at any time). Enabling raw MIME will give the most accurate
|
||||
representation of *any* received email (including complex forms like multi-message mailing list
|
||||
digests). But disabling it *may* use less memory while processing messages with many large attachments.
|
||||
|
||||
.. _Inbound Parse Webhook:
|
||||
https://sendgrid.com/docs/Classroom/Basics/Inbound_Parse_Webhook/setting_up_the_inbound_parse_webhook.html
|
||||
|
||||
|
||||
.. _sendgrid-v3-upgrade:
|
||||
|
||||
|
||||
@@ -220,3 +220,23 @@ The esp_event is the raw, `wrapped json event structure`_ as provided by SparkPo
|
||||
https://support.sparkpost.com/customer/portal/articles/1976204-webhook-event-reference
|
||||
.. _wrapped json event structure:
|
||||
https://support.sparkpost.com/customer/en/portal/articles/2311698-comparing-webhook-and-message-event-data
|
||||
|
||||
|
||||
.. _sparkpost-inbound:
|
||||
|
||||
Inbound webhook
|
||||
---------------
|
||||
|
||||
If you want to receive email from SparkPost through Anymail's normalized :ref:`inbound <inbound>`
|
||||
handling, follow SparkPost's `Enabling Inbound Email Relaying`_ guide to set up
|
||||
Anymail's inbound webhook.
|
||||
|
||||
The target parameter for the Relay Webhook will be:
|
||||
|
||||
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/sparkpost/inbound/`
|
||||
|
||||
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
|
||||
* *yoursite.example.com* is your Django site
|
||||
|
||||
.. _Enabling Inbound Email Relaying:
|
||||
https://www.sparkpost.com/docs/tech-resources/inbound-email-relay-webhook/
|
||||
|
||||
454
docs/inbound.rst
Normal file
454
docs/inbound.rst
Normal file
@@ -0,0 +1,454 @@
|
||||
.. _inbound:
|
||||
|
||||
Receiving mail
|
||||
==============
|
||||
|
||||
.. versionadded:: 1.3
|
||||
|
||||
For ESPs that support receiving inbound email, Anymail offers normalized handling
|
||||
of inbound events.
|
||||
|
||||
If you didn't set up webhooks when first installing Anymail, you'll need to
|
||||
:ref:`configure webhooks <webhooks-configuration>` to get started with inbound email.
|
||||
(You should also review :ref:`securing-webhooks`.)
|
||||
|
||||
Once you've enabled webhooks, Anymail will send a ``anymail.signals.inbound``
|
||||
custom Django :mod:`signal <django.dispatch>` for each ESP inbound message it receives.
|
||||
You can connect your own receiver function to this signal for further processing.
|
||||
(This is very much like how Anymail handles :ref:`status tracking <event-tracking>`
|
||||
events for sent messages. Inbound events just use a different signal receiver
|
||||
and have different event parameters.)
|
||||
|
||||
Be sure to read Django's :doc:`listening to signals <django:topics/signals>` docs
|
||||
for information on defining and connecting signal receivers.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from anymail.signals import inbound
|
||||
from django.dispatch import receiver
|
||||
|
||||
@receiver(inbound) # add weak=False if inside some other function/class
|
||||
def handle_inbound(sender, event, esp_name, **kwargs):
|
||||
message = event.message
|
||||
print("Received message from %s (envelope sender %s) with subject '%s'" % (
|
||||
message.from_email, message.envelope_sender, message.subject))
|
||||
|
||||
Some ESPs batch up multiple inbound messages into a single webhook call. Anymail will
|
||||
invoke your signal receiver once, separately, for each message in the batch.
|
||||
|
||||
.. _inbound-security:
|
||||
|
||||
.. warning:: **Be careful with inbound email**
|
||||
|
||||
Inbound email is user-supplied content. There are all kinds of ways a
|
||||
malicious sender can abuse the email format to give your app misleading
|
||||
or dangerous data. Treat inbound email content with the same suspicion
|
||||
you'd apply to any user-submitted data. Among other concerns:
|
||||
|
||||
* Senders can spoof the From header. An inbound message's
|
||||
:attr:`~anymail.inbound.AnymailInboundMessage.from_email` may
|
||||
or may not match the actual address that sent the message. (There are both
|
||||
legitimate and malicious uses for this capability.)
|
||||
|
||||
* Most other fields in email can be falsified. E.g., an inbound message's
|
||||
:attr:`~anymail.inbound.AnymailInboundMessage.date` may or may not accurately
|
||||
reflect when the message was sent.
|
||||
|
||||
* Inbound attachments have the same security concerns as user-uploaded files.
|
||||
If you process inbound attachments, you'll need to verify that the
|
||||
attachment content is valid.
|
||||
|
||||
This is particularly important if you publish the attachment content
|
||||
through your app. For example, an "image" attachment could actually contain an
|
||||
executable file or raw HTML. You wouldn't want to serve that as a user's avatar.
|
||||
|
||||
It's *not* sufficient to check the attachment's content-type or
|
||||
filename extension---senders can falsify both of those.
|
||||
Consider `using python-magic`_ or a similar approach
|
||||
to validate the *actual attachment content*.
|
||||
|
||||
The Django docs have additional notes on
|
||||
:ref:`user-supplied content security <django:user-uploaded-content-security>`.
|
||||
|
||||
.. _using python-magic:
|
||||
http://blog.hayleyanderson.us/2015/07/18/validating-file-types-in-django/
|
||||
|
||||
|
||||
.. _inbound-event:
|
||||
|
||||
Normalized inbound event
|
||||
------------------------
|
||||
|
||||
.. class:: anymail.signals.AnymailInboundEvent
|
||||
|
||||
The `event` parameter to Anymail's `inbound`
|
||||
:ref:`signal receiver <inbound-signal-receivers>` is an object
|
||||
with the following attributes:
|
||||
|
||||
.. attribute:: message
|
||||
|
||||
An :class:`~anymail.inbound.AnymailInboundMessage` representing the email
|
||||
that was received. Most of what you're interested in will be on this `message`
|
||||
attribute. See the full details :ref:`below <inbound-message>`.
|
||||
|
||||
.. attribute:: event_type
|
||||
|
||||
A normalized `str` identifying the type of event. For inbound events,
|
||||
this is always `'inbound'`.
|
||||
|
||||
.. attribute:: timestamp
|
||||
|
||||
A `~datetime.datetime` indicating when the inbound event was generated
|
||||
by the ESP, if available; otherwise `None`. (Very few ESPs provide this info.)
|
||||
|
||||
This is typically when the ESP received the message or shortly
|
||||
thereafter. (Use :attr:`event.message.date <anymail.inbound.AnymailInboundMessage.date>`
|
||||
if you're interested in when the message was sent.)
|
||||
|
||||
(The timestamp's timezone is often UTC, but the exact behavior depends
|
||||
on your ESP and account settings. Anymail ensures that this value is
|
||||
an *aware* datetime with an accurate timezone.)
|
||||
|
||||
.. attribute:: event_id
|
||||
|
||||
A `str` unique identifier for the event, if available; otherwise `None`.
|
||||
Can be used to avoid processing the same event twice. The exact format varies
|
||||
by ESP, and very few ESPs provide an event_id for inbound messages.
|
||||
|
||||
An alternative approach to avoiding duplicate processing is to use the
|
||||
inbound message's :mailheader:`Message-ID` header (``event.message['Message-ID']``).
|
||||
|
||||
.. attribute:: esp_event
|
||||
|
||||
The "raw" event data from the ESP, deserialized into a python data structure.
|
||||
For most ESPs this is either parsed JSON (as a `dict`), or sometimes the
|
||||
complete Django :class:`~django.http.HttpRequest` received by the webhook.
|
||||
|
||||
This gives you (non-portable) access to original event provided by your ESP,
|
||||
which can be helpful if you need to access data Anymail doesn't normalize.
|
||||
|
||||
|
||||
.. _inbound-message:
|
||||
|
||||
Normalized inbound message
|
||||
--------------------------
|
||||
|
||||
.. class:: anymail.inbound.AnymailInboundMessage
|
||||
|
||||
The :attr:`~AnymailInboundEvent.message` attribute of an :class:`AnymailInboundEvent`
|
||||
is an AnymailInboundMessage---an extension of Python's standard :class:`email.message.Message`
|
||||
with additional features to simplify inbound handling.
|
||||
|
||||
In addition to the base :class:`~email.message.Message` functionality, it includes these attributes:
|
||||
|
||||
.. attribute:: envelope_sender
|
||||
|
||||
The actual sending address of the inbound message, as determined by your ESP.
|
||||
This is a `str` "addr-spec"---just the email address portion without any display
|
||||
name (``"sender@example.com"``)---or `None` if the ESP didn't provide a value.
|
||||
|
||||
The envelope sender often won't match the message's From header---for example,
|
||||
messages sent on someone's behalf (mailing lists, invitations) or when a spammer
|
||||
deliberately falsifies the From address.
|
||||
|
||||
.. attribute:: envelope_recipient
|
||||
|
||||
The actual destination address the inbound message was delivered to.
|
||||
This is a `str` "addr-spec"---just the email address portion without any display
|
||||
name (``"recipient@example.com"``)---or `None` if the ESP didn't provide a value.
|
||||
|
||||
The envelope recipient may not appear in the To or Cc recipient lists---for example,
|
||||
if your inbound address is bcc'd on a message.
|
||||
|
||||
.. attribute:: from_email
|
||||
|
||||
The value of the message's From header. Anymail converts this to an
|
||||
:class:`~anymail.utils.EmailAddress` object, which makes it easier to access
|
||||
the parsed address fields:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> str(message.from_email) # the fully-formatted address
|
||||
'"Dr. Justin Customer, CPA" <jcustomer@example.com>'
|
||||
>>> message.from_email.addr_spec # the "email" portion of the address
|
||||
'jcustomer@example.com'
|
||||
>>> message.from_email.display_name # empty string if no display name
|
||||
'Dr. Justin Customer, CPA'
|
||||
>>> message.from_email.domain
|
||||
'example.com'
|
||||
>>> message.from_email.username
|
||||
'jcustomer'
|
||||
|
||||
(This API is borrowed from Python 3.6's :class:`email.headerregistry.Address`.)
|
||||
|
||||
If the message has an invalid or missing From header, this property will be `None`.
|
||||
Note that From headers can be misleading; see :attr:`envelope_sender`.
|
||||
|
||||
.. attribute:: to
|
||||
|
||||
A `list` of of parsed :class:`~anymail.utils.EmailAddress` objects from the To header,
|
||||
or an empty list if that header is missing or invalid. Each address in the list
|
||||
has the same properties as shown above for :attr:`from_email`.
|
||||
|
||||
See :attr:`envelope_recipient` if you need to know the actual inbound address
|
||||
that received the inbound message.
|
||||
|
||||
.. attribute:: cc
|
||||
|
||||
A `list` of of parsed :class:`~anymail.utils.EmailAddress` objects, like :attr:`to`,
|
||||
but from the Cc headers.
|
||||
|
||||
.. attribute:: subject
|
||||
|
||||
The value of the message's Subject header, as a `str`, or `None` if there is no Subject
|
||||
header.
|
||||
|
||||
.. attribute:: date
|
||||
|
||||
The value of the message's Date header, as a `~datetime.datetime` object, or `None`
|
||||
if the Date header is missing or invalid. This attribute will almost always be an
|
||||
aware datetime (with a timezone); in rare cases it can be naive if the sending mailer
|
||||
indicated that it had no timezone information available.
|
||||
|
||||
The Date header is the sender's claim about when it sent the message, which isn't
|
||||
necessarily accurate. (If you need to know when the message was received at your ESP,
|
||||
that might be available in :attr:`event.timestamp <anymail.signals.AnymailInboundEvent.timestamp>`.
|
||||
If not, you'd need to parse the messages's :mailheader:`Received` headers,
|
||||
which can be non-trivial.)
|
||||
|
||||
.. attribute:: text
|
||||
|
||||
The message's plaintext message body as a `str`, or `None` if the
|
||||
message doesn't include a plaintext body.
|
||||
|
||||
.. attribute:: html
|
||||
|
||||
The message's HTML message body as a `str`, or `None` if the
|
||||
message doesn't include an HTML body.
|
||||
|
||||
.. attribute:: attachments
|
||||
|
||||
A `list` of all (non-inline) attachments to the message, or an empty list if there are
|
||||
no attachments. See :ref:`inbound-attachments` below for the contents of each list item.
|
||||
|
||||
.. attribute:: inline_attachments
|
||||
|
||||
A `dict` mapping inline Content-ID references to attachment content. Each key is an
|
||||
"unquoted" cid without angle brackets. E.g., if the :attr:`html` body contains
|
||||
``<img src="cid:abc123...">``, you could get that inline image using
|
||||
``message.inline_attachments["abc123..."]``.
|
||||
|
||||
The content of each attachment is described in :ref:`inbound-attachments` below.
|
||||
|
||||
.. attribute:: spam_score
|
||||
|
||||
A `float` spam score (usually from SpamAssassin) if your ESP provides it; otherwise `None`.
|
||||
The range of values varies by ESP and spam-filtering configuration, so you may need to
|
||||
experiment to find a useful threshold.
|
||||
|
||||
.. attribute:: spam_detected
|
||||
|
||||
If your ESP provides a simple yes/no spam determination, a `bool` indicating whether the
|
||||
ESP thinks the inbound message is probably spam. Otherwise `None`. (Most ESPs just assign
|
||||
a :attr:`spam_score` and leave its interpretation up to you.)
|
||||
|
||||
.. attribute:: stripped_text
|
||||
|
||||
If provided by your ESP, a simplified version the inbound message's plaintext body;
|
||||
otherwise `None`.
|
||||
|
||||
What exactly gets "stripped" varies by ESP, but it often omits quoted replies
|
||||
and sometimes signature blocks. (And ESPs who do offer stripped bodies
|
||||
usually consider the feature experimental.)
|
||||
|
||||
.. attribute:: stripped_html
|
||||
|
||||
Like :attr:`stripped_text`, but for the HTML body. (Very few ESPs support this.)
|
||||
|
||||
.. rubric:: Other headers, complex messages, etc.
|
||||
|
||||
You can use all of Python's :class:`email.message.Message` features with an
|
||||
AnymailInboundMessage. For example, you can access message headers using
|
||||
Message's :meth:`mapping interface <email.message.Message.__getitem__>`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
message['reply-to'] # the Reply-To header (header keys are case-insensitive)
|
||||
message.getall('DKIM-Signature') # list of all DKIM-Signature headers
|
||||
|
||||
And you can use Message methods like :meth:`~email.message.Message.walk` and
|
||||
:meth:`~email.message.Message.get_content_type` to examine more-complex
|
||||
multipart MIME messages (digests, delivery reports, or whatever).
|
||||
|
||||
|
||||
.. _inbound-attachments:
|
||||
|
||||
Handling Inbound Attachments
|
||||
----------------------------
|
||||
|
||||
Anymail converts each inbound attachment to a specialized MIME object with
|
||||
additional methods for handling attachments and integrating with Django.
|
||||
It also backports some helpful MIME methods from newer versions of Python
|
||||
to all versions supported by Anymail.
|
||||
|
||||
The attachment objects in an AnymailInboundMessage's
|
||||
:attr:`~AnymailInboundMessage.attachments` list and
|
||||
:attr:`~AnymailInboundMessage.inline_attachments` dict
|
||||
have these methods:
|
||||
|
||||
.. class:: AnymailInboundMessage
|
||||
|
||||
.. method:: as_uploaded_file()
|
||||
|
||||
Returns the attachment converted to a Django :class:`~django.core.files.uploadedfile.UploadedFile`
|
||||
object. This is suitable for assigning to a model's :class:`~django.db.models.FileField`
|
||||
or :class:`~django.db.models.ImageField`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# allow users to mail in jpeg attachments to set their profile avatars...
|
||||
if attachment.get_content_type() == "image/jpeg":
|
||||
# for security, you must verify the content is really a jpeg
|
||||
# (you'll need to supply the is_valid_jpeg function)
|
||||
if is_valid_jpeg(attachment.get_content_bytes()):
|
||||
user.profile.avatar_image = attachment.as_uploaded_file()
|
||||
|
||||
See Django's docs on :doc:`django:topics/files` for more information
|
||||
on working with uploaded files.
|
||||
|
||||
.. method:: get_content_type()
|
||||
.. method:: get_content_maintype()
|
||||
.. method:: get_content_subtype()
|
||||
|
||||
The type of attachment content, as specified by the sender. (But remember
|
||||
attachments are essentially user-uploaded content, so you should
|
||||
:ref:`never trust the sender <inbound-security>`.)
|
||||
|
||||
See the Python docs for more info on :meth:`email.message.Message.get_content_type`,
|
||||
:meth:`~email.message.Message.get_content_maintype`, and
|
||||
:meth:`~email.message.Message.get_content_subtype`.
|
||||
|
||||
(Note that you *cannot* determine the attachment type using code like
|
||||
``issubclass(attachment, email.mime.image.MIMEImage)``. You should instead use something
|
||||
like ``attachment.get_content_maintype() == 'image'``. The email package's specialized
|
||||
MIME subclasses are designed for constructing new messages, and aren't used
|
||||
for parsing existing, inbound email messages.)
|
||||
|
||||
.. method:: get_filename()
|
||||
|
||||
The original filename of the attachment, as specified by the sender.
|
||||
|
||||
*Never* use this filename directly to write files---that would be a huge security hole.
|
||||
(What would your app do if the sender gave the filename "/etc/passwd" or "../settings.py"?)
|
||||
|
||||
.. method:: is_attachment()
|
||||
|
||||
Returns `True` for a (non-inline) attachment, `False` otherwise.
|
||||
(Anymail back-ports Python 3.4.2's :meth:`~email.message.EmailMessage.is_attachment` method
|
||||
to all supported versions.)
|
||||
|
||||
.. method:: is_inline_attachment()
|
||||
|
||||
Returns `True` for an inline attachment (one with :mailheader:`Content-Disposition` "inline"),
|
||||
`False` otherwise.
|
||||
|
||||
.. method:: get_content_disposition()
|
||||
|
||||
Returns the lowercased value (without parameters) of the attachment's
|
||||
:mailheader:`Content-Disposition` header. The return value should be either "inline"
|
||||
or "attachment", or `None` if the attachment is somehow missing that header.
|
||||
|
||||
(Anymail back-ports Python 3.5's :meth:`~email.message.Message.get_content_disposition`
|
||||
method to all supported versions.)
|
||||
|
||||
.. method:: get_content_text(charset='utf-8')
|
||||
|
||||
Returns the content of the attachment decoded to a `str` in the given charset.
|
||||
(This is generally only appropriate for text or message-type attachments.)
|
||||
|
||||
.. method:: get_content_bytes()
|
||||
|
||||
Returns the raw content of the attachment as bytes. (This will automatically decode
|
||||
any base64-encoded attachment data.)
|
||||
|
||||
.. rubric:: Complex attachments
|
||||
|
||||
An Anymail inbound attachment is actually just an :class:`AnymailInboundMessage` instance,
|
||||
following the Python email package's usual recursive representation of MIME messages.
|
||||
All :class:`AnymailInboundMessage` and :class:`email.message.Message` functionality
|
||||
is available on attachment objects (though of course not all features are meaningful in all contexts).
|
||||
|
||||
This can be helpful for, e.g., parsing email messages that are forwarded as attachments
|
||||
to an inbound message.
|
||||
|
||||
|
||||
Anymail loads all attachment content into memory as it processes each inbound
|
||||
message. This may limit the size of attachments your app can handle, beyond
|
||||
any attachment size limits imposed by your ESP. Depending on how your ESP transmits
|
||||
attachments, you may also need to adjust Django's :setting:`DATA_UPLOAD_MAX_MEMORY_SIZE`
|
||||
setting to successfully receive larger attachments.
|
||||
|
||||
|
||||
.. _inbound-signal-receivers:
|
||||
|
||||
Inbound signal receiver functions
|
||||
---------------------------------
|
||||
|
||||
Your Anymail inbound signal receiver must be a function with this signature:
|
||||
|
||||
.. function:: def my_handler(sender, event, esp_name, **kwargs):
|
||||
|
||||
(You can name it anything you want.)
|
||||
|
||||
:param class sender: The source of the event. (One of the
|
||||
:mod:`anymail.webhook.*` View classes, but you
|
||||
generally won't examine this parameter; it's
|
||||
required by Django's signal mechanism.)
|
||||
:param AnymailInboundEvent event: The normalized inbound event.
|
||||
Almost anything you'd be interested in
|
||||
will be in here---usually in the
|
||||
:class:`~anymail.inbound.AnymailInboundMessage`
|
||||
found in `event.message`.
|
||||
:param str esp_name: e.g., "SendMail" or "Postmark". If you are working
|
||||
with multiple ESPs, you can use this to distinguish
|
||||
ESP-specific handling in your shared event processing.
|
||||
:param \**kwargs: Required by Django's signal mechanism
|
||||
(to support future extensions).
|
||||
|
||||
:returns: nothing
|
||||
:raises: any exceptions in your signal receiver will result
|
||||
in a 400 HTTP error to the webhook. See discussion
|
||||
below.
|
||||
|
||||
.. TODO: this section is almost exactly duplicated from tracking. Combine somehow?
|
||||
|
||||
If (any of) your signal receivers raise an exception, Anymail
|
||||
will discontinue processing the current batch of events and return
|
||||
an HTTP 400 error to the ESP. Most ESPs respond to this by re-sending
|
||||
the event(s) later, a limited number of times.
|
||||
|
||||
This is the desired behavior for transient problems (e.g., your
|
||||
Django database being unavailable), but can cause confusion in other
|
||||
error cases. You may want to catch some (or all) exceptions
|
||||
in your signal receiver, log the problem for later follow up,
|
||||
and allow Anymail to return the normal 200 success response
|
||||
to your ESP.
|
||||
|
||||
Some ESPs impose strict time limits on webhooks, and will consider
|
||||
them failed if they don't respond within (say) five seconds.
|
||||
And they may then retry sending these "failed" events, which could
|
||||
cause duplicate processing in your code.
|
||||
If your signal receiver code might be slow, you should instead
|
||||
queue the event for later, asynchronous processing (e.g., using
|
||||
something like `Celery`_).
|
||||
|
||||
If your signal receiver function is defined within some other
|
||||
function or instance method, you *must* use the `weak=False`
|
||||
option when connecting it. Otherwise, it might seem to work at first,
|
||||
but will unpredictably stop being called at some point---typically
|
||||
on your production server, in a hard-to-debug way. See Django's
|
||||
docs on :doc:`signals <django:topics/signals>` for more information.
|
||||
|
||||
.. _Celery: http://www.celeryproject.org/
|
||||
@@ -21,6 +21,7 @@ Documentation
|
||||
quickstart
|
||||
installation
|
||||
sending/index
|
||||
inbound
|
||||
esps/index
|
||||
tips/index
|
||||
troubleshooting
|
||||
|
||||
@@ -6,28 +6,23 @@ Installation and configuration
|
||||
Installing Anymail
|
||||
------------------
|
||||
|
||||
It's easiest to install Anymail from PyPI using pip.
|
||||
To use Anymail in your Django project:
|
||||
|
||||
1. Install the django-anymail app. It's easiest to install from PyPI using pip:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ pip install django-anymail[sendgrid,sparkpost]
|
||||
|
||||
The `[sendgrid,sparkpost]` part of that command tells pip you also
|
||||
want to install additional packages required for those ESPs.
|
||||
You can give one or more comma-separated, lowercase ESP names.
|
||||
(Most ESPs don't have additional requirements, so you can often
|
||||
just skip this. Or change your mind later. Anymail will let you know
|
||||
if there are any missing dependencies when you try to use it.)
|
||||
The `[sendgrid,sparkpost]` part of that command tells pip you also
|
||||
want to install additional packages required for those ESPs.
|
||||
You can give one or more comma-separated, lowercase ESP names.
|
||||
(Most ESPs don't have additional requirements, so you can often
|
||||
just skip this. Or change your mind later. Anymail will let you know
|
||||
if there are any missing dependencies when you try to use it.)
|
||||
|
||||
|
||||
.. _backend-configuration:
|
||||
|
||||
Configuring Django's email backend
|
||||
----------------------------------
|
||||
|
||||
To use Anymail for sending email, edit your Django project's :file:`settings.py`:
|
||||
|
||||
1. Add :mod:`anymail` to your :setting:`INSTALLED_APPS` (anywhere in the list):
|
||||
2. Edit your Django project's :file:`settings.py`, and add :mod:`anymail`
|
||||
to your :setting:`INSTALLED_APPS` (anywhere in the list):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -37,8 +32,8 @@ To use Anymail for sending email, edit your Django project's :file:`settings.py`
|
||||
# ...
|
||||
]
|
||||
|
||||
2. Add an :setting:`ANYMAIL` settings dict, substituting the appropriate settings for
|
||||
your ESP:
|
||||
3. Also in :file:`settings.py`, add an :setting:`ANYMAIL` settings dict,
|
||||
substituting the appropriate settings for your ESP. E.g.:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -49,7 +44,20 @@ To use Anymail for sending email, edit your Django project's :file:`settings.py`
|
||||
The exact settings vary by ESP.
|
||||
See the :ref:`supported ESPs <supported-esps>` section for specifics.
|
||||
|
||||
3. Change your existing Django :setting:`EMAIL_BACKEND` to the Anymail backend
|
||||
Then continue with either or both of the next two sections, depending
|
||||
on which Anymail features you want to use.
|
||||
|
||||
|
||||
.. _backend-configuration:
|
||||
|
||||
Configuring Django's email backend
|
||||
----------------------------------
|
||||
|
||||
To use Anymail for *sending* email from Django, make additional changes
|
||||
in your project's :file:`settings.py`. (Skip this section if you are only
|
||||
planning to *receive* email.)
|
||||
|
||||
1. 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
|
||||
@@ -60,25 +68,27 @@ To use Anymail for sending email, edit your Django project's :file:`settings.py`
|
||||
use :ref:`multiple Anymail backends <multiple-backends>` to send particular
|
||||
messages through different ESPs.)
|
||||
|
||||
Finally, 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.)
|
||||
2. 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.)
|
||||
|
||||
With the settings above, you are ready to send outgoing email through your ESP.
|
||||
If you also want to enable status tracking, continue with the
|
||||
optional settings below. Otherwise, skip ahead to :ref:`sending-email`.
|
||||
If you also want to enable status tracking or inbound handling, continue with the
|
||||
settings below. Otherwise, skip ahead to :ref:`sending-email`.
|
||||
|
||||
|
||||
.. _webhooks-configuration:
|
||||
|
||||
Configuring status tracking webhooks (optional)
|
||||
-----------------------------------------------
|
||||
Configuring tracking and inbound webhooks
|
||||
-----------------------------------------
|
||||
|
||||
Anymail can optionally connect to your ESP's event webhooks to notify your app
|
||||
of status like bounced and rejected emails, successful delivery, message opens
|
||||
and clicks, and other tracking.
|
||||
Anymail can optionally connect to your ESP's event webhooks to notify your app of:
|
||||
|
||||
If you aren't using Anymail's webhooks, skip this section.
|
||||
* status tracking events for sent email, like bounced or rejected messages,
|
||||
successful delivery, message opens and clicks, etc.
|
||||
* inbound message events, if you are set up to receive email through your ESP
|
||||
|
||||
Skip this section if you won't be using Anymail's webhooks.
|
||||
|
||||
.. warning::
|
||||
|
||||
@@ -87,14 +97,13 @@ If you aren't using Anymail's webhooks, skip this section.
|
||||
that could expose your users' emails and other private information,
|
||||
or subject your app to malicious input data.
|
||||
|
||||
At a minimum, your site should **use SSL** (https), and you should
|
||||
At a minimum, your site should **use https** and you should
|
||||
configure **webhook authorization** as described below.
|
||||
|
||||
See :ref:`securing-webhooks` for additional information.
|
||||
|
||||
|
||||
If you want to use Anymail's status tracking webhooks, follow the steps above
|
||||
to :ref:`configure an Anymail backend <backend-configuration>`, and then:
|
||||
If you want to use Anymail's inbound or tracking webhooks:
|
||||
|
||||
1. In your :file:`settings.py`, add
|
||||
:setting:`WEBHOOK_AUTHORIZATION <ANYMAIL_WEBHOOK_AUTHORIZATION>`
|
||||
@@ -148,31 +157,33 @@ to :ref:`configure an Anymail backend <backend-configuration>`, and then:
|
||||
3. Enter the webhook URL(s) into your ESP's dashboard or control panel.
|
||||
In most cases, the URL will be:
|
||||
|
||||
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/{esp}/tracking/`
|
||||
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/{esp}/{type}/`
|
||||
|
||||
* "https" (rather than http) is *strongly recommended*
|
||||
* *random:random* is the WEBHOOK_AUTHORIZATION string you created in step 1
|
||||
* *yoursite.example.com* is your Django site
|
||||
* "anymail" is the url prefix (from step 2)
|
||||
* *esp* is the lowercase name of your ESP (e.g., "sendgrid" or "mailgun")
|
||||
* "tracking" is used for Anymail's sent-mail event tracking webhooks
|
||||
* *type* is either "tracking" for Anymail's sent-mail event tracking webhooks,
|
||||
or "inbound" for receiving email
|
||||
|
||||
Some ESPs support different webhooks for different tracking events. You can
|
||||
usually enter the same Anymail webhook URL for all of them (or all that you
|
||||
want to receive). But be sure to check the specific details for your ESP
|
||||
under :ref:`supported-esps`.
|
||||
usually enter the same Anymail *tracking* webhook URL for all of them (or all that you
|
||||
want to receive)---but be sure to use the separate *inbound* URL for inbound webhooks.
|
||||
And always check the specific details for your ESP under :ref:`supported-esps`.
|
||||
|
||||
Also, some ESPs try to validate the webhook URL immediately when you enter it.
|
||||
If so, you'll need to deploy your Django project to your live server before you
|
||||
can complete this step.
|
||||
|
||||
Some WSGI servers may need additional settings to pass HTTP authorization headers
|
||||
through to Django. For example, Apache with `mod_wsgi`_ requires
|
||||
`WSGIPassAuthorization On`, else Anymail will complain about "missing or invalid
|
||||
basic auth" when your webhook is called.
|
||||
Some WSGI servers may need additional settings to pass HTTP authorization headers
|
||||
through to Django. For example, Apache with `mod_wsgi`_ requires
|
||||
`WSGIPassAuthorization On`, else Anymail will complain about "missing or invalid
|
||||
basic auth" when your webhook is called.
|
||||
|
||||
See :ref:`event-tracking` for information on creating signal handlers and the
|
||||
status tracking events you can receive.
|
||||
status tracking events you can receive. See :ref:`inbound` for information on
|
||||
receiving inbound message events.
|
||||
|
||||
.. _mod_wsgi: http://modwsgi.readthedocs.io/en/latest/configuration-directives/WSGIPassAuthorization.html
|
||||
|
||||
|
||||
@@ -17,5 +17,6 @@ Problems? We have some :ref:`troubleshooting` info that may help.
|
||||
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>`
|
||||
|
||||
Reference in New Issue
Block a user