Python’s email.mime subpackage provides classes for constructing MIME email messages from scratch. from email.mime.text import MIMEText; from email.mime.multipart import MIMEMultipart. Core classes: MIMEText(content, subtype="plain", charset="utf-8") — text/plain or text/html leaf part; MIMEMultipart("mixed") — container for mixed content (body + attachments); MIMEMultipart("alternative") — plain+HTML alternatives (MUA picks best); MIMEMultipart("related") — HTML body with inline images; MIMEBase(maintype, subtype) — raw binary part (call email.encoders.encode_base64(part) after setting payload); MIMEImage(data, subtype="png") — image/png, image/jpeg, etc.; MIMEAudio(data, subtype="mpeg") — audio/* leaf; MIMEApplication(data, subtype="octet-stream") — application/* leaf. Assembly: msg.attach(part) adds a child part to a multipart container. Serialise: msg.as_string() or msg.as_bytes(); set msg["From"], msg["To"], msg["Subject"] headers directly as dict keys. Send with smtplib.SMTP or write to file for testing. Claude Code generates plain-text mailers, HTML mailers, multipart alternative builders, inline-image emails, file-attachment composers, and complete SMTP dispatch pipelines.
CLAUDE.md for email.mime
## email.mime Stack
- Stdlib: from email.mime.text import MIMEText
- from email.mime.multipart import MIMEMultipart
- from email.mime.application import MIMEApplication
- from email.mime.image import MIMEImage
- from email.mime.base import MIMEBase
- from email import encoders
- Plain: msg = MIMEText("Hello", "plain", "utf-8")
- HTML: msg = MIMEText("<b>Hi</b>", "html", "utf-8")
- Multi: msg = MIMEMultipart("mixed") # or "alternative" / "related"
- msg.attach(part)
- Binary: part = MIMEApplication(data, Name="file.pdf")
- part["Content-Disposition"] = 'attachment; filename="file.pdf"'
- Send: msg["From"] = ...; msg["To"] = ...; msg["Subject"] = ...
- msg.as_string() / msg.as_bytes()
email.mime MIME Construction Pipeline
# app/emailmimeutil.py — plain, html, alternative, attachments, inline images, send
from __future__ import annotations
import os
import smtplib
import mimetypes
from dataclasses import dataclass, field
from email import encoders
from email.mime.application import MIMEApplication
from email.mime.audio import MIMEAudio
from email.mime.base import MIMEBase
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formataddr, formatdate, make_msgid
from typing import Any
# ─────────────────────────────────────────────────────────────────────────────
# 1. Plain-text message builder
# ─────────────────────────────────────────────────────────────────────────────
def build_plain(
subject: str,
body: str,
from_addr: str,
to_addrs: "list[str]",
*,
reply_to: str | None = None,
charset: str = "utf-8",
) -> MIMEText:
"""
Build a simple plain-text email message.
Example:
msg = build_plain(
"Hello from Python",
"This is the body.",
"[email protected]",
["[email protected]"],
)
print(msg.as_string())
"""
msg = MIMEText(body, "plain", charset)
msg["Subject"] = subject
msg["From"] = from_addr
msg["To"] = ", ".join(to_addrs)
msg["Date"] = formatdate(localtime=True)
msg["Message-ID"] = make_msgid()
if reply_to:
msg["Reply-To"] = reply_to
return msg
# ─────────────────────────────────────────────────────────────────────────────
# 2. HTML + plain-text alternative message
# ─────────────────────────────────────────────────────────────────────────────
def build_alternative(
subject: str,
plain_body: str,
html_body: str,
from_addr: str,
to_addrs: "list[str]",
*,
charset: str = "utf-8",
) -> MIMEMultipart:
"""
Build a multipart/alternative message with both plain and HTML parts.
MUAs that support HTML will show html_body; others fall back to plain_body.
Example:
msg = build_alternative(
"Newsletter",
"Read in browser.",
"<h1>Hello!</h1>",
"[email protected]",
["[email protected]"],
)
"""
wrapper = MIMEMultipart("alternative")
wrapper["Subject"] = subject
wrapper["From"] = from_addr
wrapper["To"] = ", ".join(to_addrs)
wrapper["Date"] = formatdate(localtime=True)
wrapper["Message-ID"] = make_msgid()
wrapper.attach(MIMEText(plain_body, "plain", charset))
wrapper.attach(MIMEText(html_body, "html", charset))
return wrapper
# ─────────────────────────────────────────────────────────────────────────────
# 3. File attachment helper
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class AttachmentSpec:
path: str # file system path
filename: str = "" # override filename in email; defaults to basename
inline: bool = False # True → Content-Disposition: inline
def _make_attachment(spec: AttachmentSpec) -> MIMEBase:
"""
Create a MIME part for a file attachment.
Guesses content type from extension; falls back to application/octet-stream.
"""
filename = spec.filename or os.path.basename(spec.path)
ctype, _ = mimetypes.guess_type(spec.path)
if ctype is None:
maintype, subtype = "application", "octet-stream"
else:
maintype, subtype = ctype.split("/", 1)
with open(spec.path, "rb") as f:
data = f.read()
if maintype == "text":
part: MIMEBase = MIMEText(
data.decode("utf-8", errors="replace"), subtype, "utf-8"
)
elif maintype == "image":
part = MIMEImage(data, subtype)
elif maintype == "audio":
part = MIMEAudio(data, subtype)
else:
part = MIMEApplication(data, Name=filename)
disposition = "inline" if spec.inline else "attachment"
part["Content-Disposition"] = f'{disposition}; filename="{filename}"'
return part
def build_with_attachments(
subject: str,
plain_body: str,
html_body: str | None,
from_addr: str,
to_addrs: "list[str]",
attachments: "list[AttachmentSpec | str]",
*,
charset: str = "utf-8",
) -> MIMEMultipart:
"""
Build a multipart/mixed message with body and file attachments.
If html_body is provided, the body section is multipart/alternative.
attachments may be AttachmentSpec objects or plain file-path strings.
Example:
msg = build_with_attachments(
"Report",
"See attached PDF.",
None,
"[email protected]",
["[email protected]"],
["/tmp/report.pdf", "/tmp/logo.png"],
)
"""
outer = MIMEMultipart("mixed")
outer["Subject"] = subject
outer["From"] = from_addr
outer["To"] = ", ".join(to_addrs)
outer["Date"] = formatdate(localtime=True)
outer["Message-ID"] = make_msgid()
if html_body:
body_part: MIMEBase = MIMEMultipart("alternative")
body_part.attach(MIMEText(plain_body, "plain", charset)) # type: ignore[arg-type]
body_part.attach(MIMEText(html_body, "html", charset)) # type: ignore[arg-type]
else:
body_part = MIMEText(plain_body, "plain", charset)
outer.attach(body_part)
for att in attachments:
spec = att if isinstance(att, AttachmentSpec) else AttachmentSpec(att)
outer.attach(_make_attachment(spec))
return outer
# ─────────────────────────────────────────────────────────────────────────────
# 4. Inline-image HTML email (multipart/related)
# ─────────────────────────────────────────────────────────────────────────────
def build_inline_image(
subject: str,
html_template: str,
image_path: str,
cid: str,
from_addr: str,
to_addrs: "list[str]",
) -> MIMEMultipart:
"""
Build an HTML email that embeds an inline image via Content-ID (<img src='cid:...'/>).
html_template should reference the image via: src="cid:{cid}"
Example:
html = '<html><body><img src="cid:logo" /><p>Hi!</p></body></html>'
msg = build_inline_image(
"Inline image",
html,
"/tmp/logo.png",
"logo",
"[email protected]",
["[email protected]"],
)
"""
outer = MIMEMultipart("related")
outer["Subject"] = subject
outer["From"] = from_addr
outer["To"] = ", ".join(to_addrs)
outer["Date"] = formatdate(localtime=True)
outer["Message-ID"] = make_msgid()
outer.attach(MIMEText(html_template, "html", "utf-8"))
with open(image_path, "rb") as f:
img_data = f.read()
_, ext = os.path.splitext(image_path)
subtype = ext.lstrip(".").lower() or "png"
img_part = MIMEImage(img_data, subtype)
img_part["Content-ID"] = f"<{cid}>"
img_part["Content-Disposition"] = "inline"
outer.attach(img_part)
return outer
# ─────────────────────────────────────────────────────────────────────────────
# 5. SMTP send helper + dry-run serialiser
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class SmtpConfig:
host: str = "localhost"
port: int = 587
username: str = ""
password: str = ""
use_tls: bool = True
def send_message(
msg: MIMEBase,
config: SmtpConfig | None = None,
*,
dry_run: bool = False,
) -> str:
"""
Send a MIME message via SMTP, or return its string form on dry_run.
Uses STARTTLS when config.use_tls is True.
Example:
msg = build_plain("Hi", "Hello!", "[email protected]", ["[email protected]"])
raw = send_message(msg, dry_run=True)
print(raw[:200])
"""
if dry_run:
return msg.as_string()
cfg = config or SmtpConfig()
from_addr = msg["From"]
to_addrs = [a.strip() for a in msg["To"].split(",")]
with smtplib.SMTP(cfg.host, cfg.port) as smtp:
smtp.ehlo()
if cfg.use_tls:
smtp.starttls()
smtp.ehlo()
if cfg.username:
smtp.login(cfg.username, cfg.password)
smtp.sendmail(from_addr, to_addrs, msg.as_bytes())
return f"sent to {to_addrs}"
def message_summary(msg: MIMEBase) -> dict[str, Any]:
"""
Return a summary dict of a built MIME message for inspection/logging.
Example:
msg = build_plain("Test", "body", "[email protected]", ["[email protected]"])
print(message_summary(msg))
"""
return {
"subject": msg.get("Subject", ""),
"from": msg.get("From", ""),
"to": msg.get("To", ""),
"date": msg.get("Date", ""),
"message_id": msg.get("Message-ID", ""),
"content_type": msg.get_content_type(),
"parts": [p.get_content_type() for p in msg.walk()],
"size_bytes": len(msg.as_bytes()),
}
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import json
print("=== email.mime demo ===")
# ── plain-text message ─────────────────────────────────────────────────
print("\n--- build_plain ---")
plain_msg = build_plain(
"Hello from email.mime",
"This is a plain-text body.\nLine two.",
"[email protected]",
["[email protected]", "[email protected]"],
)
print(message_summary(plain_msg))
# ── alternative message ────────────────────────────────────────────────
print("\n--- build_alternative ---")
alt_msg = build_alternative(
"Newsletter #42",
"Read the newsletter at https://example.com/news",
"<html><body><h1>Newsletter #42</h1><p>Read online.</p></body></html>",
"[email protected]",
["[email protected]"],
)
print(message_summary(alt_msg))
# ── manual MIMEApplication part ───────────────────────────────────────
print("\n--- manual MIMEApplication ---")
pdf_data = b"%PDF-1.4 fake pdf content"
pdf_part = MIMEApplication(pdf_data, Name="report.pdf")
pdf_part["Content-Disposition"] = 'attachment; filename="report.pdf"'
print(f" content-type: {pdf_part.get_content_type()}")
print(f" disposition : {pdf_part['Content-Disposition']}")
# ── MIMEBase with explicit encode_base64 ───────────────────────────────
print("\n--- MIMEBase + encode_base64 ---")
raw_part = MIMEBase("application", "octet-stream")
raw_part.set_payload(b"\x00\x01\x02\x03binary data")
encoders.encode_base64(raw_part)
raw_part["Content-Disposition"] = 'attachment; filename="data.bin"'
print(f" encoding: {raw_part['Content-Transfer-Encoding']}")
print(f" payload len: {len(raw_part.get_payload())}")
# ── dry-run send ───────────────────────────────────────────────────────
print("\n--- send_message dry_run ---")
raw = send_message(plain_msg, dry_run=True)
lines = raw.splitlines()
for line in lines[:10]:
print(f" {line}")
print(f" ... ({len(lines)} lines total, {len(raw)} bytes)")
print("\n=== done ===")
For the smtplib stdlib companion — smtplib.SMTP and SMTP_SSL handle the transport layer while email.mime handles message construction; always pair them: build the MIME message with email.mime.*, then send it with smtplib.SMTP.sendmail() or the higher-level send_message() method — use smtp.send_message(msg) (Python 3.2+) to let smtplib extract From/To headers automatically. For the aiosmtplib (PyPI) alternative — await aiosmtplib.send(msg, hostname=..., port=..., start_tls=True) provides a drop-in asyncio SMTP client that accepts the same email.mime message objects — use aiosmtplib in asyncio applications (FastAPI, aiohttp) to send email without blocking the event loop; use stdlib smtplib for scripts, CLI tools, and synchronous Django/Flask apps. The Claude Skills 360 bundle includes email.mime skill sets covering build_plain() plain-text mailer, build_alternative() HTML+plain composer, AttachmentSpec/build_with_attachments() file attachment builder, build_inline_image() multipart/related inline-image email, SmtpConfig/send_message() SMTP dispatcher, and message_summary() inspection helper. Start with the free tier to try MIME construction patterns and email.mime pipeline code generation.