I'll start by butchering Zawinski's law of software development and say that most applications evolve until they can either send or read mail. Web apps are no exception. Be it a simple registration or account activation email, or something more complex like a "marketing" (Ugh!) campaign, most apps have to be very good at sending email.
The Problem with Sending Email
The problem with email is that it is not a tiny problem, yet it tends to get overlooked and underestimated by many developers. That is why we get many systems where email sending is spread all over the code base, making it a nightmare to maintain. We get models or views sending email straight from code that deals with business logic, turning everything into an untestable big ball of spaghetti.
I've been guilty of most of those. When I started learning Django, most of my mail sends consisted of importing
django.core.mail.send_mail and slapping my message body in a multiline Python string constant. Yuck! It quickly dawned on me that there has to be a better way, and, after a quick run to the docs, I was able to come up with a simple system that used Django templates to render email content from template files sitting in my app. My system quickly evolved to something that looked a lot like Rails' mailers and their convention of having two templates for plain text and HTML content.
Not Invented Here?
My system works fine, but I am not too fond of it either. I was stuck coding while I should have asked around or checked sites like djangopackages.com. I realized that I have built an inferior reimplementation of Philipp Wassibauer's templated-emails or Stephen McDonald's django-email-extras. My advice -- don't make the same mistake and just pick one of those two packages. Instead of falling prey to the Not Invented Here syndrome, go use that time to build another cool feature for your project.
Making a Proper Home for your Email Code
In my current project I stumbled upon a very cool pattern that helps me handle email. It boils down to house all email-related pieces together in a separate Django app. It has been very convenient to have both the code that sets up templates and contexts and the actual templates all together in a place that is easy to find. If your project sends many different email types, you can even have more than one app and logically group related emails together. Most of the time people go with one app and I usually pick the mundane name of "emailer." Here is how it looks like:
emailer ├── __init__.py ├── models.py ├── templates │ └── emailer │ ├── feedback_sent.txt │ ├── ...
Email sending logic goes in the
models module, and templates get hosted below
Django Signals to Tie Everything Together
You might have been wondering how the rest of the system gets to talk to the emailer app. I would feel way bad if I had to import the
emailer.models module in my other apps' model code and use that. The solution to this problem is to make your emailer aware of the other modules. It has to know about them anyway since it would be displaying information related to their model objects. This is where Django signals come in -- we define signals for important events that happen to our models and we handle those signals in the emailer app. Here is how a signal for a feedback email looks like:
1 2 3 4
from django.dispatch import Signal ... # Sent when a user submits feedback from the site. feedback_sent = Signal(providing_args=["feedback"])
And here is how we raise it, passing the feedback model object as an argument:
1 2 3
from feedback.signals import feedback_sent ... feedback_sent.send(sender=feedback, feedback=feedback)
Then we let our emailer app handle it:
1 2 3 4 5 6
from django.dispatch import receiver from feedback.signals import feedback_sent ... @receiver(feedback_sent) def on_feedback_sent(sender, feedback, **kwargs): ...
on_feedback_sent function is the place that sets up templates and actually sends the email.
This simple pattern gives us an additional advantage -- we get a huge "DON'T SEND EMAIL" switch in our app. We can easily enable or disable all email sends by simply not registering our
"emailer" app in our
INSTALLED_APPS collection. That may prove very useful in a local development or test setup. Yes, I am aware that Django doesn't send emails and prints them to the console when running a debug version of your app in the local development web server, but there are still legitimate cases where you would want to disable email processing completely.
Another huge benefit you get with a separate app is that your solution gets more future-proof. Say you want to track each email that gets sent to every user, so that you can be sure that important notifications get sent through. Or maybe you want to forward a copy of some notification to the site administrators. You now have the right place to build additional smarts into email sending logic. A lot easier than going through all your apps and updating email send code, I'd say.
Background Sends - Coming up
This post is getting quite long already, so I'll have to leave my version of background sends for tomorrow. Stay tuned!
Update: Part 2 is now up.