What to check in email headers: Reply-To, Message-ID, unsubscribe

Updated October 17, 2026 · Zyntra

Check the headers a template change can silently break: From and Reply-To (where replies go), a valid Message-ID, and on marketing or subscribed mail List-Unsubscribe with List-Unsubscribe-Post: List-Unsubscribe=One-Click, which Gmail has required from bulk senders since February 2024. Then send the one-click POST yourself and check that it unsubscribes.

Which headers, and why

  • From and Reply-To. They decide who the user sees and where a reply goes. Both usually come from per-environment config, which is exactly what a template test never reads.
  • Message-ID. Google's sender guidelines ask that every message include a valid Message-ID. Assert it, so a sending path that leaves it out is caught.
  • List-Unsubscribe and List-Unsubscribe-Post. Since February 1, 2024, Gmail requires marketing and subscribed messages from senders of more than 5,000 messages a day to Gmail to support one-click unsubscribe with these two headers. RFC 8058 says the List-Unsubscribe header must contain one HTTPS URI.
  • The DKIM signature covers them. RFC 8058 also requires a valid DKIM signature that covers both list headers. The signature's h= tag lists the headers it signs.

What the headers look like when you read them

Inbox APIs usually return headers parsed, not as raw text. Zyntra parses them with mailparser: names are lowercase, addresses become objects, and all List-* headers are merged into one list object. For a newsletter with one-click unsubscribe, the headers field looks like this (shortened):

headers
{
  "from": { "value": [{ "address": "[email protected]", "name": "Acme" }], "text": "\"Acme\" <[email protected]>" },
  "reply-to": { "value": [{ "address": "[email protected]", "name": "" }], "text": "[email protected]" },
  "message-id": "<[email protected]>",
  "list": {
    "unsubscribe": { "url": "https://acme.example/u/xyz", "mail": "[email protected]?subject=unsub" },
    "unsubscribe-post": { "name": "List-Unsubscribe=One-Click" }
  },
  "dkim-signature": {
    "value": "v=1",
    "params": { "d": "acme.example", "s": "s1", "h": "from:to:subject:list-unsubscribe:list-unsubscribe-post" }
  }
}

When an email has two DKIM signatures (yours and your provider's), dkim-signature is an array.

The test

newsletter-headers.spec.ts
import { test, expect } from "@playwright/test";

type Dkim = { params: { d: string; h: string } };

test("newsletter has one-click unsubscribe", async () => {
  test.setTimeout(90_000);
  const address = `${process.env.ZYNTRA_TEAM_ID}.news-${Date.now()}@zyntramail.com`;
  // Your own helper, e.g. a staging-only endpoint that sends one issue to this address.
  // A fresh address means the newest email is the newsletter, nothing older.
  await sendNewsletterTo(address);

  const res = await fetch(
    `https://api.zyntra.app/emails/last?email=${encodeURIComponent(address)}`,
    {
      headers: { "X-API-Key": process.env.ZYNTRA_API_KEY! },
      signal: AbortSignal.timeout(45_000),
    },
  );
  const { headers: h } = await res.json();

  expect(h["reply-to"]?.value[0].address).toBe("[email protected]");
  expect(h["message-id"]).toMatch(/^<[^@\s]+@[^>\s]+>$/);

  const unsubscribe: string = h.list?.unsubscribe?.url;
  expect(unsubscribe).toMatch(/^https:\/\//);
  expect(h.list?.["unsubscribe-post"]?.name).toBe("List-Unsubscribe=One-Click");

  // The signature from your own domain must cover both list headers.
  const sigs: Dkim[] = [].concat(h["dkim-signature"] ?? []);
  const own = sigs.find((s) => s.params.d === "acme.example");
  const signed = own?.params.h.toLowerCase().split(/\s*:\s*/) ?? [];
  expect(signed).toEqual(expect.arrayContaining(["list-unsubscribe", "list-unsubscribe-post"]));
});

Send the one-click POST yourself

RFC 8058 describes the request a mailbox provider sends: a POST to the HTTPS URI with List-Unsubscribe=One-Click in the body, with no cookies or authorization, and the sender must not answer with a redirect. Test your endpoint the same way:

one-click.ts
const form = new FormData();
form.append("List-Unsubscribe", "One-Click");

const res = await fetch(unsubscribe, { method: "POST", body: form, redirect: "manual" });
expect(res.status).toBe(200); // not 3xx, not a login page
// Then check in your app (or API) that the address is unsubscribed.

What header checks don't prove

The test reads the h= list but does not verify the signature itself or your DNS. For that, paste the raw headers into the email header analyzer or check the domain with the SPF, DKIM and DMARC checker; the staging deliverability post walks through the results.

Questions

Do password-reset and sign-up emails need List-Unsubscribe?
Google's requirement names marketing and subscribed messages. Transactional emails such as a password reset are not subscriptions; check your provider's guidance before adding an unsubscribe link to them.
Should automatic emails carry Auto-Submitted?
RFC 3834 defines Auto-Submitted: auto-generated for messages a program sends on its own, so that auto-responders don't answer them. If you set it, assert it like any other header.