Skip to content

Instantly share code, notes, and snippets.

@Konfekt
Created February 20, 2026 20:55
Show Gist options
  • Select an option

  • Save Konfekt/4f9f08fbd23a341d3f10ff7514b95af3 to your computer and use it in GitHub Desktop.

Select an option

Save Konfekt/4f9f08fbd23a341d3f10ff7514b95af3 to your computer and use it in GitHub Desktop.
View an HTML email with CID-embedded images in w3m from mutt/neomutt
#!/usr/bin/env python3
"""
View an HTML email with CID-embedded images in w3m from mutt/neomutt using a
macro in muttrc that pipes the whole message to this script such as
macro index l "<pipe-message>mutt-view-html-with-images.py<enter>" "View HTML with images"
Steps performed by the script:
1. Pipe the raw message to stdin.
2. Extract image parts to a temp directory.
3. Rewrite cid: URLs in HTML to extracted filenames.
4. Open HTML via w3m with working relative paths.
5. Clean up on exit.
"""
import email
import mimetypes
import os
import re
import shutil
import subprocess
import sys
import tempfile
from email import policy
def decode_text_payload(part: email.message.Message) -> str:
try:
content = part.get_content()
if isinstance(content, str):
return content
payload = content if isinstance(content, (bytes, bytearray)) else b""
except Exception:
payload = part.get_payload(decode=True) or b""
charset = part.get_content_charset() or "utf-8"
try:
return payload.decode(charset, errors="replace")
except LookupError:
return payload.decode("utf-8", errors="replace")
def main() -> int:
msg = email.message_from_bytes(sys.stdin.buffer.read(), policy=policy.default)
w3m = shutil.which("w3m")
if not w3m:
print("w3m not found in PATH.", file=sys.stderr)
return 1
with tempfile.TemporaryDirectory(prefix="mutt-html-") as tempdir:
image_map: dict[str, str] = {}
html_parts: list[str] = []
image_counter = 1
def unique_filename(name: str) -> str:
base, ext = os.path.splitext(name)
candidate = name
n = 1
while os.path.exists(os.path.join(tempdir, candidate)):
n += 1
candidate = f"{base}-{n}{ext}"
return candidate
for part in msg.walk():
if part.get_content_maintype() == "text" and part.get_content_subtype() == "html":
html_parts.append(decode_text_payload(part))
continue
if part.get_content_maintype() != "image":
continue
filename = os.path.basename(part.get_filename() or "")
if not filename:
ext = mimetypes.guess_extension(part.get_content_type()) or ".bin"
filename = f"image{image_counter}{ext}"
image_counter += 1
filename = unique_filename(filename)
with open(os.path.join(tempdir, filename), "wb") as f:
f.write(part.get_payload(decode=True) or b"")
cid = part.get("Content-ID")
if cid:
image_map[cid.strip().strip("<>").strip()] = filename
if not html_parts:
print("No HTML part found in the message.", file=sys.stderr)
return 1
html = html_parts[0]
cid_pattern = re.compile(r"cid:\s*<?\s*([^\"'>\s]+)\s*>?", flags=re.IGNORECASE)
def replace_cid(m: re.Match[str]) -> str:
cid = m.group(1).strip().strip("<>").strip()
return image_map.get(cid, m.group(0))
html = cid_pattern.sub(replace_cid, html)
with open(os.path.join(tempdir, "body.html"), "w", encoding="utf-8") as f:
f.write(html)
# input("Press Enter to return to mutt...")
proc = subprocess.run(
[
w3m,
"-F",
"-o",
"auto_image=1",
"-o",
"display_image=1",
"-o",
"ext_image_viewer=1",
"-T",
"text/html",
"body.html",
],
cwd=tempdir,
)
return proc.returncode
if __name__ == "__main__":
raise SystemExit(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment