Testing email sending in Django

PythonDjangoEmailSmtpDjango Testing

Python Problem Overview


I need to test that my Django application sends e-mails with correct content. I don't want to rely on external systems (like an ad-hoc gmail account), since I'm not testing the actual e-mail service...

I would like to, maybe, store the emails locally, within a folder as they are sent. Any tip on how to achieve it?

Python Solutions


Solution 1 - Python

Django test framework has some built in helpers to aid you with testing e-mail service.

Example from docs (short version):

from django.core import mail
from django.test import TestCase

class EmailTest(TestCase):
    def test_send_email(self):
        mail.send_mail('Subject here', 'Here is the message.',
            '[email protected]', ['[email protected]'],
            fail_silently=False)
        self.assertEqual(len(mail.outbox), 1)
        self.assertEqual(mail.outbox[0].subject, 'Subject here')

Solution 2 - Python

You can use a file backend for sending emails which is a very handy solution for development and testing; emails are not sent but stored in a folder you can specify!

Solution 3 - Python

If you are into unit-testing the best solution is to use the In-memory backend provided by django.

EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'

Take the case of use it as a py.test fixture

@pytest.fixture(autouse=True)
def email_backend_setup(self, settings):
    settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'  

In each test, the mail.outbox is reset with the server, so there are no side effects between tests.

from django.core import mail

def test_send(self):
    mail.send_mail('subject', 'body.', '[email protected]', ['[email protected]'])
    assert len(mail.outbox) == 1

def test_send_again(self):
    mail.send_mail('subject', 'body.', '[email protected]', ['[email protected]'])
    assert len(mail.outbox) == 1

Solution 4 - Python

Use MailHog

> Inspired by MailCatcher, easier to install. > > Built with Go - MailHog runs without installation on multiple platforms.


Also, it has a component called Jim, the MailHog Chaos Monkey, which enables you to test sending emails with various problems happening:

> What can Jim do? > > * Reject connections > * Rate limit connections > * Reject authentication > * Reject senders > * Reject recipients

Read more about it here.


(Unlike original mailcatcher, which failed on me when sending emails with emoji, encoded in UTF-8 and it WASN'T really fixed in the current release, MailHog just works.)

Solution 5 - Python

For any project that doesn't require sending attachments, I use django-mailer, which has the benefit of all outbound emails ending up in a queue until I trigger their sending, and even after they've been sent, they are then logged - all of which is visible in the Admin, making it easy to quickly check what you emailing code is trying to fire off into the intertubes.

Solution 6 - Python

Django also has an in-memory email backend. More details in the docs under In-memory backend. This is present in Django 1.6 not sure if it's present in anything earlier.

Solution 7 - Python

Patching SMTPLib for testing purposes can help test sending mails without sending them.

Solution 8 - Python

Why not start your own really simple SMTP Server by inherit from smtpd.SMTPServer and threading.Thread:

class TestingSMTPServer(smtpd.SMTPServer, threading.Thread):
    def __init__(self, port=25):
        smtpd.SMTPServer.__init__(
            self,
            ('localhost', port),
            ('localhost', port),
            decode_data=False
        )
        threading.Thread.__init__(self)

    def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
        self.received_peer = peer
        self.received_mailfrom = mailfrom
        self.received_rcpttos = rcpttos
        self.received_data = data

    def run(self):
        asyncore.loop()

process_message is called whenever your SMTP Server receive a mail request, you can do whatever you want there.

In the testing code, do something like this:

smtp_server = TestingSMTPServer()
smtp_server.start()
do_thing_that_would_send_a_mail()
smtp_server.close()
self.assertIn(b'hello', smtp_server.received_data)

Just remember to close() the asyncore.dispatcher by calling smtp_server.close() to end the asyncore loop(stop the server from listening).

Solution 9 - Python

Tying a few of the pieces here together, here's a straightforward setup based on filebased.EmailBackend. This renders a list view linking to the individual log files, which have conveniently timestamped filenames. Clicking a link in the list displays that message in the browser (raw):

Settings

EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend"
EMAIL_FILE_PATH = f"{MEDIA_ROOT}/email_out"

View

import os

from django.conf import settings
from django.shortcuts import render

def mailcheck(request):

    path = f"{settings.MEDIA_ROOT}/email_out"
    mail_list = os.listdir(path)

    return render(request, "mailcheck.html", context={"mail_list": mail_list})

Template

{% if mail_list %}
  <ul>
  {% for msg in mail_list %}
    <li>
      <a href="{{ MEDIA_URL }}email_out/{{msg}}">{{ msg }}</a>
    </li>
  {% endfor %}
  </ul>
{% else %}
  No messages found.
{% endif %}

urls

path("mailcheck/", view=mailcheck, name="mailcheck"),

Solution 10 - Python

If you have a TomCat server available, or other servlet engine, then a nice approach is "Post Hoc" which is a small server that looks to the application exactly like a SMTP server, but it includes a user interface that allows you to view and inspect the email messages that were sent. It is open source and freely available.

Find it at: Post Hoc GitHub Site

See the blog post: PostHoc: Testing Apps that Send Email

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRadiantHexView Question on Stackoverflow
Solution 1 - PythonDavor LucicView Answer on Stackoverflow
Solution 2 - PythonBernhard VallantView Answer on Stackoverflow
Solution 3 - PythonkirilView Answer on Stackoverflow
Solution 4 - PythonGreg DubickiView Answer on Stackoverflow
Solution 5 - PythonSteve JalimView Answer on Stackoverflow
Solution 6 - PythonJosh KView Answer on Stackoverflow
Solution 7 - PythonpyfuncView Answer on Stackoverflow
Solution 8 - PythonfrogcoderView Answer on Stackoverflow
Solution 9 - PythonshackerView Answer on Stackoverflow
Solution 10 - PythonAgileProView Answer on Stackoverflow