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
# 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
- 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.
- 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.
- 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.
- 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.
1---2name: eml-to-email-html3description: 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]".4---56# EML → ESP-safe HTML78Convert a raw email export into HTML you can safely import into an email platform's custom-HTML9editor — with the guards a plain conversion misses.1011## Why plain conversion is not enough1213- **Some ESPs re-serialize custom HTML at send time** (measured on MailerLite): every tag on its14 own line, indented per DOM depth, prose word-wrapped with the indent repeated. Measured: a 54KB15 source became a 106KB sent email (48% whitespace).16- **Gmail clips messages over ~102KB** — the bottom of the email (footer buttons, unsubscribe link)17 silently vanishes behind "[Message clipped]". That's what most "the buttons don't show" reports18 actually are.19- **Minifying doesn't help** if the ESP re-indents — only a shallower DOM, fewer tags, or less text20 does. Working budget: source ≤ 50KB, projected sent ≤ ~88KB.21- **Merge-tag unsubscribe links render inactive in test-mode sends by design** — a "dead22 unsubscribe link" in a *test* email is usually not a bug.2324## Workflow2526```bash27# 1. convert: decode QP/base64, extract cid: images, rewrite refs, build a ZIP28python3 scripts/eml_to_html.py "<file>.eml" <outdir>2930# 2. audit + slim in one pass (writes <slug>_slim.html + _slim_package.zip)31python3 scripts/esp_size_check.py <outdir>/<slug>.html --slim32```33343. Read the audit verdict. FAIL = do not import; fix and re-run. The slimmer makes only35 render-neutral removals (unreferenced `gmail-*` classes, no-op default CSS, empty anchor divs,36 comments) — it never touches layout.374. **Still over budget after slimming?** The remaining weight is structural (deeply nested tables38 from email exports). Say so plainly: rebuild flat (~3 tables, depth 3) or cut content. Don't39 import a failing file hoping for the best.405. **Choose ONE image-import path — they are mutually exclusive** (the audit FAILs on HTML that41 mixes them):42 - **ZIP path** (default for .eml conversions): EVERY `<img src>` must be a relative filename43 bundled in the ZIP. The importer only relinks bundled files — hosted-URL images are dropped44 and show as "Select Image" placeholders. Download any hosted asset into the bundle first.45 - **Paste path** (a clean, hosted template): EVERY `<img src>` must be an absolute hosted URL.46 Relative srcs break when pasting.476. Add the unsubscribe merge tag inside the ESP (not the imported HTML), and verify it via a real48 send to yourself, not a test send.4950## The privacy scan is not optional5152A re-downloaded "own sent mail" .eml can embed a `---------- Forwarded message ----------` block53whose `Bcc:` line carries the full recipient list in plain text. Importing one can ship an entire54mailing list to every recipient. `esp_size_check.py` FAILs on forwarded blocks and To:/Cc:/Bcc:55remnants (English and localized), and lists every visible email address for a human check. Fix56before import, no exceptions.5758## Reading the audit output5960| Finding | Meaning | Action |61|---|---|---|62| SIZE FAIL / WARN | projected sent > 102KB / near it | `--slim`, then flatten or cut |63| LINK FAIL (cid: / empty / `#` / relative) | broken image, dead button or link | fix the href/src, re-run |64| IMG MIXED FAIL (relative + hosted URL) | broken under EVERY import path | bundle ALL images, or host ALL and paste |65| PRIVACY FAIL | forwarded block / Bcc remnants | delete the block, re-audit |6667## Common traps6869- Images show as gray "Select Image" placeholders right after a ZIP import → hosted-URL `<img>`70 mixed into the ZIP path. Bundle every image and re-import — it's not a dead-URL problem.71- "Links are dead" in a *test* send → test mode disables the unsubscribe merge tag; real-send a72 duplicated campaign to yourself instead.73- Footer icons/buttons invisible → Gmail clipping (size budget blown), not a conversion bug. Check74 the projected size, not the source size.75- Hand-stripping `gmail-*` classes: the ones referenced by `@media` rules are the responsive76 layout — deleting them breaks mobile. The slimmer preserves style-referenced classes; use it.