Sending Mails in Python, the Modern Way
It's easy to send mails in Python, but even easier to miss how.

Sending mails in Python seems simple. It has a built-in library for creating and sending mails (email, smtplib). The Internet is awash with tutorials, StackOverflow overflows (ahem) with desperate users looking for answers.

But.

StackOverflow is useful, but the platform is also full of simply bad or outdated examples. And this is especially the case when sending mails.

The main reason: The email library both contains email.message.Message and email.message.EmailMessage. All StackOverflow samples I found use Message to deal with emails. Turns out, Message is legacy (policy: compat32) reaching back to Python 3.2 - and it lacks many convenient methods to handle mails. Since we already reached the astronomic level of Python 3.11 it’s time to ditch such legacy and provide examples using EmailMessage. Strangely, I found few to none examples on the Internet at the time I needed it, with the impression that users simply copypaste code without thinking (yes, we’re all guilty).

Sending Mails the Modern Way

Here’s the solution I implemented, abbreviated and lacking error handling for clarity, in the hope that others find it useful and do not fall back to legacy libraries. I heavily relied on the examples provided in the email library itself - many thanks to the authors, because otherwise I would have been lost.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import smtplib
from email.message import EmailMessage

# create mail
mail_from = "pythonmail@example.com"
mail_to = "user@example.com"
mail_subject = "You've got Python mail!"
mail_body = "Hello from Python!\nCheers, a lovely snake"

msg = EmailMessage() # <= NOT email.message.Message!

msg["Subject"] = mail_subject
msg["From"] = mail_from
msg["To"] = mail_to
msg.set_content(mail_body)

# send mail
server = smtplib.SMTP(host="myhost.example.com")  # port=25 is default
server.send_message(msg)
server.quit()

That’s it - the simplest working example. The mail is sent in plaintext, since I did not explicitly define a MIME type.

Attachments and a Converter

Two further code snippets come in handy. Here’s how to add an original mail as attachment. Note that the MIME type is, strangely, application/octet-stream. Googling reveals that there’s no MIME type defined for emails sent as attachments. Again omitting error handling for clarity.

1
2
3
4
5
6
7
a_mail = pathlib.Path("path_to_original_mail.eml")
with a_mail.open("rb") as fi:  # .msg also possible
    attachment_data = fi.read()
    msg.add_attachment(attachment_data,
                       maintype="application",
                       subtype="octet-stream",
                       filename="Subject of the original mail.eml")  # or .msg

You can choose the filename to be whatever you want, usually it’s the subject of the original mail.

Often, libraries or applications will return a Message object, in my case pytest-localserver. Since you want to be a modern Pythonista, you need to convert it to an EmailMessage. Here’s the code:

1
2
3
def convert_message(mail: email.message.Message) -> email.message.EmailMessage:
    msg_as_bytes = mail.as_bytes()
    return email.message_from_bytes(msg_as_bytes, policy=email.policy.default)

Image courtesy of Nightcafe and Dall-E.


Last modified on 2022-11-09

Comments Disabled.