django-anymail

repository·main·Indexed 23 days ago

https://github.com/anymail/django-anymail

A Django integration for transactional Email Service Providers (ESPs) that extends Django's built-in email functionality. It provides a consistent, portable API for features such as metadata, tags, tracking, and inbound message handling across various providers, including a specialized backend for Amazon SES.

Tokens
54.2K
Snippets
134
Records
233
Agent score
83%

What's inside django-anymail

  1. View supported Email Service Providers (ESPs)

    main

    Anymail supports a variety of Email Service Providers (ESPs). Each provider has specific configuration requirements, limitations, and quirks. Supported ESPs include:

    • amazon_ses
    • brevo
    • mailersend
    • mailgun
    • mailjet
    • mailtrap
    • mandrill
    • postal
    • postmark
    • resend
    • scaleway
    • sendgrid
    • sparkpost
    • unisender_go

    For detailed setup instructions and specific settings for any of these providers, refer to their individual documentation pages within the Anymail project.

  2. Memory considerations for inbound attachments

    main

    Anymail loads all attachment content into memory during processing.

    If you are handling large attachments, be aware that:

    1. You may be limited by the memory available to your application.
    2. You may need to increase Django's DATA_UPLOAD_MAX_MEMORY_SIZE setting to successfully receive larger payloads from your ESP.
  3. Compare Mailgun inbound message formats

    main

    Anymail supports two formats for Mailgun inbound messages. Using raw MIME is highly recommended.

    FeatureRaw MIME (Recommended)Fully-parsed (Not Recommended)
    URL Suffix/inbound_mime//inbound/
    AccuracyMost accurate representationLess accurate
    AttachmentsHandles attachments and inline images reliablyMay strip attachments/images due to Django multipart/form-data limitations
  4. Manage unsupported features in Payloads

    main

    Use self.unsupported_feature() when a feature requested by the user cannot be accurately communicated to the ESP API.

    Guidelines:

    • Unsupported: If the ESP API cannot accept the data (e.g., the user provides multiple tags but the ESP only accepts a single string).
    • Supported: If the ESP API can accept the data, even if the ESP documentation says it shouldn't (e.g., sending 10 tags when the docs say the limit is 3). Anymail should pass the data and let the ESP decide whether to error out.
  5. Resend limitations and unsupported features

    main

    The following features are not supported by Resend or have specific constraints when using the Anymail backend:

    • Attachment Filenames: Filename extensions must match the content type (mimetype). Anymail attempts to verify this, otherwise Resend may silently drop the message.
    • Tracking: track_clicks and track_opens are not supported; tracking must be configured at the domain level in Resend.
    • Delayed Sending: Attachments and batch sending are not supported when using send_at.
    • Batch Sending: Attachments are not supported when using merge_metadata.
    • Envelope Sender: envelope_sender is not supported.
    • Unicode Mailboxes: Resend does not support non-ASCII (EAI) mailboxes (the user part of the address).
  6. Handle batch sending and 'merge_data'

    main

    When implementing the serialize_data method in a Payload, you must handle the merge_data setting to support proper batching:

    • If merge_data is set: A separate message should be sent to each address in the to list. Recipients should not see the full list of other recipients.
    • If merge_data is NOT set: A single message should be sent containing all addresses in the To header.
  7. Manage tags and metadata with Amazon SES

    main

    Anymail provides two ways to associate data with messages:

    1. Custom Email Headers (Default): Anymail sends metadata as a JSON-encoded X-Metadata header and tags as X-Tag headers. These are available in Anymail tracking webhooks.
    2. SES Message Tags: These can be used for CloudWatch metrics. To use them, set AMAZON_SES_MESSAGE_TAG_NAME in your ANYMAIL settings to the desired tag name. Note that this only supports a single tag, and both the name and value must be alphanumeric, hyphen, or underscore only.

    For complex use cases involving multiple SES Message Tags, use the EmailTags (or DefaultEmailTags for templates) key within the esp_extra dictionary.

    ANYMAIL = {
        "AMAZON_SES_MESSAGE_TAG_NAME": "Type",
    }
    
    # This results in the SES Message Tag "Type": "Marketing"
    message = EmailMessage(...)
    message.tags = ["Marketing"]
    message.send()
  8. Use MailerSend advanced personalization for templates

    main

    MailerSend supports two personalization syntaxes. Anymail only supports the advanced personalization syntax.

    If your MailerSend templates use the simple syntax ({$variable_name}), you must convert them to the advanced syntax ({{ variable_name }}) to work with Anymail's merge_data and merge_global_data.

    Anymail emulates global merge data by copying merge_global_data values to every recipient, as MailerSend does not natively support global merge data.

    message = EmailMessage(
        from_email="shipping@example.com",
        subject="Your order {{ order_no }} has shipped",
        body="""Hi {{ name }},\n                We shipped your order {{ order_no }}\n                on {{ ship_date }}.""",
        to=["alice@example.com", "Bob <bob@example.com>"]
    )
    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"
    }
    message.esp_extra = {
        "batch-send-mode": "use-bulk-email"
    }
  9. Attach arbitrary metadata with metadata and merge_metadata

    main

    You can attach metadata to messages for later retrieval (e.g., via Anymail's status tracking webhooks).

    • metadata: A dict of metadata values applied to the entire message.
    • merge_metadata: A dict of per-recipient metadata values. The keys are recipient email addresses (address portion only), and the values are dictionaries of metadata.

    Best Practices & Warnings:

    • Portability: Use alphanumeric keys and string/numeric values.
    • Security: Some ESPs expose this metadata in email headers. Do not include sensitive data.
    • Precedence: If keys conflict, merge_metadata values take precedence over metadata for a specific recipient.
    # Global metadata
    message.metadata = {"customer": customer.id, "order": order.reference_number}
    
    # Per-recipient metadata
    message.to = ["wile@example.com", "Mr. Runner <rr@example.com>"]
    message.merge_metadata = {
        "wile@example.com": {"customer": 123, "order": "acme-zxyw"},
        "rr@example.com": {"customer": 45678, "order": "acme-wblt"},
    }
  10. Understand the AnymailInboundMessage class

    main

    The anymail.inbound.AnymailInboundMessage class is an extension of Python's standard email.message.EmailMessage. It is provided as the .message attribute of an AnymailInboundEvent to simplify handling inbound emails. It includes enhanced attributes for accessing envelope information, parsed email addresses, and content like HTML, plaintext, and attachments.

    Key features include:

    • Envelope Information: Access envelope_sender and envelope_recipient to see the actual routing addresses, which may differ from the From or To headers.
    • Parsed Addresses: The from_email attribute is converted to an anymail.utils.EmailAddress object, allowing easy access to addr_spec, display_name, domain, and username.
    • Content Access: Provides direct access to text (plaintext), html, attachments, and inlines (inline images).
    • Spam Metadata: Includes spam_score and spam_detected if provided by your ESP.
    • Compatibility: Since it inherits from EmailMessage, you can use all standard Python email methods like .walk(), .get_content_type(), and header mapping.
    >>> 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'
  11. Set per-recipient headers with merge_headers

    main

    If your ESP supports it, you can use merge_headers to provide unique email headers for each recipient in a multi-recipient message.

    When merge_headers is used, Anymail utilizes the ESP's batch sending option, ensuring each recipient receives an individual message and cannot see other recipients in the to list.

    • Keys: The keys in the merge_headers dictionary must be the recipient email addresses (address portion only).
    • Values: A dictionary of header fields and values for that recipient.
    • Defaults: If a header is defined in message.extra_headers but not in merge_headers, it will be applied to all recipients. If a header is defined in merge_headers for only some recipients, behavior for the others depends on the ESP (some include an empty header, others omit it).
    message.to = ["wile@example.com", "R. Runner <rr@example.com>"]
    message.extra_headers = {
        # Headers for all recipients
        "List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
    }
    message.merge_headers = {
        # Per-recipient headers
        "wile@example.com": {
            "List-Unsubscribe": "<https://example.com/unsubscribe/12345>",
        },
        "rr@example.com": {
            "List-Unsubscribe": "<https://example.com/unsubscribe/98765>",
        },
    }
  12. Handle CC and BCC behavior in Mandrill

    main

    Mandrill's handling of cc and bcc depends on the preserve_recipients option:

    • True: A single message is sent to all recipients. To and Cc headers list all addresses, and bcc addresses are blind copied.
    • False: Mandrill sends multiple copies (one per recipient). Each message contains only that specific recipient in the To header.

    Note: Anymail automatically sets preserve_recipients to False when using batch sending (per-recipient merge data). To override this for individual messages, use esp_extra:

    message.esp_extra = {"message": {"preserve_recipients": True}}