pyguides

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