Python’s mailcap module reads /etc/mailcap and ~/.mailcap UNIX files to find programs registered to handle MIME types. import mailcap. Load database: caps = mailcap.getcaps() → dict[str, list[dict]] — keys are MIME types like "text/html", "image/png", "application/pdf". Find handler: match, cmd = mailcap.findmatch(caps, "image/jpeg", filename="/tmp/photo.jpg") → (entry_dict, cmd_string) or (None, None). Key parameter: mailcap.findmatch(caps, "text/html", key="print", ...) — selects the print= capability instead of the default view=. Template substitution: %s → filename, %t → MIME type, %% → literal %. Wildcards: "image/*" matches any image type; "*" is the global fallback. Note: mailcap is UNIX-specific and reads ~/.mailcap, ~/.mime.types etc.; the spec comes from RFC 1524. Deprecated since Python 3.11 — use only in UNIX sysadmin tools that need native capability lookup. Claude Code generates file preview launchers, MIME type routers, content-type-aware open handlers, and media player selectors.
CLAUDE.md for mailcap
## mailcap Stack
- Stdlib: import mailcap (Unix only; deprecated Python 3.11)
- Load: caps = mailcap.getcaps() # read /etc/mailcap + ~/.mailcap
- Find: match, cmd = mailcap.findmatch(caps, "image/jpeg", filename="/tmp/a.jpg")
- Key: mailcap.findmatch(caps, "text/html", key="print", filename="doc.html")
- Note: returns (None, None) when no handler found
- cmd is ready to pass to subprocess.run(cmd, shell=True)
mailcap MIME Capability Pipeline
# app/mailcaputil.py — loader, finder, router, launcher, inventory
from __future__ import annotations
import mailcap
import mimetypes
import os
import re
import shlex
import subprocess
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
# ─────────────────────────────────────────────────────────────────────────────
# 1. Database loading
# ─────────────────────────────────────────────────────────────────────────────
_caps_cache: "dict[str, list[dict[str, Any]]] | None" = None
def get_caps(reload: bool = False) -> dict[str, list[dict[str, Any]]]:
"""
Return the cached mailcap capability database.
Call with reload=True to re-read from disk.
Example:
caps = get_caps()
print(list(caps.keys())[:5])
"""
global _caps_cache
if _caps_cache is None or reload:
_caps_cache = mailcap.getcaps()
return _caps_cache
def list_types(caps: "dict[str, list[dict]] | None" = None) -> list[str]:
"""
Return all MIME types registered in the mailcap database, sorted.
Example:
for t in list_types():
print(t)
"""
c = caps or get_caps()
return sorted(c.keys())
# ─────────────────────────────────────────────────────────────────────────────
# 2. Handler lookup
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class CapabilityResult:
"""Result of a mailcap handler lookup."""
mime_type: str
key: str
found: bool
command: str = ""
entry: "dict[str, Any] | None" = None
def shell_args(self) -> list[str]:
"""Return the command split for subprocess.run with shell=False."""
return shlex.split(self.command) if self.command else []
def find_handler(
mime_type: str,
filename: str = "",
key: str = "view",
plist: "list[str] | None" = None,
caps: "dict[str, list[dict]] | None" = None,
) -> CapabilityResult:
"""
Find the mailcap handler for a MIME type and optional key.
Returns a CapabilityResult with found=False if no handler exists.
Example:
r = find_handler("text/html", filename="/tmp/page.html")
if r.found:
subprocess.run(r.command, shell=True)
"""
c = caps or get_caps()
entry, cmd = mailcap.findmatch(
c, mime_type, key=key,
filename=filename or "/dev/null",
plist=plist or [],
)
return CapabilityResult(
mime_type=mime_type,
key=key,
found=entry is not None,
command=cmd or "",
entry=entry,
)
def find_handler_for_file(
path: "str | Path",
key: str = "view",
caps: "dict[str, list[dict]] | None" = None,
) -> CapabilityResult:
"""
Guess the MIME type from the file extension and find its handler.
Example:
r = find_handler_for_file("/tmp/report.pdf")
if r.found:
subprocess.run(r.command, shell=True)
"""
p = Path(path)
mime, _ = mimetypes.guess_type(str(p))
mime = mime or "application/octet-stream"
return find_handler(mime, filename=str(p), key=key, caps=caps)
# ─────────────────────────────────────────────────────────────────────────────
# 3. MIME type router
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class RouteResult:
"""Which handler program is preferred for a given MIME type."""
mime_type: str
program: str = "" # extracted program name (first token of command)
command: str = ""
found: bool = False
def route_mime(
mime_type: str,
filename: str = "",
caps: "dict[str, list[dict]] | None" = None,
) -> RouteResult:
"""
Return the preferred viewer for mime_type and extract the program name.
Example:
r = route_mime("application/pdf")
print(r.program) # "evince" or "okular" etc.
"""
result = find_handler(mime_type, filename=filename, caps=caps)
program = ""
if result.found and result.command:
tokens = shlex.split(result.command)
program = os.path.basename(tokens[0]) if tokens else ""
return RouteResult(mime_type=mime_type, program=program,
command=result.command, found=result.found)
def inventory(caps: "dict[str, list[dict]] | None" = None) -> list[RouteResult]:
"""
Return RouteResult for every MIME type in the database.
Example:
for r in inventory():
if r.found:
print(f"{r.mime_type:30s} → {r.program}")
"""
c = caps or get_caps()
results: list[RouteResult] = []
for mime_type in sorted(c.keys()):
results.append(route_mime(mime_type, caps=c))
return results
# ─────────────────────────────────────────────────────────────────────────────
# 4. Safe launcher
# ─────────────────────────────────────────────────────────────────────────────
def launch_viewer(
path: "str | Path",
key: str = "view",
dry_run: bool = False,
) -> "subprocess.CompletedProcess | None":
"""
Find and launch the mailcap viewer for a file (by extension → MIME type).
dry_run=True prints the command without executing.
Returns None if no handler found.
Example:
proc = launch_viewer("/tmp/image.png")
proc = launch_viewer("/tmp/document.pdf", dry_run=True)
"""
r = find_handler_for_file(path, key=key)
if not r.found:
print(f" No mailcap handler for {r.mime_type}")
return None
if dry_run:
print(f" [dry-run] {r.command}")
return None
return subprocess.run(r.command, shell=True)
def has_handler(mime_type: str, key: str = "view") -> bool:
"""
Return True if a mailcap handler exists for the given MIME type and key.
Example:
has_handler("application/pdf") # True or False
has_handler("text/html", key="print")
"""
return find_handler(mime_type, key=key).found
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== mailcap demo ===")
if os.name == "nt":
print(" mailcap is Unix-only — skipping on Windows")
else:
caps = get_caps()
print(f"\n loaded {len(caps)} MIME type entries from mailcap database")
# ── list_types ─────────────────────────────────────────────────────────
print("\n--- list_types (first 10) ---")
types = list_types(caps)
for t in types[:10]:
print(f" {t}")
# ── find_handler ──────────────────────────────────────────────────────
print("\n--- find_handler ---")
for mime, fname in [
("text/html", "/tmp/page.html"),
("application/pdf", "/tmp/doc.pdf"),
("image/png", "/tmp/img.png"),
("video/mp4", "/tmp/film.mp4"),
("text/plain", "/tmp/readme.txt"),
]:
r = find_handler(mime, filename=fname, caps=caps)
status = f"cmd={r.command[:50]!r}" if r.found else "NOT FOUND"
print(f" {mime:25s}: {status}")
# ── inventory (first 8 found) ─────────────────────────────────────────
print("\n--- inventory (first 8 with handlers) ---")
inv = [r for r in inventory(caps) if r.found]
for r in inv[:8]:
print(f" {r.mime_type:30s} → {r.program}")
# ── has_handler ────────────────────────────────────────────────────────
print("\n--- has_handler ---")
for mime in ["text/html", "application/pdf", "image/svg+xml", "nope/nope"]:
print(f" {mime:30s}: view={has_handler(mime)}")
# ── launch_viewer dry_run ─────────────────────────────────────────────
print("\n--- launch_viewer (dry_run) ---")
for path in ["/tmp/test.html", "/tmp/test.pdf", "/tmp/test.xyz"]:
print(f" {path}: ")
launch_viewer(path, dry_run=True)
print("\n=== done ===")
For the mimetypes stdlib alternative — mimetypes.guess_type(filename) maps file extensions to MIME type strings, and mimetypes.guess_extension(mime_type) goes the other direction — use mimetypes when you need MIME-type detection from filenames; use mailcap when you additionally need the program registered to handle that MIME type on Unix. For the xdg-open / open / os.startfile alternative — subprocess.run(["xdg-open", filename]) on Linux, subprocess.run(["open", filename]) on macOS, and os.startfile(filename) on Windows delegate MIME handling to the OS desktop environment without any Python lookup — use this approach for consumer desktop apps where you want the system default for any file; use mailcap.findmatch when you need programmatic control over which specific program to invoke or need to inspect available capabilities. The Claude Skills 360 bundle includes mailcap skill sets covering get_caps()/list_types() database helpers, CapabilityResult/find_handler()/find_handler_for_file() lookups, RouteResult/route_mime()/inventory() router, and launch_viewer()/has_handler() utilities. Start with the free tier to try mailcap patterns and MIME handler pipeline code generation.