mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
378 lines
13 KiB
ReStructuredText
378 lines
13 KiB
ReStructuredText
.. _resend-backend:
|
|
|
|
Resend
|
|
======
|
|
|
|
Anymail integrates Django with the `Resend`_ transactional
|
|
email service, using their `send-email API`_ endpoint.
|
|
|
|
.. versionadded:: 10.2
|
|
|
|
.. _Resend: https://resend.com/
|
|
.. _send-email API: https://resend.com/docs/api-reference/emails/send-email
|
|
|
|
|
|
.. _resend-installation:
|
|
|
|
Installation
|
|
------------
|
|
|
|
Anymail uses the :pypi:`svix` package to validate Resend webhook signatures.
|
|
If you will use Anymail's :ref:`status tracking <event-tracking>` webhook
|
|
with Resend, and you want to use webhook signature validation, be sure
|
|
to include the ``[resend]`` option when you install Anymail:
|
|
|
|
.. code-block:: console
|
|
|
|
$ python -m pip install 'django-anymail[resend]'
|
|
|
|
(Or separately run ``python -m pip install svix``.)
|
|
|
|
The svix package pulls in several other dependencies, so its use
|
|
is optional in Anymail. See :ref:`resend-webhooks` below for details.
|
|
To avoid installing svix with Anymail, just omit the ``[resend]`` option.
|
|
|
|
|
|
Settings
|
|
--------
|
|
|
|
.. rubric:: EMAIL_BACKEND
|
|
|
|
To use Anymail's Resend backend, set:
|
|
|
|
.. code-block:: python
|
|
|
|
EMAIL_BACKEND = "anymail.backends.resend.EmailBackend"
|
|
|
|
in your settings.py.
|
|
|
|
|
|
.. setting:: ANYMAIL_RESEND_API_KEY
|
|
|
|
.. rubric:: RESEND_API_KEY
|
|
|
|
Required for sending. An API key from your `Resend API Keys`_.
|
|
Anymail needs only "sending access" permission; "full access" is not recommended.
|
|
|
|
.. code-block:: python
|
|
|
|
ANYMAIL = {
|
|
...
|
|
"RESEND_API_KEY": "re_...",
|
|
}
|
|
|
|
Anymail will also look for ``RESEND_API_KEY`` at the
|
|
root of the settings file if neither ``ANYMAIL["RESEND_API_KEY"]``
|
|
nor ``ANYMAIL_RESEND_API_KEY`` is set.
|
|
|
|
.. _Resend API Keys: https://resend.com/api-keys
|
|
|
|
|
|
.. setting:: ANYMAIL_RESEND_SIGNING_SECRET
|
|
|
|
.. rubric:: RESEND_SIGNING_SECRET
|
|
|
|
The Resend webhook signing secret used to verify webhook posts.
|
|
Recommended if you are using activity tracking, otherwise not necessary.
|
|
(This is separate from Anymail's
|
|
:setting:`WEBHOOK_SECRET <ANYMAIL_WEBHOOK_SECRET>` setting.)
|
|
|
|
Find this in your Resend `Webhooks settings`_: after adding
|
|
a webhook, click into its management page and look for "signing secret"
|
|
near the top.
|
|
|
|
.. code-block:: python
|
|
|
|
ANYMAIL = {
|
|
...
|
|
"RESEND_SIGNING_SECRET": "whsec_...",
|
|
}
|
|
|
|
If you provide this setting, the svix package is required.
|
|
See :ref:`resend-installation` above.
|
|
|
|
|
|
.. setting:: ANYMAIL_RESEND_API_URL
|
|
|
|
.. rubric:: RESEND_API_URL
|
|
|
|
The base url for calling the Resend API.
|
|
|
|
The default is ``RESEND_API_URL = "https://api.resend.com/"``.
|
|
(It's unlikely you would need to change this.)
|
|
|
|
.. _Webhooks settings: https://resend.com/webhooks
|
|
|
|
|
|
.. _resend-quirks:
|
|
|
|
Limitations and quirks
|
|
----------------------
|
|
|
|
Resend does not support a few features offered by some other ESPs,
|
|
and can have unexpected behavior for some common use cases.
|
|
|
|
Anymail normally raises an :exc:`~anymail.exceptions.AnymailUnsupportedFeature`
|
|
error when you try to send a message using features that Resend doesn't support.
|
|
You can tell Anymail to suppress these errors and send the messages
|
|
anyway---see :ref:`unsupported-features`.
|
|
|
|
**Restricted characters in ``from_email`` display names**
|
|
Resend's API does not accept many email address display names
|
|
(a.k.a. "friendly names" or "real names") formatted according
|
|
to the relevant standard (:rfc:`5322`). Anymail implements a
|
|
workaround for the ``to``, ``cc``, ``bcc`` and ``reply_to``
|
|
fields, but Resend rejects attempts to use this workaround
|
|
for ``from_email`` display names.
|
|
|
|
These characters will cause problems in a *From* address display name:
|
|
|
|
* Double quotes (``"``) and some other punctuation characters
|
|
can cause a "Resend API response 422" error complaining of an
|
|
"Invalid \`from\` field", or can result in a garbled *From* name
|
|
(missing segments, additional punctuation inserted) in the
|
|
resulting message.
|
|
* A question mark immediately followed by any alphabetic character
|
|
(e.g., ``?u``) will cause a "Resend API response 451" security error
|
|
complaining that "The email payload contain invalid characters".
|
|
(This behavior prevents use of standard :rfc:`2047` encoded words
|
|
in *From* display names---which is the workaround Anymail implements
|
|
for other address fields.)
|
|
|
|
There may be other character combinations that also cause problems.
|
|
If you need to include punctuation in a *From* display name, be sure
|
|
to verify the results. (The issues were reported to Resend in October, 2023.)
|
|
|
|
**Attachment filename determines content type**
|
|
Resend determines the content type of an attachment from its filename extension.
|
|
|
|
If you try to send an attachment without a filename, Anymail will substitute
|
|
"attachment\ *.ext*" using an appropriate *.ext* for the content type.
|
|
|
|
If you try to send an attachment whose content type doesn't match its filename
|
|
extension, Resend will change the content type to match the extension.
|
|
(E.g., the filename "data.txt" will always be sent as "text/plain",
|
|
even if you specified a "text/csv" content type.)
|
|
|
|
**No inline images**
|
|
Resend's API does not provide a mechanism to send inline content
|
|
or to specify :mailheader:`Content-ID` for an attachment.
|
|
|
|
**Anymail tags and metadata are exposed to recipient**
|
|
Anymail implements its normalized :attr:`~anymail.message.AnymailMessage.tags`
|
|
and :attr:`~anymail.message.AnymailMessage.metadata` features for Resend
|
|
using custom email headers. That means they can be visible to recipients
|
|
via their email app's "show original message" (or similar) command.
|
|
**Do not include sensitive data in tags or metadata.**
|
|
|
|
Resend also offers a feature it calls "tags", which allows arbitrary key-value
|
|
data to be tracked with a sent message (similar Anymail's
|
|
:attr:`~anymail.message.AnymailMessage.metadata`). Resend's native tags
|
|
are *not* exposed to recipients, but they have significant restrictions
|
|
on character set and length (for both keys and values).
|
|
|
|
If you want to use Resend's native tags with Anymail, you can send them
|
|
using :ref:`esp_extra <resend-esp-extra>`, and retrieve them in a status
|
|
tracking webhook using :ref:`esp_event <resend-esp-event>`. (The linked
|
|
sections below include examples.)
|
|
|
|
**No stored templates or batch sending**
|
|
Resend does not currently offer ESP stored templates or merge capabilities,
|
|
including Anymail's
|
|
:attr:`~anymail.message.AnymailMessage.merge_data`,
|
|
:attr:`~anymail.message.AnymailMessage.merge_global_data`,
|
|
:attr:`~anymail.message.AnymailMessage.merge_metadata`, and
|
|
:attr:`~anymail.message.AnymailMessage.template_id` features.
|
|
(Resend's current template feature is only supported in node.js,
|
|
using templates that are rendered in their API client.)
|
|
|
|
**No click/open tracking overrides**
|
|
Resend does not support :attr:`~anymail.message.AnymailMessage.track_clicks`
|
|
or :attr:`~anymail.message.AnymailMessage.track_opens`. Its
|
|
tracking features can only be configured at the domain level
|
|
in Resend's control panel.
|
|
|
|
**No delayed sending**
|
|
Resend does not support :attr:`~anymail.message.AnymailMessage.send_at`.
|
|
|
|
**No envelope sender**
|
|
Resend does not support specifying the
|
|
:attr:`~anymail.message.AnymailMessage.envelope_sender`.
|
|
|
|
**Status tracking does not identify recipient**
|
|
If you send a message with multiple recipients (to, cc, and/or bcc),
|
|
Resend's status webhooks do not identify which recipient applies
|
|
for an event. See the :ref:`note below <resend-tracking-recipient>`.
|
|
|
|
|
|
.. _resend-api-rate-limits:
|
|
|
|
API rate limits
|
|
---------------
|
|
Resend provides `rate limit headers`_ with each API call response.
|
|
To access them after a successful send, use (e.g.,)
|
|
``message.anymail_status.esp_response.headers["ratelimit-remaining"]``.
|
|
|
|
If you exceed a rate limit, you'll get an :exc:`~anymail.exceptions.AnymailAPIError`
|
|
with ``error.status_code == 429``, and can determine how many seconds to wait
|
|
from ``error.response.headers["retry-after"]``.
|
|
|
|
.. _rate limit headers:
|
|
https://resend.com/docs/api-reference/introduction#rate-limit
|
|
|
|
|
|
.. _resend-esp-extra:
|
|
|
|
exp_extra support
|
|
-----------------
|
|
|
|
Anymail's Resend backend will pass :attr:`~anymail.message.AnymailMessage.esp_extra`
|
|
values directly to Resend's `send-email API`_. Example:
|
|
|
|
.. code-block:: python
|
|
|
|
message = AnymailMessage(...)
|
|
message.esp_extra = {
|
|
# Use Resend's native "tags" feature
|
|
# (be careful about character set restrictions):
|
|
"tags": [
|
|
{"name": "Co_Brand", "value": "Acme_Inc"},
|
|
{"name": "Feature_Flag_1", "value": "test_22_a"},
|
|
],
|
|
}
|
|
|
|
|
|
.. _resend-webhooks:
|
|
|
|
Status tracking webhooks
|
|
------------------------
|
|
|
|
Anymail's normalized :ref:`status tracking <event-tracking>` works
|
|
with Resend's webhooks.
|
|
|
|
Resend implements webhook signing, using the :pypi:`svix` package
|
|
for signature validation (see :ref:`resend-installation` above). You have
|
|
three options for securing the status tracking webhook:
|
|
|
|
* Use Resend's webhook signature validation, by setting
|
|
:setting:`RESEND_SIGNING_SECRET <ANYMAIL_RESEND_SIGNING_SECRET>`
|
|
(requires the svix package)
|
|
* Use Anymail's shared secret validation, by setting
|
|
:setting:`WEBHOOK_SECRET <ANYMAIL_WEBHOOK_SECRET>`
|
|
(does not require svix)
|
|
* Use both
|
|
|
|
Signature validation is recommended, unless you do not want to add
|
|
svix to your dependencies.
|
|
|
|
To configure Anymail status tracking for Resend,
|
|
add a new webhook endpoint to your `Resend Webhooks settings`_:
|
|
|
|
* For the "Endpoint URL", enter one of these
|
|
(where *yoursite.example.com* is your Django site).
|
|
|
|
If are *not* using Anymail's shared webhook secret:
|
|
|
|
:samp:`https://{yoursite.example.com}/anymail/resend/tracking/`
|
|
|
|
Or if you *are* using Anymail's :setting:`WEBHOOK_SECRET <ANYMAIL_WEBHOOK_SECRET>`,
|
|
include the *random:random* shared secret in the URL:
|
|
|
|
:samp:`https://{random}:{random}@{yoursite.example.com}/resend/tracking/`
|
|
|
|
* For "Events to listen", select any or all events you want to track.
|
|
|
|
* Click the "Add" button.
|
|
|
|
Then, if you are using Resend's webhook signature validation (with svix),
|
|
add the webhook signing secret to your Anymail settings:
|
|
|
|
* Still on the `Resend Webhooks settings`_ page, click into the
|
|
webhook endpoint URL you added above,
|
|
and copy the "signing secret" listed near the top of the page.
|
|
|
|
* Add that to your settings.py ``ANYMAIL`` settings as
|
|
:setting:`RESEND_SIGNING_SECRET <ANYMAIL_RESEND_SIGNING_SECRET>`:
|
|
|
|
.. code-block:: python
|
|
|
|
ANYMAIL = {
|
|
# ...
|
|
"RESEND_SIGNING_SECRET": "whsec_..."
|
|
}
|
|
|
|
Resend will report these Anymail
|
|
:attr:`~anymail.signals.AnymailTrackingEvent.event_type`\s:
|
|
sent, delivered, bounced, deferred, complained, opened, and clicked.
|
|
|
|
|
|
.. _resend-tracking-recipient:
|
|
|
|
.. note::
|
|
|
|
**Multiple recipients not recommended with tracking**
|
|
|
|
If you send a message with multiple recipients (to, cc, and/or bcc),
|
|
you will receive separate events (delivered, bounced, opened, etc.)
|
|
for *every* recipient. But Resend does not identify *which* recipient
|
|
applies for a particular event.
|
|
|
|
The :attr:`event.recipient <anymail.signals.AnymailTrackingEvent.recipient>`
|
|
will always be the first ``to`` email, but the event might actually have been
|
|
generated by some other recipient.
|
|
|
|
To avoid confusion, it's best to send each message to exactly one ``to``
|
|
address, and avoid using cc or bcc.
|
|
|
|
|
|
.. _resend-esp-event:
|
|
|
|
The status tracking event's :attr:`~anymail.signals.AnymailTrackingEvent.esp_event`
|
|
field will be the parsed Resend webhook payload. For example, if you provided
|
|
Resend's native "tags" via :ref:`esp_extra <resend-esp-extra>` when sending,
|
|
you can retrieve them in your tracking signal receiver like this:
|
|
|
|
.. code-block:: python
|
|
|
|
@receiver(tracking)
|
|
def handle_tracking(sender, event, esp_name, **kwargs):
|
|
...
|
|
resend_tags = event.esp_event.get("tags", {})
|
|
# resend_tags will be a flattened dict (not
|
|
# the name/value list used when sending). E.g.:
|
|
# {"Co_Brand": "Acme_Inc", "Feature_Flag_1": "test_22_a"}
|
|
|
|
|
|
.. _Resend Webhooks settings: https://resend.com/webhooks
|
|
|
|
|
|
.. _resend-inbound:
|
|
|
|
Inbound
|
|
-------
|
|
|
|
Resend does not currently support inbound email.
|
|
|
|
|
|
.. _resend-troubleshooting:
|
|
|
|
Troubleshooting
|
|
---------------
|
|
|
|
If Anymail's Resend integration isn't behaving like you expect,
|
|
Resend's dashboard includes diagnostic logs that can help
|
|
isolate the problem:
|
|
|
|
* `Resend Logs page`_ lists every call received by Resend's API
|
|
* `Resend Emails page`_ shows every event related to email
|
|
sent through Resend
|
|
* `Resend Webhooks page`_ shows every attempt by Resend to call
|
|
your webhook (click into a webhook endpoint url to see
|
|
the logs for that endpoint)
|
|
|
|
.. _Resend Emails page: https://resend.com/emails
|
|
.. _Resend Logs page: https://resend.com/logs
|
|
.. _Resend Webhooks page: https://resend.com/webhooks
|
|
|
|
See Anymail's :ref:`troubleshooting` docs for additional suggestions.
|