Improved SMTP backend for Django

Django

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

django-improved-smtp-backend

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 admin.py

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


admin.site.register(EmailLog, EmailLogAdmin)

Then create a mail.py 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
    """

    @cached_property
    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(
            sender=email_message.from_email,
            to=email_message.to,
            cc=email_message.cc,
            bcc=email_message.bcc,
            reply_to=email_message.reply_to,
            subject=email_message.subject,
            body=email_message.body,
        )
        attachments = []
        for attachment in email_message.attachments:
            if isinstance(attachment, tuple):
                attachments.append(attachment[0])
            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 system@company.com 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

        try:
            super_response = super()._send(email_message)
        except smtplib.SMTPException as e:
            log_object.status = False
            log_object.exception=str(e)
            log_object.traceback=traceback.format_exc()
            if not fail_silently:
                log_object.save()
                raise
            else:
                super_response = None
        except Exception as e:
            log_object.status = False
            log_object.exception=str(e)
            log_object.traceback=traceback.format_exc()
            log_object.save()
            raise
        finally:
            if reset_recipients:
                email_message.recipients = original_recipients

        self.fail_silently = fail_silently

        if not super_response:
            log_object.status = False

        log_object.save()

        return super_response

And finally edit your settings.py

EMAIL_BACKEND = "your_app.mail.ImprovedSMTPEmailBackend"
if DEBUG:
    # 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 = ["your.email@example.com", "your.project.manager@example.com"]  # set your e-mail addresses

See? Easy peasy 🙂