Python’s quopri module encodes and decodes the Quoted-Printable content transfer encoding used in MIME email — non-ASCII bytes become =XX escapes; lines are soft-broken at 76 characters. import quopri. encode: quopri.encode(input, output, quotetabs=False, header=False) — reads bytes from file-like input, writes QP bytes to output; quotetabs=True also escapes tabs/spaces (required for text/plain transport); header=True uses modified header encoding (_ for space, no soft line breaks). decode: quopri.decode(input, output, header=False) — reverse. encodestring: quopri.encodestring(s, quotetabs=False, header=False) → bytes — convenience wrapper around encode. decodestring: quopri.decodestring(s, header=False) → bytes. Lines ending in \r\n are normalized; =\r\n is a soft line break (continuation). Safe bytes (printable ASCII except =) pass through unchanged, making QP output human-readable. Claude Code generates MIME body encoders, email header encoders, QP validators, and multi-part message builders.
CLAUDE.md for quopri
## quopri Stack
- Stdlib: import quopri
- Encode: quopri.encodestring(b"Hello caf\xe9", quotetabs=True)
- Decode: quopri.decodestring(b"Hello caf=E9")
- Stream: quopri.encode(io.BytesIO(data), buf, quotetabs=True)
- Header: quopri.encodestring(b"Ren\xe9", header=True) # =?iso-8859-1?q?...?=
- Check: line lengths \u2264 76 chars; = escapes non-ASCII
quopri Quoted-Printable Pipeline
# app/quoproutil.py — encode, decode, stream, MIME builder, validator, batch
from __future__ import annotations
import io
import quopri
from dataclasses import dataclass
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.charset import Charset, QP
from pathlib import Path
from typing import Any
# ─────────────────────────────────────────────────────────────────────────────
# 1. Basic encode / decode
# ─────────────────────────────────────────────────────────────────────────────
def encode_qp(
data: bytes,
quotetabs: bool = True,
header: bool = False,
) -> bytes:
"""
Encode bytes to Quoted-Printable.
quotetabs: encode tabs/spaces to =09/=20 (required for text/plain).
header: use modified header QP (=?charset?q?...?=) — spaces become _.
Example:
encoded = encode_qp("Ren\xe9 Müller".encode("latin-1"))
print(encoded) # b"Ren=E9 M=FCller"
"""
return quopri.encodestring(data, quotetabs=quotetabs, header=header)
def decode_qp(data: bytes, header: bool = False) -> bytes:
"""
Decode Quoted-Printable bytes back to binary.
Example:
decoded = decode_qp(b"caf=E9")
print(decoded) # b"caf\\xe9"
"""
return quopri.decodestring(data, header=header)
def encode_qp_stream(
data: bytes,
quotetabs: bool = True,
) -> bytes:
"""
Encode using the streaming API (encode file-like objects).
Example:
encoded = encode_qp_stream(b"Hello, world! \xe9\n")
"""
inp = io.BytesIO(data)
out = io.BytesIO()
quopri.encode(inp, out, quotetabs=quotetabs)
return out.getvalue()
def decode_qp_stream(data: bytes) -> bytes:
"""Decode using the streaming API."""
inp = io.BytesIO(data)
out = io.BytesIO()
quopri.decode(inp, out)
return out.getvalue()
# ─────────────────────────────────────────────────────────────────────────────
# 2. Text encoding helpers (str → QP bytes)
# ─────────────────────────────────────────────────────────────────────────────
def encode_text(
text: str,
charset: str = "utf-8",
quotetabs: bool = True,
) -> bytes:
"""
Encode a str as QP bytes using the given charset.
Example:
qp = encode_text("Héllo wörld")
print(qp) # b"H=C3=A9llo w=C3=B6rld"
"""
return encode_qp(text.encode(charset), quotetabs=quotetabs)
def decode_text(
data: bytes,
charset: str = "utf-8",
header: bool = False,
) -> str:
"""
Decode QP bytes and decode as str using charset.
Example:
text = decode_text(b"H=C3=A9llo")
print(text) # "Héllo"
"""
return decode_qp(data, header=header).decode(charset)
def encode_header_word(text: str, charset: str = "utf-8") -> str:
"""
Encode a single header word using RFC 2047 QP encoding.
Returns the encoded-word string: =?charset?q?...?=
Example:
word = encode_header_word("Ren\xe9 Müller", "iso-8859-1")
# =?iso-8859-1?q?Ren=E9_M=FCller?=
"""
qp_bytes = encode_qp(text.encode(charset), header=True)
# Replace '=' with '=', spaces become '_' (done by header=True)
return f"=?{charset}?q?{qp_bytes.decode('ascii')}?="
# ─────────────────────────────────────────────────────────────────────────────
# 3. QP inspector / validator
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class QpAnalysis:
line_count: int
max_line_length: int
escape_count: int # number of =XX sequences
soft_breaks: int # number of soft line breaks (=\r\n or =\n)
valid: bool
issues: list[str]
def __str__(self) -> str:
return (f"lines={self.line_count} max_len={self.max_line_length} "
f"escapes={self.escape_count} soft_breaks={self.soft_breaks} "
f"valid={self.valid}")
def analyze_qp(data: bytes) -> QpAnalysis:
"""
Analyze a QP-encoded byte string for RFC 2045 compliance.
Example:
analysis = analyze_qp(encode_text("Héllo wörld"))
print(analysis)
"""
issues: list[str] = []
lines = data.split(b"\n")
max_len = 0
escapes = 0
soft_breaks = 0
for i, line in enumerate(lines, start=1):
# Strip \r if present
stripped = line.rstrip(b"\r")
line_len = len(stripped)
if line_len > max_len:
max_len = line_len
if line_len > 76:
issues.append(f"line {i} too long ({line_len} > 76)")
if stripped.endswith(b"="):
soft_breaks += 1
# Count =XX escapes
j = 0
while j < len(stripped):
if stripped[j:j+1] == b"=" and j + 2 < len(stripped):
hex_part = stripped[j+1:j+3]
try:
int(hex_part, 16)
escapes += 1
j += 3
except ValueError:
if not stripped[j+1:j+2] == b"\n":
issues.append(f"invalid escape at line {i} col {j}")
j += 1
else:
j += 1
return QpAnalysis(
line_count=len(lines),
max_line_length=max_len,
escape_count=escapes,
soft_breaks=soft_breaks,
valid=len(issues) == 0,
issues=issues,
)
# ─────────────────────────────────────────────────────────────────────────────
# 4. MIME message builder
# ─────────────────────────────────────────────────────────────────────────────
def build_mime_text(
body: str,
charset: str = "utf-8",
encoding: str = "quoted-printable",
subject: str = "",
sender: str = "",
recipient: str = "",
) -> MIMEText:
"""
Build a MIMEText message with explicit QP or base64 encoding.
encoding: "quoted-printable" or "base64"
Example:
msg = build_mime_text(
"bonjour café",
charset="utf-8",
subject="Test",
sender="[email protected]",
recipient="[email protected]"
)
print(msg.as_string()[:200])
"""
cset = Charset(charset)
if encoding == "quoted-printable":
cset.header_encoding = QP
cset.body_encoding = QP
# MIMEText handles encoding internally
msg = MIMEText(body, _charset=cset)
if subject:
msg["Subject"] = subject
if sender:
msg["From"] = sender
if recipient:
msg["To"] = recipient
return msg
def build_multipart_email(
plain_body: str,
html_body: str | None = None,
charset: str = "utf-8",
subject: str = "",
sender: str = "",
recipient: str = "",
) -> MIMEMultipart:
"""
Build a multipart/alternative email with QP-encoded plain and optional HTML parts.
Example:
msg = build_multipart_email(
plain_body="Hello, world!",
html_body="<p>Hello, <b>world</b>!</p>",
subject="Greetings",
)
print(msg.as_string()[:300])
"""
outer = MIMEMultipart("alternative")
if subject: outer["Subject"] = subject
if sender: outer["From"] = sender
if recipient: outer["To"] = recipient
outer.attach(build_mime_text(plain_body, charset=charset))
if html_body:
outer.attach(build_mime_text(html_body, charset=charset))
return outer
# ─────────────────────────────────────────────────────────────────────────────
# 5. File encode / decode helpers
# ─────────────────────────────────────────────────────────────────────────────
def encode_file(
src: str | Path,
dst: str | Path | None = None,
quotetabs: bool = True,
) -> Path:
"""
QP-encode a file. dst defaults to src + ".qp".
Example:
out_path = encode_file("report.txt")
print(out_path) # "report.txt.qp"
"""
src_path = Path(src)
dst_path = Path(dst) if dst else src_path.with_suffix(src_path.suffix + ".qp")
with src_path.open("rb") as inp, dst_path.open("wb") as out:
quopri.encode(inp, out, quotetabs=quotetabs)
return dst_path
def decode_file(
src: str | Path,
dst: str | Path | None = None,
) -> Path:
"""
Decode a QP-encoded file. dst defaults to stripping trailing ".qp".
Example:
orig = decode_file("report.txt.qp")
"""
src_path = Path(src)
if dst:
dst_path = Path(dst)
else:
name = src_path.name
dst_path = src_path.with_name(name[:-3] if name.endswith(".qp") else name + ".decoded")
with src_path.open("rb") as inp, dst_path.open("wb") as out:
quopri.decode(inp, out)
return dst_path
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import tempfile, os
print("=== quopri demo ===")
# ── encode / decode bytes ──────────────────────────────────────────────────
print("\n--- encode_qp / decode_qp ---")
samples = [
b"Hello, world!",
"Héllo caf\xe9 Zürich".encode("utf-8"),
b"Tab\there\nNewline\nand spaces ",
bytes(range(128, 145)),
]
for s in samples:
encoded = encode_qp(s, quotetabs=True)
decoded = decode_qp(encoded)
ok = decoded == s
print(f" {s[:30]!r} → {encoded[:40]!r} roundtrip={ok}")
# ── encode_text ────────────────────────────────────────────────────────────
print("\n--- encode_text ---")
text_samples = ["plain ASCII", "Héllo wörld", "日本語テキスト"]
for t in text_samples:
qp = encode_text(t, charset="utf-8")
back = decode_text(qp, charset="utf-8")
print(f" {t!r:25s} → {qp[:40]!r} back={back!r}")
# ── header word encoding ───────────────────────────────────────────────────
print("\n--- encode_header_word ---")
for name in ["John Smith", "René Müller", "山田 太郎"]:
word = encode_header_word(name, charset="utf-8")
print(f" {name!r:20s} → {word}")
# ── analyze_qp ────────────────────────────────────────────────────────────
print("\n--- analyze_qp ---")
test_data = encode_text("Héllo " * 20 + "\nSecond line with unicode: \xe9\xf6", charset="utf-8")
analysis = analyze_qp(test_data)
print(f" {analysis}")
if analysis.issues:
for issue in analysis.issues[:3]:
print(f" issue: {issue}")
# ── build_mime_text ────────────────────────────────────────────────────────
print("\n--- build_mime_text ---")
msg = build_mime_text(
"Bonjour,\n\nMerci de votre réponse. À bientôt!\n",
charset="utf-8",
subject="Test MIME QP",
sender="[email protected]",
recipient="[email protected]",
)
raw = msg.as_string()
for line in raw.splitlines()[:12]:
print(f" {line}")
# ── file encode/decode ─────────────────────────────────────────────────────
print("\n--- encode_file / decode_file ---")
with tempfile.TemporaryDirectory() as tmpdir:
src = os.path.join(tmpdir, "sample.txt")
with open(src, "wb") as f:
f.write("Héllo, wörld!\nЗдравствуйте!\n".encode("utf-8"))
qp_path = encode_file(src)
decoded_path = decode_file(qp_path)
orig = open(src, "rb").read()
back = open(decoded_path, "rb").read()
print(f" orig={len(orig)}B qp={qp_path.stat().st_size}B "
f"decoded={len(back)}B match={orig == back}")
print("\n=== done ===")
For the base64 alternative — base64.encodebytes() and base64.b64encode() encode binary data using Base64, producing a roughly 33% size increase but handling any byte value without =XX escaping — use base64 when encoding binary attachments (images, PDFs, zip files) where readability is irrelevant and compact encoding matters; use quopri for text/plain and text/html MIME body parts where the content is mostly ASCII (e.g. English prose with occasional accented characters) and where human-readable diff output and in-flight debugging are valued, since QP leaves ASCII characters unescaped. For the email.charset / email.encoders alternative — email.charset.Charset and email.encoders.encode_quopri() integrate QP encoding directly into the email.message.Message API — email.encoders.encode_quopri(msg) calls quopri.encodestring() internally and sets the Content-Transfer-Encoding header — use the email package’s high-level API when constructing multi-part MIME messages that need correct headers, charset parameters, and payload encoding in a single call; use quopri directly when implementing a custom transport layer, building a QP codec, or encoding/decoding QP data outside of the MIME context (e.g. HTTP form data, custom wire protocols). The Claude Skills 360 bundle includes quopri skill sets covering encode_qp()/decode_qp()/encode_qp_stream()/decode_qp_stream() byte-level codecs, encode_text()/decode_text()/encode_header_word() string helpers, QpAnalysis with analyze_qp() RFC 2045 validator, build_mime_text()/build_multipart_email() MIME builders, and encode_file()/decode_file() file-level tools. Start with the free tier to try MIME encoding patterns and quopri pipeline code generation.