Wellfire Interactive // Expertise for established Django SaaS applications

Nobody talks about sending email in your Django app (This Old Pony #47)

Email is one of the important batteries in Django’s “batteries included” list. And yet nobody talks about email and Django!

Now that’s not entirely true, nor, as true, is it entirely warranted. The Django docs[0] have a section on email, so it’s not like it’s undocumented. And it’s not the most important part of a web application, not usually at least. Plus it seems kind of like a solved problem, right, so why bother?

The thing is, when email matters to your application, it usually has an outsized impact. It’s affects how people register, how they remedy lost passwords, it’s how they get notified of important data events. In short it only affects things related to user satisfaction, customer retention, and lower support burdens. No biggie, right?

We only have one email - at least this week - so let’s first go through some of the types of emails you might send your users, how to manage templates and content, plus issues around testing and the production vs. development divide.
 

The types of email we send

There are a lot of ways to categorize the kinds of emails that we can get from websites we sign up for (want to get is another story). We’ll start with a simple divide: (1) marketing emails and (2) transactional emails.

Marketing emails include things like newsletters and onboarding campaigns. These are emails that are not individually requested by the user or pushed out specifically to the user based on some activity related to them in your app. All things being equal you’re better off handling these emails outside of your Django site entirely. If you need to make use of information about customers the best way to do this is by using events or tags in a marketing automation system (like Drip or ActiveCampaign[1]).

Transactional emails include a pretty wide swath of emails though. Password resets, signup confirmations, new event notification, renewal notices, requests for review… it’s a pretty long list.

These are the kinds of emails that users expect to “just work”. They’re also the kind of thing that users don’t really notice, at least not when everything looks and works smoothly.
 

Managing templates and content types

Let’s take a break from discussion and just take in some strategies to make working with emails easier.

Treat email templates much like you would regular templates. Keep them in your templates folder(s) and, as warranted, in app-specific folders. It’s a Very Good Idea (TM) to also organize them by a consistent folder structure beyond this, e.g. “templates/email” or “app/templates/app/email” - I’m sure you get the idea. This gives you a consistent name space to work with and avoids they “email_blah_blah_blah” file naming pattern. 

If you’re using both text and HTML content templates, name them the same, excepting for the extension of course. E.g. “email/confirmation.txt” and “email/confirmation.html”. It may seem obvious but when this isn’t done it ends up causing confusion.

Just like with your web templates, maintain sitewide base email templates. If you’re using text and HTML, you’ll want base templates for both. You’ll likely want consistent footers, at the very least, and having these base templates makes this possible.

Managing two templates for the same content can get tedious. There are cases where you’ll want the control over text email formatting only afforded by managing the template directly. If this isn’t the case you can build the HTML content first and then build the plain text content directly from this[2]. Tip: you can combine both of these strategies. 

Oh yeah, you’ll want to ensure you extend _built-in _templates from other apps, like Django’s registration app, so that your beautiful site isn’t sending a mixed bag of email styles to users.

And last but not least, don’t go crazy with HTML email templates. If you think dealing with multiple browsers is hard, wait until you dive into the world of email clients…
 

Working with different emails here and there

Django’s email interface is so beautifully simple it makes sense for simple cases to just import “send_mail” wherever you need it and send from there. Once you start dealing with more emails though this lends itself to more and more logic where you don’t want it.

So here’s the secret: treat emails like views.

But also, like tasks. Treat them like views that are tasks.

Emails have a template, maybe more than one. Actually, they should have more than one because the _subject _is often going to be templated in on way or another, too. And emails, being templated content, need a context of data to render. 

This means that when you’re sending, say, a confirmation email, you don’t call “send_mail” in your view with the confirmation template name(s) and subjection string. Instead it means you pass a dictionary of data to your “confirmation_email” function (or “ConfirmationEmail” class, as it were) which can encapsulate the subject, various templates, and any other conditional logic. This becomes very handy beyond just a few basic emails.
 

Development and production

Okay, time for some more knowledge bombs.

When you’re working in development your default settings is the console email backend. Ensure that is your _default _for development. Now if you want to override this you should probably be using the SMTP backend with an email catcher of one kind or another. You can use MailHog[3] locally or something like Mailtrap[4] if you have a deployed test environment or you just want to share the dev inbox results with someone else.

Now, why?

First off, you want to ensure you can see the emails sent from the app. The console backend has the benefit of being built in and dead simple to use. The downside is that your messages are rendered entirely in plaintext in the terminal. Using a local mail catcher adds One More Thing (TM) you need have running but will allow you to see a rendered version of the email.

Things don’t get easier in production, I’m afraid. You have two choices - set up your own SMTP server or use a transactional email sender. Using your own SMTP server is fine if you have a small deployment for, say, the use of a single in-house team. Beyond this you’ll start running into delivery issues, and more importantly, delivery issues that matter. 

If you are running this locally, postfix and exim4 are going to be your go-to choices, at least on Linux. But you probably don’t want to do this. And if you’re using a PaaS it’s not an option. So you’ll need to use a [paid] transactional email service. The downside is that it’s Yet Another Third Party Service (TM) but they’re [hopefully] expert in issues of email deliverability leaving one less thing for you to worry about. These are your SendGrids, Mailguns, Postmarks, and even SES’s.

How do you choose one? That’s beyond the scope of this email, but I can suggest how to connect to your choice: SMTP. Many will offer their own HTTP API, but unless this provides access to some meaningful feature otherwise out of reach, stick with SMTP if you can. If you cannot, take the time to write a slim wrapper so that one little section of your code “knows” anything about this API. You do not want to be locked in here, and you also want to be able to still work in dev mode locally.

That’s a great rule, in general. There are a few exceptions, like payments, but in general you should be able to operate features of your site in development mode without recourse to an internet connection. This is a prime “smell” for development friction - but that’s another story.

RFC 2822’d yours,
Ben

P.S. If you signed up for this email within the last month or two and this is your first issue - sorry! We discovered an issue with an old signup for that meant you got into the system but weren’t getting the emails… we fixed the glitch.

[0] Django docs: https://docs.djangoproject.com/en/2.0/topics/email/
[1] Drip, ActiveCampaign
[2] Convert HTML content to plaintext https://pypi.org/project/html2text/
[3] Local mail catching app: https://github.com/mailhog/MailHog
[4] Mail-catcher-as-a-Service https://mailtrap.io/

Learn from more articles like this how to make the most out of your existing Django site.