Email, explained for
web developers.
A practical guide covering HTML email rendering, transactional sending services, SPF/DKIM/DMARC authentication, and testing tools — everything you need without the marketing fluff.
HTML Email Fundamentals
§Email HTML is not web HTML. Clients strip or ignore most CSS properties, block external stylesheets, and render in rendering engines you haven't thought about since 2003. The key rules:
- Use tables for layout. Flexbox and CSS Grid are unsupported in Outlook (Windows) and older Gmail apps. Nest
<table>elements to build multi-column layouts. - Inline every style. Gmail strips
<style>blocks in forwarded mail and some mobile views. Use inlinestyle=""on every element, or let a framework/build tool do it for you. - Max width 600–680px. Most preview panes and mobile clients clip wider emails. Set
width="600"on the outer table (HTML attribute, not just CSS) for Outlook compatibility. - Host images on a CDN. Attach
widthandheightattributes directly on<img>tags so layout doesn't break when images are blocked by default. Usealttext that still communicates the message. - Support dark mode. Apple Mail, Outlook 2021+, and Gmail app (Android) invert colors in dark mode. Use
@media (prefers-color-scheme: dark)in a<style>block — most modern clients support it even when they strip other styles. - Always send a plain-text part. MIME multipart/alternative with both
text/plainandtext/htmlparts improves deliverability and serves users on screen readers or minimal clients.
Minimal table skeleton
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Your Subject Line</title>
<!--[if mso]>
<noscript><xml><o:OfficeDocumentSettings>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings></xml></noscript>
<![endif]-->
</head>
<body style="margin:0;padding:0;background:#f4f4f4;">
<!-- Outer wrapper -->
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center" style="padding:40px 0;">
<!-- Email card -->
<table role="presentation" width="600" cellpadding="0" cellspacing="0"
style="background:#ffffff;border-radius:8px;">
<tr>
<td style="padding:40px 48px;">
<h1 style="margin:0 0 16px;font-family:sans-serif;font-size:24px;">
Hello!
</h1>
<p style="margin:0;font-family:sans-serif;font-size:16px;line-height:1.6;">
Your message here.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>Frameworks & Tooling
§Writing raw email HTML by hand is painful. These tools abstract the table-soup and inline CSS away.
React Email
Write email templates as React components using purpose-built primitives (Button, Section, Text…). Renders to email-safe HTML with inline styles. Ships a preview dev server.
MJML
XML-like markup (<mj-section>, <mj-text>…) that compiles to cross-client HTML. Battle-tested, huge community, VS Code extension available.
Maizzle
Tailwind CSS utility classes in email — Maizzle inlines all styles at build time. Good choice if you already think in Tailwind and want rapid prototyping.
Parcel (email mode)
Parcel's built-in email transformer inlines CSS and optimises images. Works with plain HTML/MJML. Good for teams that want a standard build pipeline.
Handlebars / Nunjucks
Server-side templating for dynamic transactional email. Pair with juice for CSS inlining. Simple, no build step beyond your server rendering.
Foundation for Emails
Zurb's Inky templating language. Similar idea to MJML — custom tags compile to tables. Less actively maintained but stable and widely used in large orgs.
Transactional Email Services
§Don't run your own SMTP server for transactional mail — IP reputation, bounce handling, and feedback loops are full-time jobs. Pick a provider.
| Provider | Free tier | DX | REST API | SMTP relay | Best for |
|---|---|---|---|---|---|
| Resend | 3 000 / mo | ⭐⭐⭐⭐⭐ | ✓ | ✓ | Modern dev teams, React Email |
| Postmark | 100 / mo | ⭐⭐⭐⭐⭐ | ✓ | ✓ | Deliverability-first transactional |
| AWS SES | 62 000 / mo* | ⭐⭐⭐ | ✓ | ✓ | High volume, AWS shops |
| SendGrid | 100 / day | ⭐⭐⭐⭐ | ✓ | ✓ | Marketing + transactional combo |
| Mailgun | 100 / day (trial) | ⭐⭐⭐⭐ | ✓ | ✓ | API-first, good logs |
| Brevo (ex-Sendinblue) | 300 / day | ⭐⭐⭐ | ✓ | ✓ | Budget-friendly, EU data residency |
* AWS SES free tier applies to emails sent from EC2 instances or Lambda. Sending from outside AWS costs $0.10/1 000.
Sending from Node.js
§Nodemailer (SMTP)
The classic Node.js mailer. Works with any SMTP relay.
import nodemailer from 'nodemailer';
const transport = nodemailer.createTransport({
host: 'smtp.postmarkapp.com',
port: 587,
auth: {
user: process.env.POSTMARK_API_KEY,
pass: process.env.POSTMARK_API_KEY,
},
});
await transport.sendMail({
from: '"Acme" <hello@acme.com>',
to: user.email,
subject: 'Welcome to Acme',
text: 'Thanks for signing up.',
html: '<p>Thanks for signing up.</p>',
});Resend SDK
Minimal REST wrapper — no SMTP config needed.
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
const { data, error } = await resend.emails.send({
from: 'Acme <hello@acme.com>',
to: [user.email],
subject: 'Welcome to Acme',
html: '<p>Thanks for signing up.</p>',
});
if (error) throw new Error(error.message);Development: catch all outgoing mail locally
# Mailpit — modern Mailhog replacement
docker run -d -p 1025:1025 -p 8025:8025 axllent/mailpit
# Then point Nodemailer at localhost:1025 (no auth)
# Open http://localhost:8025 to read caught mailEmail Authentication: SPF, DKIM & DMARC
§Since February 2024, Google and Yahoo require bulk senders to have SPF, DKIM, and DMARC configured. Even for low volume you should set these up — they directly affect inbox placement.
v=spf1 include:_spf.google.com include:spf.postmarkapp.com ~allList every service that sends mail for your domain. End with ~all (softfail) while testing, switch to -all (hardfail) once stable.
v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUA...Your sending provider generates the key pair. You publish the public key as a DNS record. The selector prefix (pm above) is provider-specific.
v=DMARC1; p=quarantine; pct=100; rua=mailto:dmarc@yourdomain.comStart with p=none to monitor, then escalate to quarantine → reject once you're confident all legitimate senders pass. rua is where ISPs send aggregate XML reports.
Deliverability
§Good deliverability is the combined result of technical setup, list hygiene, and sending behaviour. Here's what actually moves the needle:
- Use a subdomain for transactional mail. Send from
mail.acme.comornotifications.acme.comrather than your root domain. Reputation problems on one subdomain don't contaminate the other. - Handle bounces and complaints immediately. Subscribe to your provider's webhook events for
bounce,complaint, andunsubscribe. Suppress those addresses within seconds — ISPs watch your bounce and complaint rates closely. - Warm up new IPs and domains. Start with a small volume (a few hundred / day) and ramp up over 2–4 weeks. Jumping from 0 to 50 000 emails/day triggers spam filters.
- Apple Mail Privacy Protection (MPP) inflates open rates. Since iOS 15, Apple pre-fetches tracking pixels for ~60 % of your list. Open rates are no longer a reliable metric. Use click-through rate and conversion instead.
- One-click unsubscribe (RFC 8058). Required by Google and Yahoo for bulk senders. Add
List-UnsubscribeandList-Unsubscribe-Postheaders so mail clients display a native unsubscribe button.
Testing Tools
§Test at three levels: rendering across clients, catching mail during development, and validating authentication records.
Mailpit
Local SMTP catcher with a web UI. Docker image is tiny. Replaces the unmaintained Mailhog. Run it in dev; zero production risk.
Mailtrap
Hosted fake SMTP inbox — safe to use from CI/staging. Also has a real sending infra product and an email preview UI for client rendering checks.
Litmus
Gold standard for cross-client rendering previews. Tests in 100+ clients including Outlook 2013–2021, Gmail, Apple Mail, and mobile apps.
Email on Acid
Litmus competitor with similar client coverage. Often slightly cheaper. Good accessibility checker built in.
mail-tester.com
Send a test email to a unique address, get a deliverability score. Checks SPF, DKIM, DMARC, spam-filter heuristics, and HTML size.
MXToolbox
DNS record lookup, blacklist checks, and SMTP diagnostics. Paste your domain and see SPF/DKIM/DMARC parsed and validated instantly.