Python’s email package composes and parses RFC 5322 / MIME messages. from email.message import EmailMessage. EmailMessage: msg = EmailMessage() — modern API (Python 3.6+); set headers: msg["Subject"] = "...", msg["From"] = "...", msg["To"] = "...". set_content: msg.set_content("plain text") — sets text/plain part. add_alternative: msg.add_alternative("<html>...</html>", subtype="html") — creates multipart/alternative. add_attachment: msg.add_attachment(data, maintype="application", subtype="pdf", filename="report.pdf") — binary attachment. Serialization: msg.as_string() / msg.as_bytes() with policy=email.policy.SMTP. email.headerregistry: from email.headerregistry import Address; addr = Address("Name", addr_spec="user@host"). email.utils: formataddr(("Name", "user@host")) → "Name <user@host>"; parseaddr(s) → (name, addr); formatdate(localtime=True) → RFC 2822 date; make_msgid(domain="host") → unique Message-ID. Parsing: from email.parser import BytesParser; msg = BytesParser(policy=email.policy.default).parsebytes(raw). Policy: email.policy.EmailPolicy(utf8=True) for modern unicode SMTP connections. Legacy: MIMEText / MIMEMultipart from email.mime.* still work for custom MIME tree building. Claude Code generates newsletter composers, attachment wrappers, bounce parsers, and templated email pipelines.
CLAUDE.md for email
## email Stack
- Stdlib: from email.message import EmailMessage; import email.policy
- Plain: msg = EmailMessage(); msg["Subject"]="..."; msg.set_content("body")
- HTML: msg.add_alternative("<html>…</html>", subtype="html")
- Attach: msg.add_attachment(data, maintype="application", subtype="pdf", filename="f.pdf")
- Send: smtplib.SMTP_SSL(host).sendmail(frm, to, msg.as_bytes(policy=email.policy.SMTP))
- Parse: BytesParser(policy=email.policy.default).parsebytes(raw_bytes)
email Message Composition Pipeline
# app/emailutil.py — compose, attach, parse, template, address helpers
from __future__ import annotations
import email.policy
import email.utils
import mimetypes
import os
import re
import textwrap
from dataclasses import dataclass, field
from email.headerregistry import Address
from email.message import EmailMessage
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.parser import BytesParser, Parser
from pathlib import Path
from typing import Any
# ─────────────────────────────────────────────────────────────────────────────
# 1. Address helpers
# ─────────────────────────────────────────────────────────────────────────────
def make_address(display_name: str, addr: str) -> str:
"""
Format a displayname + addr-spec into a proper RFC 5322 address string.
Example:
make_address("Alice Smith", "[email protected]")
# → "Alice Smith <[email protected]>"
"""
return email.utils.formataddr((display_name, addr))
def parse_address(header_value: str) -> tuple[str, str]:
"""
Parse an RFC 5322 address into (display_name, addr_spec).
Example:
name, addr = parse_address("Alice Smith <[email protected]>")
"""
return email.utils.parseaddr(header_value)
def parse_address_list(header_value: str) -> list[tuple[str, str]]:
"""
Parse a comma-separated address list into [(name, addr), ...].
Example:
recipients = parse_address_list("Alice <[email protected]>, Bob <[email protected]>")
"""
return email.utils.getaddresses([header_value])
def is_valid_email(addr: str) -> bool:
"""
Lightweight RFC 5322-ish email validation (no DNS lookup).
Example:
is_valid_email("[email protected]") # True
is_valid_email("not-an-email") # False
"""
pattern = re.compile(r"^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$")
return bool(pattern.match(addr.strip()))
# ─────────────────────────────────────────────────────────────────────────────
# 2. Message builders
# ─────────────────────────────────────────────────────────────────────────────
def plain_email(
subject: str,
body: str,
to: str | list[str],
from_addr: str,
*,
cc: str | list[str] | None = None,
bcc: str | list[str] | None = None,
reply_to: str | None = None,
) -> EmailMessage:
"""
Build a plain-text EmailMessage.
Example:
msg = plain_email("Hello", "Body text", "[email protected]", "[email protected]")
print(msg.as_string())
"""
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = from_addr
msg["To"] = ", ".join(to) if isinstance(to, list) else to
if cc:
msg["Cc"] = ", ".join(cc) if isinstance(cc, list) else cc
if bcc:
msg["Bcc"] = ", ".join(bcc) if isinstance(bcc, list) else bcc
if reply_to:
msg["Reply-To"] = reply_to
msg["Message-ID"] = email.utils.make_msgid()
msg["Date"] = email.utils.formatdate(localtime=True)
msg.set_content(body)
return msg
def html_email(
subject: str,
text_body: str,
html_body: str,
to: str | list[str],
from_addr: str,
**kwargs,
) -> EmailMessage:
"""
Build a multipart/alternative email with plain-text and HTML parts.
Example:
msg = html_email(
"Newsletter",
"View online: http://example.com",
"<h1>Hello!</h1><p>View online: <a href='http://example.com'>here</a></p>",
"[email protected]",
from_addr="[email protected]",
)
"""
msg = plain_email(subject, text_body, to, from_addr, **kwargs)
msg.add_alternative(html_body, subtype="html")
return msg
def attach_file(msg: EmailMessage, path: str | Path, filename: str | None = None) -> None:
"""
Attach a file to an EmailMessage, auto-detecting MIME type.
Example:
msg = plain_email("Report", "See attached.", "[email protected]", "[email protected]")
attach_file(msg, "/tmp/report.pdf")
"""
fpath = Path(path)
fname = filename or fpath.name
maintype, subtype = (mimetypes.guess_type(fname)[0] or "application/octet-stream").split("/", 1)
data = fpath.read_bytes()
msg.add_attachment(data, maintype=maintype, subtype=subtype, filename=fname)
def attach_bytes(
msg: EmailMessage,
data: bytes,
filename: str,
content_type: str = "application/octet-stream",
) -> None:
"""
Attach raw bytes to an EmailMessage with a given filename.
Example:
attach_bytes(msg, csv_bytes, "export.csv", content_type="text/csv")
"""
maintype, subtype = content_type.split("/", 1)
msg.add_attachment(data, maintype=maintype, subtype=subtype, filename=filename)
# ─────────────────────────────────────────────────────────────────────────────
# 3. Template renderer
# ─────────────────────────────────────────────────────────────────────────────
def render_template(template: str, variables: dict[str, str]) -> str:
"""
Simple {{var}} template renderer for email bodies.
Example:
body = render_template("Hello {{name}}, your code is {{code}}.",
{"name": "Alice", "code": "XK29"})
"""
for key, value in variables.items():
template = template.replace("{{" + key + "}}", value)
return template
def build_email_from_template(
subject_template: str,
text_template: str,
html_template: str | None = None,
*,
variables: dict[str, str],
to: str | list[str],
from_addr: str,
**kwargs,
) -> EmailMessage:
"""
Render subject + body templates and build an EmailMessage.
Example:
msg = build_email_from_template(
"Welcome, {{name}}!",
"Hi {{name}}, your account {{email}} is ready.",
variables={"name": "Bob", "email": "[email protected]"},
to="[email protected]",
from_addr="[email protected]",
)
"""
subject = render_template(subject_template, variables)
text_body = render_template(text_template, variables)
if html_template:
html_body = render_template(html_template, variables)
return html_email(subject, text_body, html_body, to, from_addr, **kwargs)
return plain_email(subject, text_body, to, from_addr, **kwargs)
# ─────────────────────────────────────────────────────────────────────────────
# 4. Parsing helpers
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class ParsedEmail:
subject: str
from_addr: str
to_addrs: list[str]
date: str
text_body: str | None
html_body: str | None
attachments: list[dict[str, Any]] = field(default_factory=list)
# {filename, content_type, data}
@classmethod
def from_bytes(cls, raw: bytes) -> "ParsedEmail":
"""
Parse raw RFC 5322 bytes into a ParsedEmail.
Example:
msg = ParsedEmail.from_bytes(Path("message.eml").read_bytes())
print(msg.subject, msg.text_body[:80])
"""
msg = BytesParser(policy=email.policy.default).parsebytes(raw)
return cls._from_message(msg)
@classmethod
def from_string(cls, raw: str) -> "ParsedEmail":
msg = Parser(policy=email.policy.default).parsestr(raw)
return cls._from_message(msg)
@classmethod
def _from_message(cls, msg) -> "ParsedEmail":
text_body: str | None = None
html_body: str | None = None
attachments: list[dict[str, Any]] = []
if msg.is_multipart():
for part in msg.walk():
ct = part.get_content_type()
disp = str(part.get("Content-Disposition") or "")
if "attachment" in disp:
attachments.append({
"filename": part.get_filename() or "attachment",
"content_type": ct,
"data": part.get_payload(decode=True),
})
elif ct == "text/plain" and text_body is None:
text_body = part.get_payload(decode=True).decode(
part.get_content_charset() or "utf-8", errors="replace")
elif ct == "text/html" and html_body is None:
html_body = part.get_payload(decode=True).decode(
part.get_content_charset() or "utf-8", errors="replace")
else:
payload = msg.get_payload(decode=True)
if payload:
decoded = payload.decode(msg.get_content_charset() or "utf-8", errors="replace")
if msg.get_content_type() == "text/html":
html_body = decoded
else:
text_body = decoded
to_raw = msg.get("To") or ""
return cls(
subject=str(msg.get("Subject") or ""),
from_addr=str(msg.get("From") or ""),
to_addrs=[addr.strip() for addr in to_raw.split(",") if addr.strip()],
date=str(msg.get("Date") or ""),
text_body=text_body,
html_body=html_body,
attachments=attachments,
)
def summary(self) -> str:
body_preview = (self.text_body or self.html_body or "")[:100].replace("\n", " ")
return (
f"From: {self.from_addr}\n"
f"To: {', '.join(self.to_addrs)}\n"
f"Date: {self.date}\n"
f"Subj: {self.subject}\n"
f"Body: {body_preview}\n"
f"Att: {len(self.attachments)} attachment(s)"
)
# ─────────────────────────────────────────────────────────────────────────────
# 5. SMTP send helper (requires smtplib)
# ─────────────────────────────────────────────────────────────────────────────
def send_via_smtp(
msg: EmailMessage,
host: str,
port: int = 587,
username: str = "",
password: str = "",
use_tls: bool = True,
) -> None:
"""
Send an EmailMessage via SMTP with optional STARTTLS and auth.
Set host/port/credentials in calling code or environment variables.
Example:
msg = plain_email("Test", "Hello!", "[email protected]", "[email protected]")
send_via_smtp(msg, "smtp.example.com", 587, "user", "pass")
"""
import smtplib
import ssl
ctx = ssl.create_default_context() if use_tls else None
with smtplib.SMTP(host, port) as smtp:
if use_tls and ctx:
smtp.starttls(context=ctx)
if username:
smtp.login(username, password)
smtp.send_message(msg)
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== email demo ===")
# ── Plain email ───────────────────────────────────────────────────────────
print("\n--- plain_email ---")
msg = plain_email(
subject="Monthly Report",
body="Hi Bob,\n\nPlease find the report below.\n\nBest,\nAlice",
to="[email protected]",
from_addr="[email protected]",
)
raw = msg.as_bytes(policy=email.policy.SMTP)
print(f" size: {len(raw)} bytes")
print(f" subject: {msg['Subject']}")
print(f" content-type: {msg.get_content_type()}")
# ── HTML email ────────────────────────────────────────────────────────────
print("\n--- html_email ---")
hmsg = html_email(
"Newsletter Issue #42",
"Read this month's highlights at https://example.com/newsletter",
"<h1>Newsletter #42</h1><p>Read the <a href='https://example.com'>highlights</a>.</p>",
to="[email protected]",
from_addr="[email protected]",
)
print(f" content-type: {hmsg.get_content_type()}")
parts = list(hmsg.iter_parts())
print(f" parts: {[p.get_content_type() for p in parts]}")
# ── Attachment ────────────────────────────────────────────────────────────
print("\n--- attach_bytes ---")
amsg = plain_email("Q3 Data", "CSV attached.", "[email protected]", "[email protected]")
csv_data = b"id,name,value\n1,alpha,100\n2,beta,200\n"
attach_bytes(amsg, csv_data, "data.csv", content_type="text/csv")
parts2 = list(amsg.walk())
print(f" walk parts: {[p.get_content_type() for p in parts2]}")
# ── Template ──────────────────────────────────────────────────────────────
print("\n--- build_email_from_template ---")
tmpl_msg = build_email_from_template(
"Welcome, {{name}}!",
"Hi {{name}},\n\nYour account {{email}} has been created.\n",
variables={"name": "Carol", "email": "[email protected]"},
to="[email protected]",
from_addr="[email protected]",
)
print(f" subject: {tmpl_msg['Subject']}")
print(f" body preview: {tmpl_msg.get_body().get_content()[:60]!r}")
# ── Round-trip parse ──────────────────────────────────────────────────────
print("\n--- ParsedEmail.from_bytes ---")
raw_bytes = amsg.as_bytes(policy=email.policy.SMTP)
parsed = ParsedEmail.from_bytes(raw_bytes)
print(parsed.summary())
# ── Address helpers ───────────────────────────────────────────────────────
print("\n--- address helpers ---")
formatted = make_address("Dave Jones", "[email protected]")
print(f" formataddr: {formatted}")
name, addr = parse_address(formatted)
print(f" parseaddr: name={name!r} addr={addr!r}")
print(f" is_valid('[email protected]'): {is_valid_email('[email protected]')}")
print(f" is_valid('not-email'): {is_valid_email('not-email')}")
print("\n=== done ===")
For the smtplib alternative — smtplib handles the SMTP transport layer — EHLO, STARTTLS, AUTH, DATA envelope — but does not compose messages; it pairs with email to form a complete send pipeline: email builds the message bytes, smtplib.SMTP delivers them — these two modules are complementary and are almost always used together; the send_via_smtp() helper above unifies them into one call. For the mailbox alternative — mailbox (stdlib) reads and writes on-disk mail storage formats including Maildir, mbox, MH, Babyl, and MMDF; it provides random access to stored messages as mailbox.Message objects which are subclasses of email.message.Message — use mailbox when you need to read or maintain a local mail store (archiving, migration, batch processing of saved .eml files); use email alone when you only need to compose or parse individual messages without persisting them to a mailbox format. The Claude Skills 360 bundle includes email skill sets covering make_address()/parse_address()/parse_address_list()/is_valid_email() address utilities, plain_email()/html_email() message builders, attach_file()/attach_bytes() attachment helpers, render_template()/build_email_from_template() template pipeline, ParsedEmail dataclass with from_bytes()/from_string()/summary() for round-trip parsing, and send_via_smtp() SMTP integration. Start with the free tier to try email composition patterns and email pipeline code generation.