Python’s email.contentmanager module provides the ContentManager machinery that powers EmailMessage.get_content() and set_content() in the modern email API (Python 3.6+). from email import contentmanager. Two built-in content managers: contentmanager.content_manager — the standard one used by email.policy.default; handles text/*, multipart/*, message/rfc822, and binary parts (returns decoded str for text, bytes for others). contentmanager.raw_data_manager — returns raw decoded payload without content-type awareness. Use via EmailMessage: msg.get_content() → decoded str/bytes; msg.set_content(text_or_bytes, subtype, ...) → sets MIME type, charset, and transfer encoding in one call; msg.make_alternative() → convert to multipart/alternative; msg.make_mixed() → multipart/mixed; msg.make_related() → multipart/related; msg.add_attachment(data, maintype, subtype, ...) → attach data with auto-encoding. Custom ContentManager: subclass and call cm.add_get_handler(content_type, fn) / cm.add_set_handler(type_, fn) to register custom reader/writer functions. Claude Code generates high-level email readers, attachment extractors, text body accessors, HTML readers, and policy-aware MIME construction pipelines.
CLAUDE.md for email.contentmanager
## email.contentmanager Stack
- Stdlib: from email import contentmanager, policy
- from email.message import EmailMessage
- Use via EmailMessage (policy.default):
- msg = EmailMessage()
- msg.set_content("Plain text") # text/plain
- msg.set_content("<b>Hi</b>", subtype="html") # text/html
- text = msg.get_content() # → str (decodes charset)
- msg.make_alternative() # → multipart/alternative
- msg.add_attachment(img_bytes, maintype="image", subtype="png",
- filename="photo.png")
- Raw: raw_data_manager.get_content(part) # raw payload
- Custom:
- cm = contentmanager.ContentManager()
- cm.add_get_handler("text/x-mytype", my_reader)
- cm.add_set_handler(MyClass, my_writer)
email.contentmanager High-Level Email Pipeline
# app/emailcontentmanagerutil.py — get, set, attach, extract, custom
from __future__ import annotations
import io
import mimetypes
import os
from dataclasses import dataclass, field
from email import contentmanager, policy as _policy
from email.message import EmailMessage, Message
from email.parser import BytesParser
from typing import Any
# ─────────────────────────────────────────────────────────────────────────────
# 1. EmailMessage builders using set_content / add_attachment
# ─────────────────────────────────────────────────────────────────────────────
def build_plain(subject: str, body: str,
from_addr: str, to_addrs: "list[str]") -> EmailMessage:
"""
Build a plain-text EmailMessage using the modern set_content() API.
Example:
msg = build_plain("Hello", "Body text.", "[email protected]", ["[email protected]"])
print(msg.as_string())
"""
from email.utils import formatdate, make_msgid
msg = EmailMessage(policy=_policy.default)
msg["Subject"] = subject
msg["From"] = from_addr
msg["To"] = ", ".join(to_addrs)
msg["Date"] = formatdate(localtime=True)
msg["Message-ID"] = make_msgid()
msg.set_content(body)
return msg
def build_html_alternative(subject: str,
plain: str, html: str,
from_addr: str,
to_addrs: "list[str]") -> EmailMessage:
"""
Build a multipart/alternative message with plain + HTML bodies.
Uses modern make_alternative() API.
Example:
msg = build_html_alternative(
"Newsletter",
"Read online.",
"<h1>Hello!</h1>",
"[email protected]", ["[email protected]"],
)
"""
from email.utils import formatdate, make_msgid
msg = EmailMessage(policy=_policy.default)
msg["Subject"] = subject
msg["From"] = from_addr
msg["To"] = ", ".join(to_addrs)
msg["Date"] = formatdate(localtime=True)
msg["Message-ID"] = make_msgid()
msg.set_content(plain)
msg.make_alternative()
msg.get_payload()[1].set_content(html, subtype="html") # type: ignore[index]
return msg
def build_with_files(subject: str, body: str,
from_addr: str, to_addrs: "list[str]",
file_paths: "list[str]") -> EmailMessage:
"""
Build a multipart/mixed message with file attachments.
Uses add_attachment() to attach each file with auto-detected MIME type.
Example:
msg = build_with_files(
"Report",
"See attached.",
"[email protected]", ["[email protected]"],
["/tmp/report.pdf", "/tmp/data.csv"],
)
"""
from email.utils import formatdate, make_msgid
msg = EmailMessage(policy=_policy.default)
msg["Subject"] = subject
msg["From"] = from_addr
msg["To"] = ", ".join(to_addrs)
msg["Date"] = formatdate(localtime=True)
msg["Message-ID"] = make_msgid()
msg.set_content(body)
for path in file_paths:
filename = os.path.basename(path)
ctype, _ = mimetypes.guess_type(path)
maintype, subtype = (ctype or "application/octet-stream").split("/", 1)
with open(path, "rb") as f:
data = f.read()
msg.add_attachment(data, maintype=maintype, subtype=subtype,
filename=filename)
return msg
# ─────────────────────────────────────────────────────────────────────────────
# 2. Content extractors
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class BodyParts:
plain: str = ""
html: str = ""
def extract_body(msg: "EmailMessage | Message") -> BodyParts:
"""
Extract plain-text and HTML body parts from an (optionally multipart) message.
Example:
with open("msg.eml", "rb") as f:
msg = BytesParser(policy=_policy.default).parse(f)
body = extract_body(msg)
print(body.plain[:200])
"""
parts = BodyParts()
if not msg.is_multipart():
ct = msg.get_content_type()
try:
content = msg.get_content() # type: ignore[attr-defined]
except Exception:
content = ""
if ct == "text/plain":
parts.plain = content
elif ct == "text/html":
parts.html = content
return parts
for part in msg.walk():
ct = part.get_content_type()
if ct not in ("text/plain", "text/html"):
continue
try:
content = part.get_content() # type: ignore[attr-defined]
except Exception:
content = ""
if ct == "text/plain" and not parts.plain:
parts.plain = content
elif ct == "text/html" and not parts.html:
parts.html = content
return parts
@dataclass
class Attachment:
filename: str
content_type: str
data: bytes
maintype: str
subtype: str
def extract_attachments(msg: "EmailMessage | Message") -> list[Attachment]:
"""
Extract all non-body MIME parts as Attachment objects.
Skips text/plain and text/html parts without a filename.
Example:
attachments = extract_attachments(msg)
for att in attachments:
print(att.filename, len(att.data))
with open(att.filename, "wb") as f:
f.write(att.data)
"""
result: list[Attachment] = []
for part in msg.walk():
ct = part.get_content_type()
maintype, subtype = ct.split("/", 1)
filename = part.get_filename("")
# Skip body parts that are not explicit attachments
disposition = part.get_content_disposition()
if not filename and disposition not in ("attachment", "inline"):
if ct in ("text/plain", "text/html", "multipart/mixed",
"multipart/alternative", "multipart/related"):
continue
try:
data = part.get_payload(decode=True) or b""
except Exception:
data = b""
if not isinstance(data, bytes):
continue
result.append(Attachment(
filename=filename or f"unnamed.{subtype}",
content_type=ct,
data=data,
maintype=maintype,
subtype=subtype,
))
return result
# ─────────────────────────────────────────────────────────────────────────────
# 3. Custom ContentManager extension
# ─────────────────────────────────────────────────────────────────────────────
def make_custom_content_manager() -> contentmanager.ContentManager:
"""
Return a ContentManager that extends the standard one with a
custom text/csv get handler returning a list of row dicts.
Example:
cm = make_custom_content_manager()
# attach as policy: pol = policy.default.clone(content_manager=cm)
"""
import csv
cm = contentmanager.ContentManager()
# Standard get handlers
cm.add_get_handler(
"text/plain",
lambda msg, *a, **kw: contentmanager.content_manager.get_content(msg, *a, **kw),
)
cm.add_get_handler(
"text/html",
lambda msg, *a, **kw: contentmanager.content_manager.get_content(msg, *a, **kw),
)
# Custom CSV handler
def get_csv(msg: Any, *args: Any, **kwargs: Any) -> list[dict]:
raw = contentmanager.raw_data_manager.get_content(msg)
if isinstance(raw, bytes):
raw = raw.decode("utf-8", errors="replace")
reader = csv.DictReader(raw.splitlines())
return list(reader)
cm.add_get_handler("text/csv", get_csv)
return cm
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== email.contentmanager demo ===")
# ── build_plain ────────────────────────────────────────────────────────
print("\n--- build_plain ---")
msg_plain = build_plain("Hello!", "Plain text body.", "[email protected]", ["[email protected]"])
print(f" content-type: {msg_plain.get_content_type()!r}")
print(f" get_content : {msg_plain.get_content()!r}")
# ── build_html_alternative ────────────────────────────────────────────
print("\n--- build_html_alternative ---")
msg_alt = build_html_alternative(
"Newsletter",
"Read online at https://example.com",
"<h1>Welcome!</h1><p>Read <a href='https://example.com'>online</a>.</p>",
"[email protected]", ["[email protected]"],
)
print(f" content-type: {msg_alt.get_content_type()!r}")
body = extract_body(msg_alt)
print(f" plain: {body.plain!r}")
print(f" html : {body.html[:50]!r}")
# ── add_attachment programmatically ───────────────────────────────────
print("\n--- add_attachment ---")
msg_attach = EmailMessage(policy=_policy.default)
msg_attach["Subject"] = "With attachment"
msg_attach["From"] = "[email protected]"
msg_attach["To"] = "[email protected]"
msg_attach.set_content("See attached blob.")
msg_attach.add_attachment(b"\x89PNG\r\n\x1a\n" + b"\x00" * 100,
maintype="image", subtype="png",
filename="screenshot.png")
atts = extract_attachments(msg_attach)
print(f" attachments: {[(a.filename, a.content_type, len(a.data)) for a in atts]}")
# ── raw_data_manager ──────────────────────────────────────────────────
print("\n--- raw_data_manager ---")
raw = contentmanager.raw_data_manager.get_content(msg_plain)
print(f" raw type: {type(raw).__name__} len={len(raw)}")
# ── content_manager constants ─────────────────────────────────────────
print("\n--- contentmanager module attributes ---")
attrs = [a for a in dir(contentmanager) if not a.startswith("_")]
print(f" public attrs: {attrs}")
print("\n=== done ===")
For the email.mime.* companion — email.mime.text.MIMEText, MIMEMultipart, and MIMEApplication are the older, lower-level API for building MIME messages; EmailMessage.set_content() and add_attachment() internally use the same MIME machinery but expose a cleaner, policy-aware interface — use EmailMessage + set_content() for all new code (Python 3.6+); use email.mime.* only when maintaining legacy code or needing explicit transfer-encoding control not exposed by set_content(). For the python-email-validator + aiofiles (PyPI) combination — for async attachment handling, serialise the EmailMessage to bytes with BytesGenerator, then write with aiofiles.open("msg.eml", "wb") — there is no async MIME builder in the stdlib; compose synchronously, then ship bytes to async I/O. The Claude Skills 360 bundle includes email.contentmanager skill sets covering build_plain()/build_html_alternative()/build_with_files() message builders, BodyParts/extract_body() body extractor, Attachment/extract_attachments() attachment extractor, make_custom_content_manager() registry extension, and raw_data_manager raw payload access. Start with the free tier to try high-level email content patterns and email.contentmanager pipeline code generation.