# Eml To Email HTML

> Convert a .eml file (a "Show original" / "Download original" email export) into clean, import-ready HTML for an email service provider's custom-HTML editor — decoding the encoded body, extracting embedded images, and rewriting references. Use on a .eml upload with reuse/import intent, or when debugging an ESP import where footer buttons/social icons disappear, images show as "Select Image" placeholders after a ZIP import, links look dead, or Gmail shows "[Message clipped]".

- Skill: `lucasksjang/eml-to-email-html` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add lucasksjang/eml-to-email-html`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lucasksjang/eml-to-email-html/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: LucasKSJang (https://skillmd.com/u/lucasksjang)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lucasksjang/eml-to-email-html

---


# EML → ESP-safe HTML

Convert a raw email export into HTML you can safely import into an email platform's custom-HTML
editor — with the guards a plain conversion misses.

## Why plain conversion is not enough

- **Some ESPs re-serialize custom HTML at send time** (measured on MailerLite): every tag on its
  own line, indented per DOM depth, prose word-wrapped with the indent repeated. Measured: a 54KB
  source became a 106KB sent email (48% whitespace).
- **Gmail clips messages over ~102KB** — the bottom of the email (footer buttons, unsubscribe link)
  silently vanishes behind "[Message clipped]". That's what most "the buttons don't show" reports
  actually are.
- **Minifying doesn't help** if the ESP re-indents — only a shallower DOM, fewer tags, or less text
  does. Working budget: source ≤ 50KB, projected sent ≤ ~88KB.
- **Merge-tag unsubscribe links render inactive in test-mode sends by design** — a "dead
  unsubscribe link" in a *test* email is usually not a bug.

## Workflow

```bash
# 1. convert: decode QP/base64, extract cid: images, rewrite refs, build a ZIP
python3 scripts/eml_to_html.py "<file>.eml" <outdir>

# 2. audit + slim in one pass (writes <slug>_slim.html + _slim_package.zip)
python3 scripts/esp_size_check.py <outdir>/<slug>.html --slim
```

3. Read the audit verdict. FAIL = do not import; fix and re-run. The slimmer makes only
   render-neutral removals (unreferenced `gmail-*` classes, no-op default CSS, empty anchor divs,
   comments) — it never touches layout.
4. **Still over budget after slimming?** The remaining weight is structural (deeply nested tables
   from email exports). Say so plainly: rebuild flat (~3 tables, depth 3) or cut content. Don't
   import a failing file hoping for the best.
5. **Choose ONE image-import path — they are mutually exclusive** (the audit FAILs on HTML that
   mixes them):
   - **ZIP path** (default for .eml conversions): EVERY `<img src>` must be a relative filename
     bundled in the ZIP. The importer only relinks bundled files — hosted-URL images are dropped
     and show as "Select Image" placeholders. Download any hosted asset into the bundle first.
   - **Paste path** (a clean, hosted template): EVERY `<img src>` must be an absolute hosted URL.
     Relative srcs break when pasting.
6. Add the unsubscribe merge tag inside the ESP (not the imported HTML), and verify it via a real
   send to yourself, not a test send.

## The privacy scan is not optional

A re-downloaded "own sent mail" .eml can embed a `---------- Forwarded message ----------` block
whose `Bcc:` line carries the full recipient list in plain text. Importing one can ship an entire
mailing list to every recipient. `esp_size_check.py` FAILs on forwarded blocks and To:/Cc:/Bcc:
remnants (English and localized), and lists every visible email address for a human check. Fix
before import, no exceptions.

## Reading the audit output

| Finding | Meaning | Action |
|---|---|---|
| SIZE FAIL / WARN | projected sent > 102KB / near it | `--slim`, then flatten or cut |
| LINK FAIL (cid: / empty / `#` / relative) | broken image, dead button or link | fix the href/src, re-run |
| IMG MIXED FAIL (relative + hosted URL) | broken under EVERY import path | bundle ALL images, or host ALL and paste |
| PRIVACY FAIL | forwarded block / Bcc remnants | delete the block, re-audit |

## Common traps

- Images show as gray "Select Image" placeholders right after a ZIP import → hosted-URL `<img>`
  mixed into the ZIP path. Bundle every image and re-import — it's not a dead-URL problem.
- "Links are dead" in a *test* send → test mode disables the unsubscribe merge tag; real-send a
  duplicated campaign to yourself instead.
- Footer icons/buttons invisible → Gmail clipping (size budget blown), not a conversion bug. Check
  the projected size, not the source size.
- Hand-stripping `gmail-*` classes: the ones referenced by `@media` rules are the responsive
  layout — deleting them breaks mobile. The slimmer preserves style-referenced classes; use it.

