96

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?

Don
  • 15,148
  • 9
  • 55
  • 91
RadiantHex
  • 22,589
  • 43
  • 141
  • 236
  • Moderators: please lock this question. Lots of spam is being added in the answers, proposing solutions that are ridiculously complex just to promote external services. – nemesisdesign Feb 18 '18 at 15:59

10 Answers10

192

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.',
            'from@example.com', ['to@example.com'],
            fail_silently=False)
        self.assertEqual(len(mail.outbox), 1)
        self.assertEqual(mail.outbox[0].subject, 'Subject here')
mjschultz
  • 1,776
  • 1
  • 17
  • 20
Davor Lucic
  • 26,335
  • 7
  • 61
  • 74
  • 3
    +1 Good answer. But I it's not useful for complex cases, when `send_mail` can't be used. – santiagobasulto Feb 08 '13 at 19:21
  • 3
    More precisely the doc is here: https://docs.djangoproject.com/en/1.8/topics/email/#in-memory-backend – nimiq Nov 30 '15 at 13:21
  • 3
    How would you do this if your testing a function which calls send_mail and you therefore can't access `mail`? – Matt Jun 18 '17 at 02:03
  • 3
    @MatthewDrill you can still access `mail.outbox` when `send_mail` is called in another function. – pymarco Nov 16 '17 at 05:31
  • 3
    @pymarco If you import mail from core, `mail.outbox[0].body` will show you the email sent even if the `send_mail` is performed elsewhere. – Rob Nov 16 '18 at 03:05
  • This works perfect even with mass sending using send_messages, here: https://docs.djangoproject.com/en/2.1/topics/email/#sending-multiple-emails – Manel Clos Mar 06 '19 at 09:40
45

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!

Bernhard Vallant
  • 43,536
  • 16
  • 109
  • 135
  • 1
    More info about email backends: https://docs.djangoproject.com/en/dev/topics/email/#email-backends. Sometimes even simple console backend is enough.. – Jeewes May 14 '14 at 19:26
  • 1
    But is there a way to access the generated email during (automated) testing ? – Overdrivr Jan 03 '19 at 09:54
19

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.', 'from@example.com', ['to@example.com'])
    assert len(mail.outbox) == 1

def test_send_again(self):
    mail.send_mail('subject', 'body.', 'from@example.com', ['to@example.com'])
    assert len(mail.outbox) == 1
kiril
  • 3,662
  • 1
  • 24
  • 32
8

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.)

Community
  • 1
  • 1
Greg Dubicki
  • 3,630
  • 2
  • 40
  • 58
5

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.

pyjavo
  • 1,446
  • 2
  • 23
  • 35
Steve Jalim
  • 11,291
  • 1
  • 33
  • 51
  • Further to that, the Message objects created by django-mailer mean you can prod them (and inspect their contents) in unit tests too (I know that there's outbound mailbox support in the test suite for a dummy mailbox, but using django-mailer doesn't send mail unless the management command sends it, which means you can't use that mailbox object) – Steve Jalim Sep 16 '10 at 16:38
  • Update, ages on from my original answer: https://github.com/SmileyChris/django-mailer-2 does support attachments, too – Steve Jalim May 04 '13 at 21:22
4

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.

Josh K
  • 26,152
  • 19
  • 79
  • 130
2

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

pyfunc
  • 60,253
  • 14
  • 138
  • 132
1

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"),
shacker
  • 12,421
  • 6
  • 74
  • 80
0

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).

frogcoder
  • 709
  • 1
  • 6
  • 14
0

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

AgilePro
  • 4,774
  • 2
  • 28
  • 51