Improved SMTP backend for Django


Dev recipient and e-mail log are two things that are missing in django SMTP backend, but you can add them pretty easily.


Dev recipient for when you need to test the content/design of your e-mails.

E-mail log is useful if en e-mail gets lost, or you need to check how many are being sent or so.
I don't log any html/mime parts (html content or files), only text message and filenames. But you can add it if you really need it.

First we need an EmailLog model

from django.utils.translation import gettext_lazy as _
from django.db import models

class EmailLog(models.Model):
    ctime = models.DateTimeField(null=False, auto_now_add=True, verbose_name=_('Sent time'))
    sender = models.EmailField(null=False, blank=True, default='', verbose_name=_('From'))
    to = models.JSONField(null=False, blank=True, default=list)
    cc = models.JSONField(null=False, blank=True, default=list)
    bcc = models.JSONField(null=False, blank=True, default=list)
    reply_to = models.JSONField(null=False, blank=True, default=list)
    subject = models.CharField(null=False, max_length=1024)
    body = models.TextField(null=False, blank=True, default='')
    attachments = models.JSONField(null=False, blank=True, default=list)
    status = models.BooleanField(null=False, default=True, verbose_name=_('Sent'))
    exception = models.TextField(null=False, blank=True, default='')
    traceback = models.TextField(null=False, blank=True, default='')

    def __str__(self):
        return self.subject

Of course we need to run makemigrations and migrate!

Then we add it to

from django.contrib import admin

from .models import EmailLog

class EmailLogAdmin(admin.ModelAdmin):
    list_display = ("ctime", "subject", "status")
    actions = []
    list_filter = ["status"]
    search_fields = ["subject", "to", "body"]

    def has_add_permission(self, request, obj=None):
        return False

    def has_change_permission(self, request, obj=None):
        return False

    def has_delete_permission(self, request, obj=None):
        return False, EmailLogAdmin)

Then create a in your app with the new SMTP backend

import smtplib
import traceback
from email.mime.base import MIMEBase

from django.apps import apps
from django.utils.functional import cached_property
from django.core.mail.backends.smtp import EmailBackend
from django.conf import settings

class ImprovedSMTPEmailBackend(EmailBackend):
    uses EmailLog model for logging all outgoing e-mails
    if you use fail_silently=True and the connection fails, there will not be any log

    def email_log_model(self):
        return apps.get_model(settings.EMAIL_LOG_MODEL)

    def _send(self, email_message):
        fail_silently = self.fail_silently
        self.fail_silently = False

        log_object = self.email_log_model(
        attachments = []
        for attachment in email_message.attachments:
            if isinstance(attachment, tuple):
            elif isinstance(attachment, MIMEBase):
                # TODO one day when it's needed
                attachments.append('MIMEBase attachment (getting filename not implemented)')
        log_object.attachments = attachments

        # on dev server we usually want to send all e-mails to or something
        reset_recipients = False
        if (dev_recipient := getattr(settings, "EMAIL_DEV_RECIPIENTS", None)) and email_message.recipients():
            reset_recipients = True  # we need to reset it back, so this condition works next time too
            original_recipients = email_message.recipients
            email_message.recipients = lambda *args, **kwargs: [dev_recipient] if isinstance(dev_recipient, str) else dev_recipient

            super_response = super()._send(email_message)
        except smtplib.SMTPException as e:
            log_object.status = False
            if not fail_silently:
                super_response = None
        except Exception as e:
            log_object.status = False
            if reset_recipients:
                email_message.recipients = original_recipients

        self.fail_silently = fail_silently

        if not super_response:
            log_object.status = False

        return super_response

And finally edit your

EMAIL_BACKEND = "your_app.mail.ImprovedSMTPEmailBackend"
    # EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"  # when you don't want your e-mails to be sent at all
    EMAIL_LOG_MODEL = "your_app.EmailLog"
    EMAIL_DEV_RECIPIENTS = ["", ""]  # set your e-mail addresses

See? Easy peasy 🙂