Imported from padosoft/skills (
skills/padosoft-email-html-builder/SKILL.md). Install upstream withnpx skills add padosoft/skills --skill padosoft-email-html-builder. Copyright stays with the author (MIT).
Email HTML Builder
Produces HTML emails that are production ready on the first try: table layout with HTML attributes, inline CSS reduced to typography alone, a minimal <style>, an aligned text/plain part, one-click unsubscribe headers. The expected result on Mailtrap is spam ≤ 0.1 and an HTML Check with only the allowed warnings (§5). On MailUp Check-up: zero problems.
The complete normative rules with R-xxx IDs are in references/rules.md. The linter is scripts/lint_email.py and the reference template is templates/reference-welcome-dark.html. If those files are not available, the essential rules below are enough and are binding.
0. Included scripts
| Script | Use |
|---|---|
scripts/lint_email.py |
Rule validator. python3 scripts/lint_email.py email.html --text email.txt --subject "Subject" [--production] [--transactional] [--json]. Exit 1 if any MUST is violated, 0 when clean. |
scripts/build_payload.py |
Generates the Mailtrap JSON payload with List-Unsubscribe and One-Click. --help for the options. |
scripts/send_mailtrap_sandbox.sh / .ps1 |
Sandbox send. Variables: MAILTRAP_TOKEN, MAILTRAP_INBOX_ID. |
scripts/screenshot_email.py |
Screenshots at 600px and 375px, with and without <style> (requires Playwright; if it is missing, skip gate G2 and say so in the report). |
templates/reference-welcome-dark.html |
Compliant template to start from (0 MUST violated). |
references/rules.md |
Complete R-xxx rules. Read it when a warning is not in the §5 baseline, or when you need the rationale behind a rule. |
1. Mandatory workflow
Run the phases in order. Do not deliver before G1–G2 are green. If you have access to Mailtrap, run G3–G6 as well.
-
Brief. Work out or ask (once, in a single batch) what is missing:
- type of email, brand, language and light/dark theme;
- a palette of 2–3 colors, content, CTA and URLs;
- sending platform (MailUp, Mailtrap Sending, SES…) and the syntax of the dynamic fields;
- legal details for the footer.
If the brief is incomplete do not stall: use explicit placeholders and list them in the report.
-
Content design. Before writing code, settle:
- subject: 35–50 characters;
- preheader: 40–100 characters, complementary to the subject;
- a single primary CTA;
- text/image ratio ≥ 80/20;
- which blocks to use (header, hero, card, benefits, social, footer).
-
Code. Start from the skeleton in §3 (or from
templates/). Apply every MUST rule in §4. -
Text/plain. Write the text-only version with the same content and the same links in clear text.
-
G1 Lint. Run
python3 scripts/lint_email.py email.html --text email.txt --subject "…"and bring the violated MUSTs to 0. If the script is not there, work through the §6 checklist by hand. -
G2 Render. Take screenshots with Playwright/Chromium at 600px and 375px, then repeat them without
<style>and with images blocked. No breakage allowed. -
Payload and test send.
scripts/build_payload.pygenerates the JSON payload (List-Unsubscribe included);- the send starts with
scripts/send_mailtrap_sandbox.{ps1,sh}or with the Mailtrap connector (send-sandbox-email); - on Windows use PowerShell:
$env:VAR="…"and not theVAR=… cmdsyntax.
-
Without Mailtrap (no account or connector): declare G3–G6 as not run in the report, do not estimate them, and in exchange work through the manual checklist in §6 on top of the linter. Deliver anyway.
-
G3–G6 Mailtrap verification. Use
get-sandbox-message-html-analysis,get-sandbox-message-spam-scoreand the blacklist and text reports. Compare against the §5 baseline: every entry outside the baseline is a defect to fix and resend. Do not declare a removable warning a "false positive". -
G7 MailUp (if that is the sending platform): Check-up with no errors on blacklisted links, tracking enabled, unsubscribe present, weight, dynamic fields with defaults, summary, analytics, custom tracking domain, spam and code.
-
Delivery.
.html,.txtandpayload.jsonfiles;- a report with the outcome of the gates, remaining warnings with their rationale, placeholders still to be replaced and SHOULD exceptions with their rationale;
- a version number (
v1,v2…) in the test subject.
2. Key principle (the cause of the historical mistakes)
The Mailtrap checker uses caniemail.com data and reports every CSS property present, even when there is a fallback. bgcolor next to background-color does not remove the warning: the CSS property has to be deleted.
→ Layout with HTML attributes. Inline CSS for typography only. <style> only for rounded corners and media queries.
3. Reference skeleton
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no, url=no">
<meta name="color-scheme" content="dark">
<meta name="supported-color-schemes" content="dark">
<title>Title</title>
<!--[if mso]><noscript><xml><o:OfficeDocumentSettings><o:AllowPNG/><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><![endif]-->
<style>
.r10{border-radius:10px}.r9{border-radius:9px}.r9t{border-radius:9px 9px 0 0}
@media (max-width:620px){.wrap{width:100%!important}.col{display:block!important;width:100%!important}.vgap{display:block!important;width:100%!important;height:16px!important}.gut{width:20px!important}.h1m{font-size:26px!important;line-height:32px!important}.txtm{font-size:16px!important;line-height:24px!important}.imgm{width:100%!important;height:auto!important}}
</style>
</head>
<body bgcolor="#04050a" text="#aab3c5" link="#2ff5d6" vlink="#2ff5d6" alink="#2ff5d6" marginwidth="0" marginheight="0" topmargin="0" leftmargin="0">
<!--[if !mso]><!--><div aria-hidden="true" style="display:none; mso-hide:all;">Preheader of 40-100 characters, no invisible filler.</div><!--<![endif]-->
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#04050a">
<tr><td height="32" style="font-size:1px; line-height:32px;"> </td></tr>
<tr><td align="center" valign="top">
<table role="presentation" width="632" class="wrap" align="center" cellpadding="0" cellspacing="0" border="0"><tr>
<td width="16" style="font-size:1px; line-height:1px;"> </td>
<td align="center" valign="top">
<table role="presentation" class="wrap" width="600" align="center" cellpadding="0" cellspacing="0" border="0">
<!-- blocks -->
</table>
</td>
<td width="16" style="font-size:1px; line-height:1px;"> </td>
</tr></table>
</td></tr>
<tr><td height="32" style="font-size:1px; line-height:32px;"> </td></tr>
</table>
</body>
</html>
Block patterns (copy them, do not reinvent them)
Vertical and horizontal spacing (instead of padding):
<tr><td height="24" style="font-size:1px; line-height:24px;"> </td></tr>
<td class="gut" width="24" style="font-size:1px; line-height:1px;"> </td>
Card with border and corners (instead of border and background-color):
<table role="presentation" class="r10" width="100%" cellpadding="1" cellspacing="0" border="0" bgcolor="#1c2333"><tr><td>
<table role="presentation" class="r9" width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#0d1018">
<tr><td colspan="3" height="24" style="font-size:1px; line-height:24px;"> </td></tr>
<tr><td width="20" style="font-size:1px; line-height:1px;"> </td><td><!-- content --></td><td width="20" style="font-size:1px; line-height:1px;"> </td></tr>
<tr><td colspan="3" height="24" style="font-size:1px; line-height:24px;"> </td></tr>
</table>
</td></tr></table>
Accessible H1 heading (no <h1>):
<td align="center" class="h1m" role="heading" aria-level="1" style="font-family:Arial, Helvetica, sans-serif; font-size:30px; line-height:38px; font-weight:700; color:#f2f4f8;">Title</td>
Bulletproof CTA (VML and HTML with identical dimensions):
<!--[if mso]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="https://…" style="height:48px; v-text-anchor:middle; width:220px;" arcsize="21%" fillcolor="#2ff5d6" stroke="f"><w:anchorlock/><center style="color:#04050a; font-family:Arial, Helvetica, sans-serif; font-size:15px; font-weight:bold;">Browse the products</center></v:roundrect>
<![endif]-->
<!--[if !mso]><!-->
<table role="presentation" class="r10" width="220" cellpadding="0" cellspacing="0" border="0" bgcolor="#2ff5d6"><tr>
<td align="center" height="48" style="font-family:Arial, Helvetica, sans-serif; font-size:15px; line-height:48px; font-weight:700;"><a href="https://…" target="_blank" style="color:#04050a; text-decoration:none;">Browse the products</a></td>
</tr></table>
<!--<![endif]-->
Image in a card (the cell carries height and bgcolor, so it stays intact when images are blocked):
<td align="center" valign="middle" height="120" class="r9t" bgcolor="#161b27" style="font-size:0; line-height:0;"><a href="https://…" target="_blank"><img src="https://cdn.example.com/img/x@2x.jpg" width="174" height="120" border="0" alt="Short description" class="imgm"></a></td>
Columns that stack on mobile (whole px, sum = container):
<tr><td class="col" width="176" valign="top">…</td><td class="vgap" width="12" style="font-size:1px; line-height:1px;"> </td><td class="col" width="176" valign="top">…</td><td class="vgap" width="12" style="font-size:1px; line-height:1px;"> </td><td class="col" width="176" valign="top">…</td></tr>
Divider:
<tr><td height="1" bgcolor="#1c2333" style="font-size:1px; line-height:1px;"> </td></tr>
4. Essential MUST rules (IDs in references/rules.md)
Structure and weight
- Tables only,
role="presentation" cellpadding="0" cellspacing="0" border="0", a 600px container, a 16px outer gutter built with cells (R-004/005/006). - Forbidden:
<h1>,<script>,<form>,<iframe>,<video>,<svg>,<link>,@import, theon*handlers, empty tags and superfluous comments (R-007/008/009/011). - HTML under 100 KB (target 40 KB). Images ≤ 50 KB, GIFs ≤ 100 KB with a complete first frame (R-050/052/604).
CSS
-
Inline, only
font-family,font-size,line-height,font-weight,font-style,letter-spacing,color,text-decoration,text-alignandmso-*are allowed (R-100). -
Forbidden inline: padding, margin, background, border, border-radius, width, height, max/min-*, display, opacity, overflow, visibility, outline, box-shadow, position, float, text-transform, table-layout (R-101). The only exception is the preheader (
display:none; mso-hide:all). -
Mandatory substitutions (R-200…210):
Instead of Use backgroundbgcolorpadding empty cells with height/widthborder outer table with bgcolor+cellpadding="1"CSS width/height width/heightattributesbody margins marginwidth/marginheightdefault color text/linkon<body>gap under images font-size:0; line-height:0on the cellbackground images a solid color -
A single
<style>, with one line ofborder-radiusand one line of@media (max-width:620px){…}, withoutonly screen and. No reset, no preheader classes, noprefers-color-schemeor:root(R-300…303). -
No comment containing the string
<style. The email must stay readable even without the style block (R-304/305).
Antispam and deliverability
- Marketing vs transactional. The unsubscribe rules (
R-405List-Unsubscribeheader,R-454footer link) apply to commercial and bulk email. For a purely transactional one (order confirmation, password reset, shipping) they are not required: use--transactionalin the linter, still keep an "Email preferences" link, and state it in the report. All the other rules apply to both. - HTML 100% ASCII: accented characters as numeric entities (
è) (R-402). - Never use invisible filler in the preheader (
͏,‌, repeated ): it costs +2.4 SpamAssassin points (R-401). - Preheader without
color. No text in a color equal or close to the background, separators included (R-400/403). - Text/plain aligned with the HTML.
List-UnsubscribeandList-Unsubscribe-Post: List-Unsubscribe=One-Clickheaders (R-404/405). - Subject 35–50 characters (60 max): no ALL CAPS, no
!!!, no trigger words or more than one emoji (R-406). - Spam target ≤ 0.1, blacklist 0, no shorteners or bare IPs. Text/images ≥ 80/20. Sender domain with SPF+DKIM+DMARC, a monitored
Reply-To(R-407…412).
Links (MailUp Check-up)
- Absolute, working HTTPS links. Tracking enabled with a custom tracking domain (R-450/451).
- UTM
utm_source,utm_medium=email,utm_campaign, without spaces (R-452). - Dynamic fields in the platform syntax (MailUp
[field]) with a default value. In links the protocol must be explicit (https://[field]) (R-453). - Unsubscribe in the footer, two clicks at most (R-454).
tel:without spaces, asset paths without spaces (R-457/458).
Design and accessibility
color-scheme=darkorlight, neverlight darkwithout a complete dark theme (R-500).- Calculated contrast ≥ 4.5:1 for every normal text, footer included (R-501).
- Fonts: headings ≥ 22px, body ≥ 16px on mobile (
.txtmclass), legal ≥ 12px, CTA ≥ 15px. A palette of 2–3 colors (R-502/503). aria-hiddenon glyphs and separators.langand<title>filled in. DOM order = visual order (R-504/505).<img>withwidth,height,border="0", a shortalt, HTTPS src from the brand CDN (no placeholders). The cell carriesheightandbgcolor. Never put essential text inside an image only (R-600…603).- CTA VML + HTML with identical dimensions, area ≥ 46×46px, a single primary CTA (R-700…703).
- Columns in whole px (no
33.33%), no spacing asymmetry when they stack (R-800…802).
Footer: reason for receiving, legal entity, address, VAT number, contacts, Unsubscribe, Preferences, Privacy, copyright and social. In production, zero placeholders (R-900/901).
5. Mailtrap baseline (the only allowed warnings)
| Rule | Reason |
|---|---|
style |
The style block is needed for responsive and corners |
@media, @media max-width |
Mobile media queries |
width/display/height/font-size/line-height inside the media query |
Stacked columns and mobile typography |
border-radius (1 line) |
On Outlook the corners stay square |
display on the preheader |
It is the only way to hide it |
SpamAssassin in testing: MISSING_MID 0.1 (the production MTA adds the Message-ID) and HTML_MESSAGE 0.0. Any other entry has to be fixed.
Mailtrap Market Support ≥ 95% (for Mailtrap ≥ 90% is acceptable, below 85% it has to be redone).
6. Manual checklist (when the linter is not there)
- No
style=""in the body with properties outside the whitelist, preheader aside - Every background color is
bgcolor, every spacing is a cell, every border is a table withcellpadding="1" -
<style>= 1 radius line + 1@media (max-width:620px)line -
grep -P '[^\x00-\x7F]'over the HTML finds nothing - No
͏/‌in the source -
<body>withbgcolor,text,link,marginwidth,marginheight - Contrasts ≥ 4.5:1 calculated (script or tool), footer and separators included
- Every
<img>has width, height, border="0", alt, HTTPS src; the cell has height and bgcolor - CTA VML and HTML with the same width and height
- Columns in whole px with the correct sum
- Subject 35–50 characters, preheader 40–100
- Text/plain aligned, List-Unsubscribe and One-Click in the payload
- HTTPS links with UTM and unsubscribe present
- No placeholders if it is going to production
7. Delivery report format
Version: vN | HTML weight: xx KB | Subject: "…" (nn chars) | Preheader: nn chars
G1 Lint: PASS (0 MUST, n SHOULD) G2 Render: PASS (600/375/no-style/img-off)
G3 HTML Check: xx% — warnings: [baseline only | list of extras + fix]
G4 Spam: 0.1 (MISSING_MID) G5 Blacklist: 0 G6 HTML/Text: aligned
G7 MailUp Check-up: [outcome | not run]
SHOULD exceptions with rationale: …
Placeholders to replace: …
Next step: …
Always flag the uncertainties explicitly, for example the syntax of the platform's unsubscribe placeholder that still has to be verified on the account.
