django-anymail icon indicating copy to clipboard operation
django-anymail copied to clipboard

[Feature Request] Add SMTP Backend with signal support

Open roniemartinez opened this issue 3 years ago • 3 comments

It was recommended in the docs to use the default Django EmailBackend for SMTP. However, by doing this, we are losing the advantages of the pre- and post-send signals.

The use-case that we have right now is we are using a mail catcher on the local setup (which is SMTP) and then switches our backend to Mailjet on production.

The issue is that we had to write our own SMTP backend to get the same behavior as one of the Anymail-supported backends' pre- and post-send.

Anymail has the test and console backends but is missing the SMTP backend.

roniemartinez avatar Jun 17 '22 15:06 roniemartinez

I'm open to the idea of an Anymail wrapper for Django's built-in SMTP EmailBackend if you'd like to submit a PR.

To become something officially supported, it would need to handle (correctly implement or warn about unsupported attempts to use) all of Anymail's added features that would apply to SMTP email, including:

  • Pre- and post-send signals (your use case)
  • Adding anymail_status to sent EmailMessage objects
  • Global send defaults (which can be used with standard EmailMessage properties, like bcc; ideally these could be made to work, but it could also just warn on use, like the Amazon SES backend does now)
  • Common ESP features (metadata, tags, etc.) that aren't supported by standard SMTP servers—it would need to warn on use of these

Another, simpler option might be providing a mixin class that could be used with any other standard backend (Django's or third-party), and that just adds the pre- and post-send signal logic. Since this wouldn't claim to be a complete "Anymail SMTP backend," it wouldn't need to implement some of Anymail's more complicated features.

[FWIW, Anymail's console backend doesn't handle all of the features above, and the current implementation is a bit of a hack that leaks memory if used outside test code (which is why it's not covered in the docs). There's never really been a goal for Anymail to duplicate all of Django's backends—and we're also missing Django's file EmailBackend.]

medmunds avatar Jul 01 '22 23:07 medmunds

@medmunds we are using this now though I am not sure with our implementation because we just extended the Anymail's Test Backend and added Django's EmailBackend. It works for us but we probably missed a few things.

import uuid

from django.core.mail.backends.smtp import EmailBackend as DjangoEmailBackend

from anymail.message import AnymailRecipientStatus
from anymail.backends.test import EmailBackend as AnymailTestBackend


class AnymailSmtpBackend(AnymailTestBackend, DjangoEmailBackend):
    """
    Anymail backend that send messages to SMTP, while retaining anymail statuses and signals.
    """

    def get_esp_message_id(self, message):
        return str(uuid.uuid4())

    def open(self):
        return DjangoEmailBackend.open(self)

    def close(self):
        return DjangoEmailBackend.close(self)

    def post_to_esp(self, payload, message):
        status = "sent" if DjangoEmailBackend._send(self, message) else "failed"
        recipient_status = AnymailRecipientStatus(message_id=self.get_esp_message_id(message), status=status)
        return {"recipient_status": {email: recipient_status for email in payload.recipient_emails}}

roniemartinez avatar Jul 04 '22 09:07 roniemartinez

@roniemartinez if it works for your purposes, there's nothing wrong with that.

There are a few reasons I wouldn't want to use that approach in an officially-documented-and-supported Anymail backend:

  • It's calling a private _send() method on Django's smtp.EmailBackend. (Although that private API hasn't changed in a decade, Django strongly discourages third-party packages from relying on underscore-prefixed private APIs, as they can be changed without notice in Django patch releases.)
  • It doesn't support Anymail's global send defaults.
  • It doesn't warn on use of Anymail features like metadata that aren't supported by SMTP.

Also, for an SMTP backend, the appropriate message-id would probably be the value of the Message-ID header generated as Django's smtp.EmailBackend sends the message, rather than a random uuid.

Again, what you're doing to make your dev env work with a mail catcher is fine, but there'd be a bunch of other stuff to work through for a supported Anymail SMTP backend.

medmunds avatar Jul 15 '22 16:07 medmunds