October 22, 2009
A Django application I’ve been working on sends a confirmation email near the end of a workflow. The content of that email closely matches the final web page of the workflow. Constructing an email in Django is pretty straightforward, and using templates makes the process a lot easier and more flexible.
My naive initial approach was to construct my HTML in a string, using Python’s string formatting, and then use the string as the body of the email. That was a reasonable starting point, but it sucked because it’s messy, it duplicates functionality and frankly it’s a waste of the power Django has to offer.
The next step was to harness templates to generate my content for the email. I actually need an email with both HTML and plain text content, and I already mentioned that the content is reasonably close to that used elsewhere, so there’s plenty of potential for code reuse.
The django.template.loader module contains the handy render_to_string function which, surprise, allows us to render our template to a string. It parallels the more common render_to_response function found in your view code, so I can supply a template name and a context in the same way I would for my view code: (contrived example follows)
from django.template.loader import render_to_response html_content = render_to_string( "my_template.html", my_context )
I’ll go into that HTML content shortly, but suffice to say the template can reuse snippets of template code from the rest of my application. I can therefore create reusable content that is embeddable in both my web page and email body. Likewise, I can generate a text version of the body, though reusing HTML snippets is obviously not so easy or even appropriate:
text_content = render_to_string( "my_template.txt", my_context )
I can then take these strings and use them to construct my email:
from django.core.email import EmailMultiAlternatives my_mail = EmailMultiAlternatives( subject="Foo Confirmed", body=text_content, from_email="email@example.com", to=[ "firstname.lastname@example.org" ] ) my_mail.attach_alternative( html_content, "text/html" )
Now, back to the HTML content. There are a few things to be aware of with HTML in email:
- Keep the <head></head> section empty as the content is likely to be ignored and could even be stripped out
- Use a <style></style> section after your <body> tag to inline your CSS code
- Inline references to external resources like images are likely to be blocked
Think of the HTML in your email body as needing to be a self-contained environment. I’ve included some online resources at the end of this post which will give you more in-depth information.
That’s fine for CSS, but how do you add inline images to your emails? The answer is to create MIMEImage instances of any images and attach to your email with appropriate Content-ID and Content-Disposition headers. There’s a very simple example at http://www.djangosnippets.org/snippets/1507/ that worked nicely for me.
Hope the above is a useful starting point!
October 18, 2009
In the past, my experience testing the sending of emails from web frameworks had not been a particularly smooth or easy one. There was often a bit of hackery involved, and attempting to do so from a test harness of some sort was often tricky. Fortunately, testing emails from Django (1.0+) test cases is pretty straightforward and very pleasant to use.
When you run the Django testrunner, the runner will temporarily override the SMTPConnection class. Any emails sent from code called by the testrunner will not be sent out, but captured and stored for analysis within your tests.
Let’s say I have some code that will send out a notification email, and I want to test that it works. I could have a test case along the lines of:
class NotificationEmailTest( TestCase ): def test_send_notification_email( self ): send_notification_email( "Foo!" ) ## test goes here
But how do I check the email was sent? Well, the overridden SMTPConnection stores emails in the django.core.mail.outbox list. I can therefore check to see if I have one email message in the outbox. This outbox is only available within tests, it does not exist in normal Django operation.
First, I need an import so I can access the outbox:
from django.core import mail
Then I have to revise the above test to include my assertion:
class NotificationEmailTest( TestCase ): def test_send_notification_email( self ): """ Test send_notification_email sends out one email """ send_notification_email( "Foo!" ) self.assertEqual( len(mail.outbox), 1 )
The outbox is guaranteed to be empty at the start of each test, so you don’t need to do anything in terms of setup or teardown.
Testing presence of an email is fine, but we normally want to test the content of a message (otherwise it could be junk!). Each item in the outbox is an instance of EmailMessage, with all the expected attributes. If we wanted to add a second assertion to the above test, say to check the subject, we could do so:
class NotificationEmailTest( TestCase ): def test_send_notification_email( self ): """ Test send_notification_email sends out one email """ send_notification_email( "Foo!" ) self.assertEqual( len(mail.outbox), 1 ) self.assertEqual( mail.outbox.subject, "Foo!" )
In this case, the single parameter for send_notification_email is actually the subject of the email, so my test checks to see if this matches.
When testing the contents of to and bcc, there is apparently no cc, be aware these are tuples of addresses when writing your tests.
class NotificationEmailTest( TestCase ): def test_send_notification_email( self ): """ Test send_notification_email sends out one email """ send_notification_email( "Foo!" ) self.assertEqual( len(mail.outbox), 1 ) self.assertEqual( mail.outbox.to, [ "email@example.com" ] )
This is fine if we have one recipient, but if our recipient list is going to have multiple addresses we need to allow for this in our tests. If we knew the exact ordering we could compare to a list but, unless the ordering matters, we end up making the test fragile. Instead, we can test for the presence of email addresses individually:
self.assertTrue( "firstname.lastname@example.org" in mail.outbox.to ) self.assertTrue( "email@example.com" in mail.outbox.to )
Django makes testing of emails easy, so there’s no excuse for not incorporating email tests into your Django test suites.
Hope that gives you a starting point. Have fun!