.. _mailersend-backend: MailerSend ========== Anymail integrates Django with the `MailerSend`_ transactional email service, using their `email API`_ endpoint. .. _MailerSend: https://www.mailersend.com/ .. _email API: https://developers.mailersend.com/api/v1/email.html Settings -------- .. rubric:: EMAIL_BACKEND To use Anymail's MailerSend backend, set: .. code-block:: python EMAIL_BACKEND = "anymail.backends.mailersend.EmailBackend" in your settings.py. .. setting:: ANYMAIL_MAILERSEND_API_TOKEN .. rubric:: MAILERSEND_API_TOKEN Required for sending. A MailerSend API token generated in your MailerSend `Email domains settings`_. For the token permission level, "custom access" is recommended, with full access to email and *no access* for all other features. .. code-block:: python ANYMAIL = { ... "MAILERSEND_API_TOKEN": "", } Anymail will also look for ``MAILERSEND_API_TOKEN`` at the root of the settings file if neither ``ANYMAIL["MAILERSEND_API_TOKEN"]`` nor ``ANYMAIL_MAILERSEND_API_TOKEN`` is set. If your Django project sends email from multiple MailerSend domains, you will need a separate API token for each domain. Use the token matching your :setting:`DEFAULT_FROM_EMAIL` domain in settings.py, and then override where necessary for individual emails by setting ``"api_token"`` in the message's :ref:`esp_extra `. (You could centralize this logic using Anymail's :ref:`pre-send-signal`.) .. setting:: ANYMAIL_MAILERSEND_BATCH_SEND_MODE .. rubric:: MAILERSEND_BATCH_SEND_MODE If you are using Anymail's :attr:`~anymail.message.AnymailMessage.merge_data` with multiple recipients (":ref:`batch sending `"), set this to indicate how to handle the batch. See :ref:`mailersend-batch-send` below for more information. Choices are ``"use-bulk-email"`` or ``"expose-to-list"``. The default ``None`` will raise an error if :attr:`~!anymail.message.AnymailMessage.merge_data` is used with more than one ``to`` recipient. .. code-block:: python ANYMAIL = { ... "MAILERSEND_BATCH_SEND_MODE": "use-bulk-email", } .. setting:: ANYMAIL_MAILERSEND_SIGNING_SECRET .. rubric:: MAILERSEND_SIGNING_SECRET The MailerSend webhook signing secret needed to verify webhook posts. Required if you are using activity tracking, otherwise not necessary. (This is separate from Anymail's :setting:`WEBHOOK_SECRET ` setting.) Find this in your MailerSend `Email domains settings`_: after adding a webhook, look for the "signing secret" on the webhook's management page. .. code-block:: python ANYMAIL = { ... "MAILERSEND_SIGNING_SECRET": "", } MailerSend generates a unique secret for each webhook; if you edit your webhook you will need to update this setting with the new signing secret. (Also, inbound routes use a *different* secret, with a different setting---see below.) .. setting:: ANYMAIL_MAILERSEND_INBOUND_SECRET .. rubric:: MAILERSEND_INBOUND_SECRET The MailerSend inbound route secret needed to verify inbound notifications. Required if you are using inbound routing, otherwise not necessary. Find this in your MailerSend `Email domains settings`_: after you have added an inbound route, look for the "secret" immediately below the route url on the management page for that inbound route. .. code-block:: python ANYMAIL = { ... "MAILERSEND_INBOUND_SECRET": "", } MailerSend generates a unique secret for each inbound route url; if you edit your route you will need to update this setting with the new secret. (Also, activity tracking webhooks use a *different* secret, with a different setting---see above.) .. setting:: ANYMAIL_MAILERSEND_API_URL .. rubric:: MAILERSEND_API_URL The base url for calling the MailerSend API. The default is ``MAILERSEND_API_URL = "https://api.mailersend.com/v1/"``. (It's unlikely you would need to change this.) .. _Email domains settings: https://app.mailersend.com/domains .. _mailersend-esp-extra: exp_extra support ----------------- Anymail's MailerSend backend will pass :attr:`~anymail.message.AnymailMessage.esp_extra` values directly to MailerSend's `email API`_. In addition, you can override the :setting:`MAILERSEND_API_TOKEN ` for an individual message by providing ``"api-token"``, and :setting:`MAILERSEND_BATCH_SEND_MODE ` by providing ``"batch-send-mode"`` in the :attr:`~!anymail.message.AnymailMessage.esp_extra` dict. Example: .. code-block:: python message = AnymailMessage(...) message.esp_extra = { # override your MailerSend domain's content tracking default: "settings": {"track_content": False}, # use a different MAILERSEND_API_TOKEN for this message: "api_token": MAILERSEND_API_TOKEN_FOR_MARKETING_DOMAIN, # override the MAILERSEND_BATCH_SEND_MODE setting # just for this message: "batch_send_mode": "use-bulk-email", } Nested values are merged deeply. When sending using MailerSend's bulk-email API endpoint, the :attr:`~!anymail.message.AnymailMessage.esp_extra` params are merged into the payload for every individual message in the batch. .. _mailersend-quirks: Limitations and quirks ---------------------- MailerSend does not support a few features offered by some other ESPs. Anymail normally raises an :exc:`~anymail.exceptions.AnymailUnsupportedFeature` error when you try to send a message using features that MailerSend doesn't support You can tell Anymail to suppress these errors and send the messages anyway -- see :ref:`unsupported-features`. **Attachments require filenames, ignore content type** MailerSend requires every attachment (even inline ones) to have a filename. And it determines the content type of the attachment from the 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, MailerSend 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.) **Single Reply-To** MailerSend only supports a single Reply-To address. If your message has multiple reply addresses, you'll get an :exc:`~anymail.exceptions.AnymailUnsupportedFeature` error---or if you've enabled :setting:`ANYMAIL_IGNORE_UNSUPPORTED_FEATURES`, Anymail will use only the first one. **Limited extra headers** MailerSend does not allow most extra headers. There are two exceptions: * You can include :mailheader:`In-Reply-To` in extra headers, set to a message-id (without the angle brackets). * You can include :mailheader:`Precedence` in extra headers to override the "Add precedence bulk header" option from your MailerSend domain advanced settings (look under "More settings"). Anymail will set MailerSend's ``precedence_bulk`` param to ``true`` if your extra headers have :mailheader:`Precedence` set to ``"bulk"`` or ``"list"`` or ``"junk"``, or ``false`` for any other value. Any other extra headers will raise an :exc:`~anymail.exceptions.AnymailUnsupportedFeature` error. **No metadata support** MailerSend does not support Anymail's :attr:`~anymail.message.AnymailMessage.metadata` or :attr:`~anymail.message.AnymailMessage.merge_metadata` features. **No envelope sender overrides** MailerSend does not support overriding :attr:`~anymail.message.AnymailMessage.envelope_sender` on individual messages. (To use a `MailerSend sender identity`_, set the verified identity's email address as the message's :attr:`~!django.core.email.message.EmailMessage.from_email`.) .. _MailerSend sender identity: https://www.mailersend.com/help/send-email-on-behalf-of-clients **API rate limits** MailerSend 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["x-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://developers.mailersend.com/general.html#rate-limits .. _mailersend-templates: Batch sending/merge and ESP templates ------------------------------------- MailerSend supports :ref:`ESP stored templates `, on-the-fly templating, and :ref:`batch sending ` with per-recipient merge data. MailerSend's approaches to batch sending don't align perfectly with Anymail's; be sure to read :ref:`mailersend-batch-send` below to understand the options. MailerSend offers two different syntaxes for substituting data into templates: "`simple personalization`_" and "`advanced personalization`_." Anymail supports *only* the more flexible advanced personalization syntax. If you have MailerSend templates using the "simple" syntax (``{$variable_name}``), you'll need to convert them to the "advanced" syntax (``{{ variable_name }}``) for use with Anymail's :attr:`~anymail.message.AnymailMessage.merge_data` and :attr:`~anymail.message.AnymailMessage.merge_global_data`. Here's an example defining an on-the-fly template that uses MailerSend advanced personalization variables: .. code-block:: python message = EmailMessage( from_email="shipping@example.com", subject="Your order {{ order_no }} has shipped", body="""Hi {{ name }}, We shipped your order {{ order_no }} on {{ ship_date }}.""", to=["alice@example.com", "Bob "] ) # (you'd probably also set a similar html body with variables) message.merge_data = { "alice@example.com": {"name": "Alice", "order_no": "12345"}, "bob@example.com": {"name": "Bob", "order_no": "54321"}, } message.merge_global_data = { "ship_date": "May 15" # Anymail maps globals to all recipients } # (see discussion of batch-send-mode below) message.esp_extra = { "batch-send-mode": "use-bulk-email" } To send the same message with a `MailerSend stored template`_ from your account, set :attr:`~anymail.message.AnymailMessage.template_id`, and omit any plain-text or html `~!django.core.mail.EmailMessage.body`. If you've set a subject in your MailerSend template's default settings, you can omit `~!django.core.mail.EmailMessage.subject` (otherwise you must include it). And if your template default settings specify the *From* email, that will override `~!django.core.mail.EmailMessage.from_email`. Example: .. code-block:: python message = EmailMessage( from_email="shipping@example.com", # (subject and body from template) to=["alice@example.com", "Bob "] ) message.template_id = "vzq12345678" # id of template in our account # ... set merge_data and merge_global_data as above MailerSend does not natively support global merge data. Anymail emulates the capability by copying any :attr:`~anymail.message.AnymailMessage.merge_global_data` values to every recipient. .. _simple personalization: https://www.mailersend.com/help/how-to-use-variables#simple-personalization .. _advanced personalization: https://www.mailersend.com/help/how-to-use-variables#advanced-personalization .. _MailerSend stored template: https://www.mailersend.com/help/how-to-create-a-template .. _mailersend-batch-send: Batch send mode ~~~~~~~~~~~~~~~ Anymail's model for :ref:`batch sending ` is that each recipient receives a separate email personalized for them, and that each recipient sees *only their own email address* in the message's :mailheader:`To` header. MailerSend has a `bulk-email API`_ that matches Anymail's batch sending model, but operates completely asynchronously, which can complicate status tracking and error handling. MailerSend also supports batch sending personalized emails through its regular `email API`_, which avoids the bulk-email limitations but exposes the entire :mailheader:`To` list to all recipients. If you want to use Anymail's :attr:`~anymail.message.AnymailMessage.merge_data` for batch sending to multiple `~!django.core.mail.EmailMessage.to` recipients, you must select one of these two approaches by specifying either ``"use-bulk-email"`` or ``"expose-to-list"`` in your Anymail :setting:`MAILERSEND_BATCH_SEND_MODE ` setting---or as ``"batch-send-mode"`` in the message's :ref:`esp_extra `. .. caution:: Using the ``"expose-to-list"`` MailerSend batch send mode will reveal *all* of the message's :mailheader:`To` email addresses to *every* recipient of the message. If you use the ``"use-bulk-email"`` MailerSend batch send mode: * The :attr:`message.anymail_status.status ` will be ``{"unknown"}``, because MailerSend detects errors and rejected recipients at a later time. * The :attr:`message.anymail_status.message_id ` will be a MailerSend ``bulk_email_id``, prefixed with ``"bulk:"`` to distinguish it from a regular ``message_id``. * You will need to poll MailerSend's `bulk-email status API`_ to determine whether the send was successful, partially successful, or failed, and to determine the :attr:`event.message_id ` that will be sent to status tracking webhooks. * Be aware that rate limits for the bulk-email API are significantly lower than MailerSend's regular email API. Rather than one of these batch sending options, an often-simpler approach is to loop over your recipient list and send a separate message for each. You can still use templates and :attr:`~!anymail.message.AnymailMessage.merge_data`: .. code-block:: python # How to "manually" send a batch of emails to one recipient at a time. # (There's no need to specify a MailerSend "batch-send-mode".) to_list = ["alice@example.com", "bob@example.com"] merge_data = { "alice@example.com": {"name": "Alice", "order_no": "12345"}, "bob@example.com": {"name": "Bob", "order_no": "54321"}, } merge_global_data = { "ship_date": "May 15", } for to_email in to_list: message = AnymailMessage( # just one recipient per message: to=[to_email], # provide template variables for this one recipient: merge_global_data = merge_global_data | merge_data[to_email], # any other attributes you want: template_id = "vzq12345678", from_email="shipping@example.com", ) try: message.send() except AnymailAPIError: # Handle error -- e.g., schedule for retry later. else: # Either successful send or to_email is rejected. # message.anymail_status will be {"queued"} or {"rejected"}. # message.anymail_status.message_id can be stored to match # with event.message_id in a status tracking signal receiver. .. _bulk-email API: https://developers.mailersend.com/api/v1/email.html#send-bulk-emails .. _bulk-email status API: https://developers.mailersend.com/api/v1/email.html#get-bulk-email-status .. _mailersend-webhooks: Status tracking webhooks ------------------------ If you are using Anymail's normalized :ref:`status tracking `, follow MailerSend's instructions to `add a webhook to your domain`_. * Enter this Anymail tracking URL as the webhook's "Endpoint URL" (where *yoursite.example.com* is your Django site): :samp:`https://{yoursite.example.com}/anymail/mailersend/tracking/` Because MailerSend implements webhook signing, it's not necessary to use Anymail's shared webhook secret for security with MailerSend webhooks. However, it doesn't hurt to use both. If you *have* set an Anymail :setting:`WEBHOOK_SECRET `, include that *random:random* shared secret in the webhook endpoint URL: :samp:`https://{random}:{random}@{yoursite.example.com}/anymail/mailersend/tracking/` * For "Events to send", select any or all events you want to track. * After you have saved the webhook, go back into MailerSend's webhook management page, and reveal and copy the MailerSend "webhook signing secret". Provide that in your settings.py ``ANYMAIL`` settings as :setting:`MAILERSEND_SIGNING_SECRET ` so that Anymail can verify calls to the webhook: .. code-block:: python ANYMAIL = { # ... "MAILERSEND_SIGNING_SECRET": "" } For troubleshooting, MailerSend provides a helpful log of calls to the webhook. See "`About webhook attempts`_" in their documentation for more details. .. note:: MailerSend has a relatively short three second timeout for webhook calls. Be sure to avoid any lengthy operations in your Anymail tracking signal receiver function, or MailerSend will consider the notification failed at retry it. The event's :attr:`~anymail.signals.AnymailTrackingEvent.event_id` field can help identify duplicate notifications. MailerSend retries webhook notifications only twice, with delays of 10 and then 100 seconds. If your webhook is ever offline for more than a couple minutes, you many miss some tracking events. You can use MailerSend's activity API to query for events that may have been missed. MailerSend will report these Anymail :attr:`~anymail.signals.AnymailTrackingEvent.event_type`\s: sent, delivered, bounced, complained, unsubscribed, opened, and clicked. The event's :attr:`~anymail.signals.AnymailTrackingEvent.esp_event` field will be the *complete* parsed MailerSend webhook payload, including an additional wrapper object not shown in their documentation. The activity data in MailerSend's `webhook payload example`_ is available as ``event.esp_event["data"]``. .. _add a webhook to your domain: https://www.mailersend.com/help/webhooks#adding-webhooks .. _About webhook attempts: https://www.mailersend.com/help/webhooks#webhook-attempts .. _webhook payload example: https://developers.mailersend.com/api/v1/webhooks.html#payload-example .. _mailersend-inbound: Inbound routing --------------- If you want to receive email from MailerSend through Anymail's normalized :ref:`inbound ` handling, follow MailerSend's guide to `How to set up an inbound route`_. * For "Route to" (in their step 8), enter this Anymail inbound route endpoint URL (where *yoursite.example.com* is your Django site): :samp:`https://{yoursite.example.com}/anymail/mailersend/inbound/` Because MailerSend signs its inbound notifications, it's not necessary to use Anymail's shared webhook secret for security with MailerSend inbound routing. However, it doesn't hurt to use both. If you *have* set an Anymail :setting:`WEBHOOK_SECRET `, include that *random:random* shared secret in the inbound route endpoint URL: :samp:`https://{random}:{random}@{yoursite.example.com}/anymail/mailersend/inbound/` * After you have saved the inbound route, go back into MailerSend's inbound route management page, and copy the "Secret" displayed immediately below the "Route to" URL. Provide that in your settings.py ``ANYMAIL`` settings as :setting:`MAILERSEND_INBOUND_SECRET ` so that Anymail can verify calls to the inbound endpoint: .. code-block:: python ANYMAIL = { # ... "MAILERSEND_INBOUND_SECRET": "" } Note that this is a *different* secret from the :setting:`MAILERSEND_SIGNING_SECRET ` used to verify activity tracking webhooks. If you are using both features, be sure to include both settings. For troubleshooting, MailerSend provides a helpful inbound activity log near the end of the route management page. See `Where to find inbound emails`_ in their docs for more details. .. note:: MailerSend imposes a three second limit on all notifications. If your inbound signal receiver function takes too long, MailerSend may think the notification failed. To avoid problems, it's essential you offload any lengthy operations to a background task. MailerSend does not retry failed inbound notifications. If your Django app is ever unreachable for any reason, **you will miss inbound mail** that arrives during that time. .. _How to set up an inbound route: https://www.mailersend.com/help/inbound-route .. _Where to find inbound emails: https://www.mailersend.com/help/inbound-route#where