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:
- 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.
- 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. Themagic
library works even for files without extensions. - 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 thanmimetypes
but you need to use the correct syntax. The code above uses the version that’s available in the Ubuntu repositories viasudo apt-get install python3-magic
. - Python includes several
email.mime.*
libraries that might seem tempting to use for attachments. Ignore them. The documentation implies that attachments created using the appropriateemail.mime.whatever
library are automatically sensibly encoded and have the correct headers set. This isn’t true. It is far, far safer to useemail.mime.base.MIMEBase
for all attachment types, and manually encode them using base64.
Comments
an alternative
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)
Re: an alternative
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.
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.