Sending emails with embedded images in Django
Django offers useful classes to easily send email. It is also easy to add attachments to emails. I did have to puzzle a bit to get embedded images working. This article describes the way I do it now. I will first describe the most important elements and then I will show a more complete example.
The elements
Since I send a plain text and HTML version of the email, I use the
EmailMultiAlternatives
class:
msg = EmailMultiAlternatives(subject, text_content,
sender, [to_mail])
The images are included as attachments. We do not use the
attach_file
method because we want to set the Content-ID
header. This way we can refer to the image by that ID in the template.
for f in ['logo.png', 'logo-footer.png']:
fp = open(os.path.join(os.path.dirname(__file__), f), 'rb')
msg_img = MIMEImage(fp.read())
fp.close()
msg_img.add_header('Content-ID', '<{}>'.format(f))
msg.attach(msg_img)
As far as I have seen, the images can only actually be included if the
content type of the mail is correctly set. By default, the content
type is set to “multipart/alternative
”. But this resulted in the
images just being displayed as attachments. What I needed was to set
the content type to “multipart/related
”:
msg.mixed_subtype = 'related'
(This is the thing that took the most time to figure out and triggered me to write this post so I would not have to figure it out again in the future. It also caused me to read up on multipart subtypes, see e.g. the Wikipedia article on MIME or RFC 2046 and RFC 2387.)
Complete example
Combining all these elements results in the following code:
# Do these imports at the top of the module.
import os
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from email.MIMEImage import MIMEImage
# You probably want all the following code in a function or method.
# You also need to set subject, sender and to_mail yourself.
html_content = render_to_string('foo.html', context)
text_content = render_to_string('foo.txt', context)
msg = EmailMultiAlternatives(subject, text_content,
sender, [to_mail])
msg.attach_alternative(html_content, "text/html")
msg.mixed_subtype = 'related'
for f in ['img1.png', 'img2.png']:
fp = open(os.path.join(os.path.dirname(__file__), f), 'rb')
msg_img = MIMEImage(fp.read())
fp.close()
msg_img.add_header('Content-ID', '<{}>'.format(f))
msg.attach(msg_img)
msg.send()
Now you can use “<img src="cid:img1.png">
” in your template. The
result should be that the email client shows the image embedded in the
mail at the place of the img
element and not as an attachment.
Result
Sending an email results in something like this:
Content-Type: multipart/related; boundary="===============0527806758=="
MIME-Version: 1.0
Subject: ...
From: ...
To: ...
Date: Tue, 14 Jan 2014 10:07:57 -0000
Message-ID: <20140114100757.32546.81939@...>
--===============0527806758==
Content-Type: multipart/alternative; boundary="===============1211323952=="
MIME-Version: 1.0
--===============1211323952==
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
...
--===============1211323952==
Content-Type: text/html; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
...
--===============1211323952==--
--===============0527806758==
Content-Type: image/png
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-ID: <logo.png>
...
--===============0527806758==--
As you can see, the email consists of two parts: the body—which itself consists of two parts (the plain text version and HTML alternative)—and the related image.
Improvements
Given the name of the class (EmailMultiAlternatives
), the
alternative
multipart subtype is logical. A better solution would
thus be to create an EmailMultiRelated
class which has the proper
subtype from the get-go. And perhaps even has nice methods to attach
files with a Content-ID
header.
As a matter of fact, there are a couple of snippets over on djangosnippets.org (e.g. snippet 2215) that do exactly that. I haven’t tried any of these since they are overkill for my project (for now), but they may prove to be more useful than my code.