---
title: "Testable criteria for cold email software"
url: "https://lookvalid.com/blog/industry/cold-email-software-evaluation-criteria.md"
canonical_url: "https://lookvalid.com/blog/industry/cold-email-software-evaluation-criteria"
last_updated: "2026-10-22"
type: "conceptual"
summary: "Seven capabilities to test before you commit to cold email software: list validation hooks, per-domain throttling, bounce handling and RFC 8058 headers."
related: ["https://lookvalid.com/products/email-domain-check.md", "https://lookvalid.com/docs/api/email-domain-check.md", "https://lookvalid.com/docs/quickstart.md", "https://lookvalid.com/glossary/hard-bounce.md", "https://lookvalid.com/glossary/role-account.md"]
---

# Testable criteria for cold email software

Seven capabilities to test before you commit to cold email software: list validation hooks, per-domain throttling, bounce handling and RFC 8058 headers.

Feature lists for cold email software are easy to read and hard to compare. This post reduces the choice to capabilities you can test yourself in an afternoon, each tied to a published standard or sender rule. It names no products, and every criterion has a pass condition you can observe.

## Unsubscribe headers that pass a real check

RFC 8058 defines one-click unsubscribe. A message carries a `List-Unsubscribe` header with exactly one HTTPS URI and a `List-Unsubscribe-Post` header whose value is `List-Unsubscribe=One-Click`. The message must have a valid DKIM signature covering both headers, and the URI must not answer a POST with a redirect [1]. The older header syntax, angle-bracketed URLs with mailto or http, comes from RFC 2369 [2].

Google asks senders of 5,000 or more messages a day to support one-click unsubscribe on marketing mail [3]. Yahoo adds that unsubscribe requests should be honored within 2 days [4]. The test: send yourself a message from the tool, read the raw source, and confirm both headers are present and that the signature header lists them in its `h=` tag.

A header check you can run against a saved raw message

```js
import { readFileSync } from 'node:fs';

const raw = readFileSync(process.argv.at(2), 'utf8');
const headerBlock = raw.split(/\r?\n\r?\n/).at(0) ?? '';
const unfolded = headerBlock.replace(/\r?\n[ \t]+/g, ' ');
const get = (name) =>
  unfolded.split(/\r?\n/).find((line) => line.toLowerCase().startsWith(name + ':'));

const unsub = get('list-unsubscribe') ?? '';
const post = get('list-unsubscribe-post') ?? '';
const sig = get('dkim-signature') ?? '';
const signed = (sig.match(/\bh=([^;]*)/i)?.at(1) ?? '').toLowerCase();

const results = {
  httpsUri: /<https:\/\/[^>]+>/i.test(unsub),
  oneClickPost: /List-Unsubscribe=One-Click/.test(post),
  signedUnsub: signed.includes('list-unsubscribe'),
  signedPost: signed.includes('list-unsubscribe-post'),
  oneFromHeader: unfolded.split(/\r?\n/).filter((l) => /^from:/i.test(l)).length === 1,
};
console.table(results);
process.exitCode = Object.values(results).every(Boolean) ? 0 : 1;
```

The last line of the script covers a related rule in RFC 5322: the From field appears exactly once [5].

## The evaluation table

Capabilities to test, and what a pass looks like

| Capability | How to test it | Pass condition |
| --- | --- | --- |
| List validation hook | Import a file containing a null-MX domain, a disposable domain and a role mailbox | The tool can call an HTTP endpoint per address, or accept a result column, before sending |
| Per-domain throttling | Queue 200 messages to one recipient domain and watch the send log | You can set a cap per recipient domain and per sending domain, and the log shows it holding |
| Bounce handling | Send to an address you know is dead, then to a full mailbox | Permanent failures (5.x.x) suppress the address; temporary ones (4.x.x) retry [6] |
| Unsubscribe headers | Run the script above on a real message | All five checks print true |
| Suppression across campaigns | Unsubscribe in one campaign, then add the address to another | The second campaign skips it |
| Spam-rate visibility | Look for per-domain complaint data | You can see complaint rate against the 0.3 percent ceiling both Google and Yahoo publish [3][4] |
| Export and API access | Export contacts and events | Every field you need to rebuild suppression elsewhere comes out |

## Bounces, throttles and validation hooks

Enhanced status codes carry three numbers: a class, a subject and a detail. Class 4 is a persistent transient failure where a retry may work, class 5 is a permanent failure that resending will not fix, and `5.1.1` means the destination mailbox does not exist [6]. RFC 5321 also says a relay that accepts a message and later finds it undeliverable must send a non-delivery notification to the originator, so a good tool parses those later notices and not only the immediate SMTP reply [7].

Per-domain throttling has no standard to cite, so test the behavior directly. The reason to want it is the complaint ceiling: a single noisy sending domain should be pausable without stopping the rest.

A validation hook is the cheapest control. Checking a domain before import removes addresses that can never receive mail, such as a domain that publishes a null MX or has no mail host. The `email-domain-check` product returns those signals from public DNS and open lists, and it does not contact the mail server, so it cannot prove a mailbox exists.

A validation call a tool could make for each new address

```bash
curl -X POST https://YOUR_API_HOST/v1/email-domain-check \
  -H "Authorization: Bearer $LOOKVALID_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"info@example.com"}'
```

A captured response for that address reports `null_mx: true`, `role_account: true` and a `deliverability_signal` of `undeliverable`. A tool that cannot act on a field like that before sending fails the first row of the table.

## Scoring and next step

1. Run every row of the table against each candidate and record pass or fail, not a score.
2. Drop any tool that fails unsubscribe headers or bounce handling, since both affect every message.
3. Break ties on validation hooks and export access, which decide how well the tool fits the rest of your stack.

To wire address checks into whichever tool you pick, follow the [quickstart](https://lookvalid.com/docs/quickstart.md) or read the [email-domain-check reference](https://lookvalid.com/docs/api/email-domain-check.md).

## Sources

1. [RFC 8058: Signaling One-Click Functionality for List Email Headers](https://www.rfc-editor.org/rfc/rfc8058), read 2026-10-19
2. [RFC 2369: The Use of URLs as Meta-Syntax for Core Mail List Commands](https://www.rfc-editor.org/rfc/rfc2369), read 2026-10-19
3. [Google: Email sender guidelines](https://support.google.com/a/answer/81126), read 2026-10-19
4. [Yahoo Sender Hub: sender best practices](https://senders.yahooinc.com/best-practices/), read 2026-10-19
5. [RFC 5322: Internet Message Format](https://www.rfc-editor.org/rfc/rfc5322), read 2026-10-19
6. [RFC 3463: Enhanced Mail System Status Codes](https://www.rfc-editor.org/rfc/rfc3463), read 2026-10-19
7. [RFC 5321: Simple Mail Transfer Protocol](https://www.rfc-editor.org/rfc/rfc5321), read 2026-10-19

By LookValid engineering. Published 2026-10-19, updated 2026-10-19.

## Related

- [Email Domain Check API](https://lookvalid.com/products/email-domain-check.md)
- [Email Domain Check reference](https://lookvalid.com/docs/api/email-domain-check.md)
- [Quickstart](https://lookvalid.com/docs/quickstart.md)
- [What is Hard bounce?](https://lookvalid.com/glossary/hard-bounce.md)
- [What is Role account?](https://lookvalid.com/glossary/role-account.md)
