Email Inbox in Django Dev environment
7 min read

Email Inbox in Django Dev environment

Email Inbox in Django Dev environment

My current project sends emails. Not a very controversial statement that, as most web projects probably need to send the occasional email.

To make testing during the dev phase easy, I've configured the project to use the django.core.mail.backends.filebased.EmailBackend backend, which just writes an email to a file in a directory you specify. This means I can just open up the file, read and visually verify the email, and click on any links (eg. email confirmation for registration link).

Setup of backend

settings.py

if DEBUG:
    EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend"
    EMAIL_FILE_PATH = BASE_DIR / "sent_emails"
else:
    EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
    SENDGRID_API_KEY = os.getenv("SENDGRID_API_KEY")

    EMAIL_HOST = "smtp.sendgrid.net"
    EMAIL_HOST_USER = "apikey"
    EMAIL_HOST_PASSWORD = SENDGRID_API_KEY
    EMAIL_PORT = 587
    EMAIL_USE_TLS = True

As you can see, when running in DEBUG mode, all emails gert written to a file under the sent_emails folder. A sample filename looks like this: 20210318-091634-140373308594112.log, with the YYYYMMDD-HHMMSS timestamp at the start, making it reasonably easy to find the latest emails.

If not in DEBUG mode, I am using sendgrid, though you may have a different solution. Do not use the filebased.EmailBackend solution in a produciton environment, it is intended for development use only.

Do not use the filebased.EmailBackend solution in a produciton environment, it is intended for development use only.

Accessing the mailbox during development

Of course, you can just flip over to a file browser or your terminal and open or cat the .log files containing the email details. That works just fine. I found it a little irritating after a while though, so I build a basic inbox to display them to me within my project without having to change to another utility.

My navbar is contained in its own template, and then included in the base.html, so within my _navbar.html template file, I included the following:

{% if debug %}
<li class="nav-item px-2">
    <a class="nav-link" href="{% url 'test_inbox' %}">Inbox</a>
</li>
{% endif %}

(My navbar items are contained in a <ul>, hence the list item element. Yours may look different or you may want it somewhere other than the navbar. It's the link for the <a> element that is important here though).

Of course we need test_inbox to link to something, so:
urls.py

if settings.DEBUG:
    import debug_toolbar

    from .views import Inbox, InboxMail

    urlpatterns += [
        path("__inbox/<str:filename>", InboxMail.as_view(), name="test_inbox_mail"),
        path("__inbox/", Inbox.as_view(), name="test_inbox"),
        path("__debug__/", include(debug_toolbar.urls)),
    ]

I already had the if settings.DEBUG in here, as I use Django Debug Toolbar. If you don't use this extremely helpful tool, you should look into installing it in your development environment. It is extremely helpful.

Install django-debug-toolbar to make debugging of your Django project much, much simpler!

To check the DEBUG boolean you will first need to do from django.conf import settings, which gives you access to everything in your settings.py file.

You'll also note I have a link here for a second view that will show a specific mail. We'll get to that in a bit. I've put the path at __inbox just to indicate it's a development only path.

Setup the Inbox view

So we've connected test_inbox to the Inbox view. Let's define this view.

class Inbox(ListView):
    """
    Testing util only - Displays list of mails in debug inbox
    """

    template_name = "config/inbox.html"
    context_object_name = "mail_list"
    paginate_by = 10

    def get_queryset(self):
        import os

        from django.conf import settings

        path = settings.EMAIL_FILE_PATH
        mail_list = os.listdir(path)
        mail_list.sort(reverse=True)
        return mail_list

Lets run through this to be clear on what's happening:

  1. Inbox inherits from ListView, for the simple reason that it will display a list of emails in the EMAIL_FILE_PATH folder.
  2. I have a template for the view, of course. I'll show you that in a moment.
  3. context_object_name tells the template what the data list is called, so we can loop over it and display the data.
  4. I'm paginating my inbox. You don't have to, so feel free to leave this bit out.

Within get_queryset:

  1. I import os and settings here because they're not required for any of the production views in the same views.py file, so importing them in a production run is unnecesary and will affect performance. This way, they only get imported in a dev environment when I actually access the inbox.
  2. os.listdir gives me a list of all files in the directory defined in settings.EMAIL_FILE_PATH. Assuming you have created a directory just to hold these emails (which you should), all that will be in here is emails your project has sent while DEBUG is True.
  3. The sort(reverse=True) is to ensure mails are sorted in the order, and show the most recent at the top of the list. This just makes the inbox easier to use.
  4. Finally, the view returns the list, which the template can access as mail_list (which we defined in context_object_name)

config/inbox.html

{% extends 'config/base.html' %}

{% block main_content %}
<br/>
<h1>Inbox</h1>
<br/>
{% include 'config/_pagination.html' %}
<table class="table table-striped table-hover">
    <thead>
        <tr>
            <th scope="col">Email file</th>
        </tr>
    </thead>
    <tbody>
    {% for email in mail_list %}
    <tr onclick="window.location='{% url "test_inbox_mail" filename=email %}'">
        <td>{{ email }}</td>
    </tr>
    {% empty %}
    <tr>
        <td class="text-center">No messages found.</td>
    </tr>
    {% endfor %}
    </tbody>
</table>
{% include 'config/_pagination.html' %}
{% endblock main_content %}

That's it!

  • The template extends my base.py, which also includes my navbar.
  • The include of pagination can be left out if you're not using this. I will cover a simple way to paginate a list in a seperate post, but if you're not sure how to do this, just leave it out, and take the paginate_by line out of the view. All this means is you will get a list of all files in the directory.
  • I'm putting my data into a table just to make it pretty (and using bootstrap format on my project). Style yours however you like.
  • Note the {% empty %} option on the {% for %} loop. This kicks in only if the data you give to the for loop is empty. In this case, this means there are no emails to read.
  • {% for email in mail_list %} loops over everything in mail_list, which is the name we gave the data in the view (context_object_name)
  • There is a tiny bit of javascript here just to link to the email itself. I have this so I can make the entire table row a link. If you had, eg. a list of text links you could just use an <a> element for this instead.
  • Finally, {{ email }} will give you the file name (since mail_list contained a list of all filenames in the email directory).

I may add a few bits of functionality to this at some point, but for the moment I will leave them as an exercise for the reader:

  • A Delete All button that removes everything in the inbox. Useful when you have a lot of mails and just want to start again.
  • Individual delete buttons for each mail (so each mail has a little trashcan icon by it).
  • A nicely formatted date/timestamp for each mail in the list.
  • To information (we know the from, but we may be testing mails to different users, so seeing this in the list could be useful).

Setup the Email view

You've seen from the urls.py that I have a view called InboxMail to view an individual email, and that each record in the Inbox template links to this view, passing the filename. So let's take a look at that:

class InboxMail(TemplateView):
    """
    Testing util only - Displays specific mail from the inbox
    """

    template_name = "config/inbox_email.html"

    def get_context_data(self, **kwargs):
        import os
        from datetime import datetime

        from django.conf import settings

        context = super().get_context_data(**kwargs)
        filename = context["filename"]
        date_time = " ".join(os.path.splitext(filename)[0].split("-")[:2])
        parsed_date_time = datetime.strptime(date_time, "%Y%m%d %H%M%S")

        filepath = settings.EMAIL_FILE_PATH / filename
        with open(filepath, "r") as f:
            email_content = f.read()

        context["timestamp"] = parsed_date_time
        context["email_content"] = email_content
        return context

Here, I'm inheriting from TemplateView, so all the template is really expecting is context information, which is why I am overriding get_context_data to do a few things:

  • As with the Inbox view, any imports not also relevant for a production deployment are done within the view, for performance reasons.
  • The filename is parsed to extract the date and time, and format it as a more "human readable" string, which we pass to the template.
  • The file is then opened and the contents read into another variable, which we also send to the template.

The template is then even simpler than the one for the inbox.

config/inbox_email.html

{% extends 'config/base.html' %}

{% block main_content %}
<div class="container">
    <div class="card">
        <div class="card-header font-weight-bold">{{ filename }} <span class="text-muted">, {{ timestamp }}</span></div>
        <div class="card-body">
            <a href="{% url 'test_inbox' %}" class="btn btn-primary">Return to Inbox</a>
            <br />
            <br />
            {{ email_content|linebreaks }}
        </div>
    </div>
</div>
{% endblock main_content %}
  • The email is displayed as a card, just to make it look neat.
  • The card header has the human readable timestamp we passed in
  • The card body displays the email text. The linebreaks templatetag preserves linebreaks as they are in the email itself, and makes it a lot more readable (try it without if you want to see the difference!)
  • Just before the email text, there is a button to take you back to the inbox view.

And that's it!

I find this quite useful, as I can now complete a whole manual use case test within the same application while I'm developing, and don't need to switch around between browser, file explorer, possibly notepad or some other text editor.

All the code needed is here. You may need to change some display classes, or leave off pagination, or add some extra stuff you want, but the basic implementation will work, so just copy paste and you should have it up and running in no time!

Enjoying these posts? Subscribe for more