email module
Overview
Python’s email package provides a complete toolkit for parsing, constructing, and manipulating email messages. It handles the messy details of MIME encoding, multipart messages, and non-ASCII text so you don’t have to. The package is split into several submodules: email.parser for reading existing messages, email.generator for serializing them, email.message for the object model, and email.mime for constructing new messages from scratch.
Architecture
The email package is organized around a few key concepts:
- Message object — an in-memory representation of an email, accessible as a dictionary of headers plus a payload
- Policy — controls parsing and serialization behavior, including newline handling and content transfer encoding
- Generator — converts a message object back to a byte string for sending or storage
- Content managers — handle the payload of each part, encoding it correctly
Parsing Existing Messages
From a string or bytes
from email import parser
raw = b"""From: alice@example.com\r
To: bob@example.com\r
Subject: Hello\r
\r
This is the body.
"""
msg = parser.BytesParser().parsebytes(raw)
# or for strings:
msg = parser.Parser().parsestr(raw_text)
From a file
with open("message.eml", "rb") as f:
msg = parser.BytesParser().parse(f)
Using message_from_bytes / message_from_string
from email import message_from_bytes, message_from_string
msg = message_from_bytes(raw_bytes)
msg = message_from_string(raw_string)
Reading Message Contents
Accessing headers
msg["From"] # => "alice@example.com"
msg["Subject"] # => "Hello"
msg.get("X-Custom") # => None (header absent)
# iterate over all headers
for header, value in msg.items():
print(f"{header}: {value}")
Walking the message tree
# Walk all parts (including multipart sub-parts)
for part in msg.walk():
content_type = part.get_content_type()
if content_type == "text/plain":
body = part.get_payload(decode=True)
print(body.decode("utf-8"))
Extracting the body
For a simple (non-multipart) message:
if msg.is_multipart():
for part in msg.walk():
# handle each part
pass
else:
body = msg.get_payload(decode=True)
charset = msg.get_content_charset() or "utf-8"
print(body.decode(charset))
Constructing New Messages
Simple text message
from email.message import EmailMessage
msg = EmailMessage()
msg["From"] = "alice@example.com"
msg["To"] = "bob@example.com"
msg["Subject"] = "Greetings"
msg.set_content("Hello, Bob. This is a plain text message.")
Adding HTML alongside plain text (multipart/alternative)
msg = EmailMessage()
msg["From"] = "alice@example.com"
msg["To"] = "bob@example.com"
msg["Subject"] = "Hello"
msg.add_alternative(
"Hello, Bob. This is the plain text version.",
subtype="plain"
)
msg.add_alternative(
"<html><body><p>Hello, <strong>Bob</strong>.</p></body></html>",
subtype="html"
)
Adding attachments
with open("report.pdf", "rb") as f:
msg.add_attachment(
f.read(),
maintype="application",
subtype="pdf",
filename="report.pdf"
)
Or with automatic MIME type detection:
from email import encoders
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
msg = EmailMessage()
msg["From"] = "alice@example.com"
msg["To"] = "bob@example.com"
msg["Subject"] = "Report"
msg.attach(EmailMessage()) # or use MIMEBase directly
Multipart mixed (body + attachments)
msg = EmailMessage()
msg["From"] = "alice@example.com"
msg["To"] = "bob@example.com"
msg["Subject"] = "Files attached"
msg.set_content("Please find the attached files.")
with open("data.csv", "rb") as f:
msg.add_attachment(
f.read(),
maintype="text",
subtype="csv",
filename="data.csv"
)
Email Addresses
Representing addresses
from email.address import Address
addr = Address(
name="Alice Johnson",
username="alice",
domain="example.com"
)
str(addr) # => "Alice Johnson <alice@example.com>"
Using addresses in headers
from email.message import EmailMessage
from email.address import Address
msg = EmailMessage()
msg["From"] = Address(name="Alice Johnson", username="alice", domain="example.com")
msg["To"] = Address(name="Bob Smith", username="bob", domain="example.com")
Policy and Encoding
Custom policy for stricter formatting
from email.policy import HTTP, SMTPUTF8
msg = EmailMessage(policy=SMTPUTF8)
# Allows non-ASCII headers without manual encoding
Manual encoding override
msg = EmailMessage()
msg.set_content(
"Caf\xe9 \u2014 the best",
charset="utf-8"
)
Encoding headers
from email.header import Header
msg["Subject"] = Header("Hello \u4e16\u754c", "utf-8")
# Encodes to: =?utf-8?b?5aW955CG?=
Sending Email
email handles message construction only. To send, pair it with smtplib:
import smtplib
from email.message import EmailMessage
msg = EmailMessage()
msg["From"] = "alice@example.com"
msg["To"] = "bob@example.com"
msg["Subject"] = "Test"
msg.set_content("Body here.")
with smtplib.SMTP("smtp.example.com", 587) as server:
server.starttls()
server.login("alice@example.com", "password")
server.send_message(msg)
Common Gotchas
get_payload() returns a string by default. Parsed messages return the payload as a string (possibly quoted-printable or base64 encoded). Pass decode=True to get raw bytes, then decode manually:
raw = msg.get_payload(decode=True) # bytes or None
if raw:
body = raw.decode(msg.get_content_charset() or "utf-8")
Non-ASCII headers must be encoded. Raw non-ASCII characters in headers raise an error under strict policies. Use email.header.Header or the SMTPUTF8 policy to handle them automatically.
is_multipart() tells you if a message has sub-parts. Don’t assume a message with Content-Type: text/plain is simple — check is_multipart() first.
Attachments need correct MIME types. Setting maintype and subtype correctly matters for how mail clients interpret the file. Common types: text/plain, text/html, application/pdf, image/png.
See Also
- /tutorials/python-automation/email-automation-python/ — full guide to sending emails with attachments and HTML
- /guides/python-string-templates/ — formatting strings for email bodies
- /tutorials/python-and-data/working-with-apis/ — connecting to external services including SMTP