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.

Comments

You wouldn't believe how hard it is to do something so simple. There are sooo many versions of how to do this, and none of them have worked. This worked perfectly for what I needed except for the magic module. OpenSuSE doesn't have it, but luckily, I am only sending one type of file CSV so it's simple to hard code it into my app.

Edvard Rejthar - Tue, 15/11/2022 - 15:57

Permalink

Hi, thanks for your code. Since 2019, all the e-mail related burden might be releaved with the envelope library at https://github.com/CZ-NIC/envelope. No hassle with the magic or attachments or required headers or smtp. Single object call will do all this for you so that you can concentrate on your business logic, not the e-mail implementation details.

Install it with: pip install envelope

Import it and write a single line:

from envelope import Envelope
Envelope().message('Message body\n').subject('Subject').from_('sender@example.com').to(['recipient@example.net', 'cc@example.net', 'bcc@example.net']).attach(path='/path/to/attachment1').attach(path='/path/to/attachment2').smtp('localhost').send(0)

 

Hey Edvard, thanks for taking the time to submit this. It looks very convenient! I hope that there will be an apt package available in the official Debian/Ubuntu repos at some point.

Add new comment

CAPTCHA