Skip to main content
Technology & EngineeringEmail Template358 lines

Email Deliverability

Email deliverability essentials including SPF, DKIM, DMARC, and inbox placement

Quick Summary33 lines
You are an expert in email deliverability, covering authentication protocols (SPF, DKIM, DMARC), sender reputation, and inbox placement optimization.

## Key Points

- `v=spf1` — SPF version identifier
- `include:_spf.google.com` — authorize Google Workspace servers
- `include:amazonses.com` — authorize Amazon SES
- `-all` — fail (reject) anything not explicitly authorized
1. Sending server signs the email headers and body with a private key.
2. The signature is added as a `DKIM-Signature` header.
3. The receiving server retrieves the public key from DNS using the selector.
4. It verifies the signature to confirm the message was not modified in transit.
- DMARC policy must be `quarantine` or `reject`
- Logo must be a Tiny PS SVG (specific SVG profile)
- A Verified Mark Certificate (VMC) from DigiCert or Entrust is required for Gmail
- Implement all three authentication protocols (SPF, DKIM, DMARC) before sending any production email. They are table stakes for deliverability in 2026.

## Quick Example

```
Sending Server → Recipient Server
     │
     ├─ SPF:   "Is this server allowed to send for this domain?"
     ├─ DKIM:  "Was this message tampered with in transit?"
     └─ DMARC: "What should I do if SPF or DKIM fails?"
```

```dns
; Basic SPF record
example.com.  IN  TXT  "v=spf1 include:_spf.google.com include:amazonses.com include:mail.zendesk.com -all"
```
skilldb get email-template-skills/Email DeliverabilityFull skill: 358 lines
Paste into your CLAUDE.md or agent config

Email Deliverability — Email Templates

You are an expert in email deliverability, covering authentication protocols (SPF, DKIM, DMARC), sender reputation, and inbox placement optimization.

Core Philosophy

Overview

Deliverability is the measure of whether your emails reach the recipient's inbox rather than being filtered to spam or rejected entirely. It depends on three pillars: authentication (proving you are who you say you are), reputation (your track record as a sender), and content quality (what you are sending). Even a perfectly designed email template is worthless if it lands in spam.

Core Concepts

Email Authentication Stack

The three protocols work together as layers of defense:

Sending Server → Recipient Server
     │
     ├─ SPF:   "Is this server allowed to send for this domain?"
     ├─ DKIM:  "Was this message tampered with in transit?"
     └─ DMARC: "What should I do if SPF or DKIM fails?"

SPF (Sender Policy Framework)

SPF is a DNS TXT record that lists which servers are authorized to send email for your domain:

; Basic SPF record
example.com.  IN  TXT  "v=spf1 include:_spf.google.com include:amazonses.com include:mail.zendesk.com -all"

Breakdown:

  • v=spf1 — SPF version identifier
  • include:_spf.google.com — authorize Google Workspace servers
  • include:amazonses.com — authorize Amazon SES
  • -all — fail (reject) anything not explicitly authorized

SPF mechanisms:

MechanismMeaning
+allAllow all (never use this)
~allSoft fail — accept but mark
-allHard fail — reject unauthorized
include:domainInclude another domain's SPF record
ip4:203.0.113.0/24Authorize an IP range
aAuthorize the domain's A record IP
mxAuthorize the domain's MX record IPs

DKIM (DomainKeys Identified Mail)

DKIM attaches a cryptographic signature to each outgoing message. The recipient verifies it against a public key published in DNS:

; DKIM public key record
selector1._domainkey.example.com.  IN  TXT  "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC..."

How it works:

  1. Sending server signs the email headers and body with a private key.
  2. The signature is added as a DKIM-Signature header.
  3. The receiving server retrieves the public key from DNS using the selector.
  4. It verifies the signature to confirm the message was not modified in transit.

A DKIM-Signature header looks like:

DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
  d=example.com; s=selector1;
  h=from:to:subject:date:message-id;
  bh=abc123...;
  b=xyz789...

DMARC (Domain-based Message Authentication, Reporting, and Conformance)

DMARC ties SPF and DKIM together and tells receiving servers what to do when authentication fails:

; DMARC record
_dmarc.example.com.  IN  TXT  "v=DMARC1; p=reject; rua=mailto:dmarc-reports@example.com; ruf=mailto:dmarc-forensic@example.com; adkim=s; aspf=s; pct=100"

Parameters:

TagMeaningValues
pPolicynone (monitor), quarantine (spam), reject (block)
ruaAggregate report recipientEmail address
rufForensic report recipientEmail address
adkimDKIM alignmentr (relaxed), s (strict)
aspfSPF alignmentr (relaxed), s (strict)
pctPercentage of messages to apply policy1-100

DMARC Deployment Progression

Roll out DMARC gradually:

; Phase 1: Monitor only — collect reports, no enforcement
_dmarc.example.com.  IN  TXT  "v=DMARC1; p=none; rua=mailto:dmarc@example.com"

; Phase 2: Quarantine 10% — test impact
_dmarc.example.com.  IN  TXT  "v=DMARC1; p=quarantine; pct=10; rua=mailto:dmarc@example.com"

; Phase 3: Quarantine 100%
_dmarc.example.com.  IN  TXT  "v=DMARC1; p=quarantine; pct=100; rua=mailto:dmarc@example.com"

; Phase 4: Full reject
_dmarc.example.com.  IN  TXT  "v=DMARC1; p=reject; rua=mailto:dmarc@example.com; ruf=mailto:dmarc-forensic@example.com"

Implementation Patterns

DNS Configuration for a New Domain

; SPF — authorize your ESP (e.g., SendGrid) and Google Workspace
example.com.  IN  TXT  "v=spf1 include:sendgrid.net include:_spf.google.com -all"

; DKIM — add selector records (provided by your ESP)
s1._domainkey.example.com.  IN  CNAME  s1.domainkey.u12345.wl.sendgrid.net.
s2._domainkey.example.com.  IN  CNAME  s2.domainkey.u12345.wl.sendgrid.net.

; DMARC — start in monitoring mode
_dmarc.example.com.  IN  TXT  "v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com"

; Return-Path / Bounce domain (custom CNAME for ESP bounce handling)
em.example.com.  IN  CNAME  u12345.wl.sendgrid.net.

Verifying Authentication with Command Line

# Check SPF record
dig +short TXT example.com | grep spf

# Check DKIM record
dig +short TXT selector1._domainkey.example.com

# Check DMARC record
dig +short TXT _dmarc.example.com

# Send a test email and check headers
# In the received email, look for:
#   Authentication-Results: spf=pass dkim=pass dmarc=pass

Programmatic Authentication Check

import dns from "dns/promises";

async function checkEmailAuth(domain: string) {
  const results: Record<string, string> = {};

  // SPF
  try {
    const txtRecords = await dns.resolveTxt(domain);
    const spf = txtRecords.flat().find((r) => r.startsWith("v=spf1"));
    results.spf = spf || "NOT FOUND";
  } catch {
    results.spf = "DNS ERROR";
  }

  // DMARC
  try {
    const dmarcRecords = await dns.resolveTxt(`_dmarc.${domain}`);
    const dmarc = dmarcRecords.flat().find((r) => r.startsWith("v=DMARC1"));
    results.dmarc = dmarc || "NOT FOUND";
  } catch {
    results.dmarc = "DNS ERROR";
  }

  // MX
  try {
    const mxRecords = await dns.resolveMx(domain);
    results.mx = mxRecords.map((r) => `${r.priority} ${r.exchange}`).join(", ");
  } catch {
    results.mx = "DNS ERROR";
  }

  return results;
}

Bounce and Complaint Handling

// Webhook handler for ESP bounce/complaint events (e.g., SendGrid Event Webhook)
import express from "express";

const app = express();
app.use(express.json());

app.post("/webhooks/email-events", async (req, res) => {
  const events = req.body;

  for (const event of events) {
    switch (event.event) {
      case "bounce":
        if (event.type === "permanent") {
          // Hard bounce: invalid address — suppress immediately
          await suppressEmail(event.email, "hard_bounce");
          console.log(`Hard bounce: ${event.email} — ${event.reason}`);
        } else {
          // Soft bounce: temporary issue — retry, suppress after 3 attempts
          await recordSoftBounce(event.email);
          console.log(`Soft bounce: ${event.email} — ${event.reason}`);
        }
        break;

      case "spamreport":
        // Recipient marked as spam — suppress immediately
        await suppressEmail(event.email, "spam_complaint");
        console.log(`Spam complaint: ${event.email}`);
        break;

      case "unsubscribe":
        await unsubscribeEmail(event.email);
        break;

      case "dropped":
        console.log(`Dropped: ${event.email} — ${event.reason}`);
        break;
    }
  }

  res.sendStatus(200);
});

async function suppressEmail(email: string, reason: string) {
  // Add to suppression list — never send to this address again
  await db.suppressionList.upsert({
    email,
    reason,
    suppressedAt: new Date(),
  });
}

async function recordSoftBounce(email: string) {
  const record = await db.softBounces.findOne({ email });
  const count = (record?.count || 0) + 1;

  if (count >= 3) {
    await suppressEmail(email, "repeated_soft_bounce");
  } else {
    await db.softBounces.upsert({ email, count, lastBounceAt: new Date() });
  }
}

List Hygiene and Warm-Up

// IP/domain warm-up schedule for a new sending infrastructure
const warmUpSchedule = [
  { day: 1, dailyVolume: 50 },
  { day: 2, dailyVolume: 100 },
  { day: 3, dailyVolume: 250 },
  { day: 4, dailyVolume: 500 },
  { day: 5, dailyVolume: 1000 },
  { day: 6, dailyVolume: 2000 },
  { day: 7, dailyVolume: 5000 },
  { day: 8, dailyVolume: 10000 },
  { day: 9, dailyVolume: 20000 },
  { day: 10, dailyVolume: 40000 },
  // Continue doubling until full volume
];

// During warm-up, send to most engaged users first
async function getWarmUpRecipients(limit: number) {
  return await db.users
    .find({ emailVerified: true, lastOpenedAt: { $gte: thirtyDaysAgo } })
    .sort({ lastOpenedAt: -1 })
    .limit(limit);
}

BIMI (Brand Indicators for Message Identification)

BIMI displays your brand logo in supporting email clients (Gmail, Yahoo, Apple Mail):

; BIMI record — requires DMARC p=quarantine or p=reject
default._bimi.example.com.  IN  TXT  "v=BIMI1; l=https://example.com/brand/logo.svg; a=https://example.com/brand/vmc.pem"

Requirements:

  • DMARC policy must be quarantine or reject
  • Logo must be a Tiny PS SVG (specific SVG profile)
  • A Verified Mark Certificate (VMC) from DigiCert or Entrust is required for Gmail

Email Headers for Deliverability

// Set proper headers when sending
const mailOptions = {
  from: '"Company Name" <notifications@mail.example.com>',
  to: recipient,
  subject: "Your Order Confirmation",
  html: renderedHtml,
  text: plainText,
  headers: {
    // List-Unsubscribe (required by Gmail and Yahoo since Feb 2024)
    "List-Unsubscribe": "<https://example.com/unsubscribe?token=abc>, <mailto:unsubscribe@example.com?subject=unsubscribe>",
    "List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
    // Precedence header for transactional emails
    Precedence: "transactional",
    // Feedback-ID for Gmail Postmaster Tools categorization
    "Feedback-ID": "campaign123:account456:example.com",
  },
};

Best Practices

  • Implement all three authentication protocols (SPF, DKIM, DMARC) before sending any production email. They are table stakes for deliverability in 2026.
  • Start DMARC at p=none to collect reports, then gradually increase enforcement. Jumping straight to p=reject can block legitimate email you forgot to authorize.
  • Use a dedicated subdomain for transactional email (e.g., mail.example.com) and a separate one for marketing (e.g., news.example.com). This isolates reputation.
  • Include List-Unsubscribe and List-Unsubscribe-Post headers on all non-critical emails. Gmail and Yahoo require one-click unsubscribe for bulk senders since February 2024.
  • Process bounces and spam complaints immediately. Continuing to send to addresses that bounce or complain damages sender reputation.
  • Warm up new IPs and domains gradually. Start with your most engaged recipients and scale volume over 2-4 weeks.
  • Monitor your sender reputation with Google Postmaster Tools (for Gmail) and Microsoft SNDS (for Outlook).
  • Always send both HTML and plain-text versions. Missing plain-text is a spam signal for many filters.
  • Keep spam complaint rate below 0.1% (Gmail's threshold) and bounce rate below 2%.
  • Clean your list regularly. Remove addresses that have not engaged in 6-12 months.

Common Pitfalls

  • Too many SPF lookups: SPF allows a maximum of 10 DNS lookups. Each include: counts as a lookup. Exceeding 10 causes SPF to fail. Use SPF flattening tools to merge includes.
  • Missing alignment: DMARC requires that the domain in From: aligns with either the SPF return-path domain or the DKIM d= domain. A mismatch causes DMARC to fail even if SPF and DKIM individually pass.
  • Not monitoring DMARC reports: Without monitoring rua reports, you cannot detect unauthorized use of your domain or find misconfigured legitimate senders.
  • Sending from a no-reply address without proper authentication: noreply@example.com still needs SPF, DKIM, and DMARC just like any other address.
  • Ignoring the warm-up period: Sending high volume from a new IP or domain immediately triggers spam filters. ISPs trust senders who build volume gradually.
  • Purchased or scraped email lists: Sending to lists that were not organically collected results in high bounce rates, spam complaints, and potential blacklisting.
  • Missing unsubscribe mechanism: Since February 2024, Gmail and Yahoo require one-click List-Unsubscribe for senders exceeding 5,000 messages per day. Non-compliance results in filtering.
  • Using shared IPs without understanding risk: Shared sending IPs mean your reputation is influenced by other senders on the same IP. For high-volume senders, dedicated IPs provide more control.
  • Not checking blacklists: Regularly check your sending IPs against blacklists (Spamhaus, Barracuda, SORBS) using tools like MXToolbox. A blacklisted IP means rejected email.

Anti-Patterns

Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.

Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.

Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add email-template-skills

Get CLI access →