The definitive guide to creating emails with attachments in Python 3

There is a lot of conflicting advice out on the internet about how best to construct an email with attachments using standard Python libraries. None of it explains why to do things in certain ways. Here is a definitive, tested bit of code that will create an email with attachments that will work correctly. I’m posting it here so I don’t need to spend any time working it all out again!
#!/usr/bin/env python3

# First we import the necessary libraries
import email, os, smtplib
import email.encoders
import email.header
import email.mime.base
import email.mime.multipart
import email.mime.text
import magic

# Next we set some key variables
server = 'localhost'
subject = 'Subject'
sender = 'sender@example.com'
recipients = ['recipient@example.net', 'cc@example.net', 'bcc@example.net']
body = 'Message body\n'
attachments = ['/path/to/attachment1', '/path/to/attachment2']

# Now we create the message and its headers
msg = email.mime.multipart.MIMEMultipart()
msg.set_charset( 'utf-8' )
msg['Subject'] = email.header.Header( subject )
msg['From'] = email.header.Header( sender )
msg['To'] = email.header.Header( recipients[0] )
msg['Cc'] = email.header.Header( recipients[1] )
msg['Date'] = email.header.Header( email.utils.formatdate() )
msg['Message-ID'] = email.header.Header( email.utils.make_msgid() )

# Here we attach the message body
msg.attach( email.mime.text.MIMEText( body, 'plain' ) )

# Now we attach the other files, including detecting type and setting appropriate headers
m = magic.open( magic.MAGIC_MIME_TYPE )
m.load()
for attachment in attachments:
	filetype = m.file( attachment )
	maintype, subtype = filetype.split( '/' )
	part = email.mime.base.MIMEBase( maintype, subtype )
	part.set_payload( open( attachment, 'rb' ).read() )
	email.encoders.encode_base64( part )
	part.add_header( 'Content-Disposition', 'attachment; filename="{}"'.format( os.path.basename( attachment ) ) )
	msg.attach( part )

# Finally, we send the message
s = smtplib.SMTP( server )
try:
	s.sendmail( sender, recipients, msg.as_string() )
except smtplib.SMTPSenderRefused as e:
	raise
s.quit()
Things to note:
  1. The Subject, From, To, Date and Message-ID headers are all required. Without them your message is likely to be marked as spam at the receiving end.
  2. Many online examples use the mimetypes library to detect the file type of attachments. That library only works if the files have an extension, e.g. file.txt. The magic library works even for files without extensions.
  3. There are two different, incompatible, versions of the magic library. This StackOverflow answer might help if you’re not sure which version you should use. Both versions will work better than mimetypes but you need to use the correct syntax. The code above uses the version that’s available in the Ubuntu repositories via sudo apt-get install python3-magic.
  4. Python includes several email.mime.* libraries that might seem tempting to use for attachments. Ignore them. The documentation implies that attachments created using the appropriate email.mime.whatever library are automatically sensibly encoded and have the correct headers set. This isn’t true. It is far, far safer to use email.mime.base.MIMEBase for all attachment types, and manually encode them using base64.

Add new comment

(If you're a human, don't change the following field)
Your first name.
(If you're a human, don't change the following field)
Your first name.

Filtered HTML

  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.