What SPF and DMARC records tell you about a signup domain
· Updated · 7 min read · Email validation
SPF and DMARC records tell you whether a domain's owner has configured authentication for mail sent in the domain's name, and how strict they want receivers to be about it. To check them, query the domain's TXT records for one starting with v=spf1, and the TXT record at _dmarc.<domain> for one starting with v=DMARC1. For signup screening they are a weak legitimacy hint at best: their presence shows someone set up DNS with care, and their absence is common among real small businesses.
This post shows how to read them, what they can and can't say about the person signing up, and how to fold them in next to a domain classification.
What the two records are for
Both records describe sending. They're how a receiving mail server decides whether a message that claims to come from example.com really does.
SPF (RFC 7208) lists the hosts allowed to send mail for a domain. It lives in a TXT record on the domain itself and must begin with v=spf1. Mechanisms like include:, ip4: and mx name the allowed senders, and the final all mechanism sets the default. Its qualifier matters: -all means fail, ~all softfail, ?all neutral, and a bare all (or +all) passes everything. The RFC also says a domain may only publish one SPF record; more than one is a permerror. And evaluation may trigger at most 10 DNS-querying terms, another common cause of permerror.
DMARC sits on top. It says what a receiver should do with mail that fails SPF and DKIM alignment, and where to send reports. The record lives at _dmarc.<domain>, starts with v=DMARC1, and its p tag is none, quarantine or reject. DMARC was first published as RFC 7489, an Informational document. As of 2026 it has a Standards Track successor, RFC 9989, which obsoletes RFC 7489. Records written for the old spec keep working (the version is still DMARC1), but a few details changed:
- The
pcttag was removed and replaced byt=y/t=n(test mode). You'll still seepct=100in the wild; unknown tags are ignored. - A new
nptag sets policy for non-existent subdomains, andpsdmarks public suffix domains. - Policy discovery uses a DNS tree walk up the domain's labels instead of the Public Suffix List.
None of this is about the mailbox of the person in front of you. SPF and DMARC are statements by the domain owner about outbound mail.
What they can tell you about a signup
Here is the honest map, for a domain that classifies as a company domain:
| What you find | What it suggests |
|---|---|
Valid SPF and DMARC at p=quarantine or p=reject |
Someone has set up mail authentication with care. Common at established companies, and easy for anyone to copy. |
Valid SPF, DMARC at p=none |
Monitoring mode. Very common, including at large providers. |
| SPF only, no DMARC | Typical for small businesses on hosted mail. Neutral. |
| Neither record | Owner hasn't configured outbound authentication. Common for personal domains and small firms, also common for domains bought only to receive confirmation emails. |
SPF is exactly v=spf1 -all |
Declares that the domain sends no mail at all. Normal for parked and receive-only domains. |
| Two SPF records, or a syntax error | Misconfiguration. Says more about the admin than about fraud. |
The two examples worth remembering, both checked in 2026:
mailinator.com, a disposable inbox service, publishesv=spf1 -alland a DMARC record withp=reject. A strict policy. A receive-only service has every reason to lock down outbound spoofing, and strictness tells you nothing about whether the address is disposable.gmail.compublishes DMARC withp=none(andsp=quarantinefor subdomains). The largest webmail domain is in monitoring mode at the top level.
So DMARC strength is a poor proxy for "real business". That's why classification has to come first: a blocklist check catches Mailinator no matter what its DNS says, and a free-provider list puts Gmail in the consumer bucket.
Where SPF and DMARC help is the long tail of unknown company domains. A domain with no web presence, no SPF, no DMARC and a registration date from last week looks like a mail-catching domain. A domain with a full SPF include chain for a hosted mail provider looks like someone's working office mail. Neither is proof. Both nudge a score.
Looking them up over DNS-over-HTTPS
You don't need a DNS library. A DoH JSON endpoint works from any runtime with fetch, including edge functions. The mechanics, including the Status codes, are in DNS-over-HTTPS for domain checks.
One detail bites people with TXT records: providers format data differently. Cloudflare returns the value wrapped in quotes, and long records arrive as several quoted chunks ("v=spf1 include:a" " include:b ~all"). Google returns it unquoted. Normalize before parsing.
type DohAnswer = { type: number; data: string };
type DohResponse = { Status: number; Answer?: DohAnswer[] };
const TXT = 16;
async function txt(name: string): Promise<string[] | null> {
const url = `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(name)}&type=TXT`;
try {
const res = await fetch(url, {
headers: { accept: 'application/dns-json' },
signal: AbortSignal.timeout(3000),
});
if (!res.ok) return null;
const body = (await res.json()) as DohResponse;
if (body.Status === 3) return []; // NXDOMAIN: no such name
if (body.Status !== 0) return null; // SERVFAIL etc.: unknown
return (body.Answer ?? [])
.filter((a) => a.type === TXT)
.map((a) => joinTxt(a.data));
} catch {
return null;
}
}
// '"v=spf1 include:a" " -all"' -> 'v=spf1 include:a -all'; unquoted data passes through.
function joinTxt(data: string): string {
const parts = data.match(/"((?:[^"\\]|\\.)*)"/g);
return parts ? parts.map((p) => p.slice(1, -1).replace(/\\(.)/g, '$1')).join('') : data;
}
export type MailAuth = {
spf: 'none' | 'valid' | 'multiple' | 'unknown';
spfAll: '-all' | '~all' | '?all' | '+all' | null;
dmarc: 'none' | 'quarantine' | 'reject' | 'missing' | 'unknown';
};
export async function mailAuth(domain: string): Promise<MailAuth> {
const [root, dmarcTxt] = await Promise.all([txt(domain), txt(`_dmarc.${domain}`)]);
let spf: MailAuth['spf'] = 'unknown';
let spfAll: MailAuth['spfAll'] = null;
if (root) {
const records = root.filter((r) => /^v=spf1(\s|$)/i.test(r.trim()));
spf = records.length === 0 ? 'none' : records.length > 1 ? 'multiple' : 'valid';
if (records.length === 1) {
const m = records[0].match(/(?:^|\s)([-~?+]?)all(?:\s|$)/i);
if (m) spfAll = (`${m[1] || '+'}all`) as MailAuth['spfAll'];
}
}
let dmarc: MailAuth['dmarc'] = 'unknown';
if (dmarcTxt) {
const rec = dmarcTxt.find((r) => /^v=DMARC1\s*(;|$)/i.test(r.trim()));
if (!rec) dmarc = 'missing';
else {
const p = rec.match(/(?:^|;)\s*p\s*=\s*(none|quarantine|reject)\s*(?:;|$)/i)?.[1]?.toLowerCase();
dmarc = (p as MailAuth['dmarc']) ?? 'none'; // RFC 9989: no valid p tag is treated as p=none
}
}
return { spf, spfAll, dmarc };
}
Two simplifications to know about. First, this checks _dmarc on the domain you pass, not the full RFC 9989 tree walk. If you pass a registrable domain (which is what you want for signups), that covers the organizational record. Second, it doesn't evaluate SPF against an IP, follow include: chains or count lookups. You're not authenticating a message here; you're reading a configuration hint.
Combining it with a domain lookup
pingutil does not check SPF or DMARC. It classifies the domain, scores the local part and returns a verdict, and with check_dns: true it adds domain_signals (whether the domain exists, can receive mail, and has a website). Use that to decide whether the TXT lookups are worth making.
import { mailAuth } from './mail-auth';
export async function legitimacyHints(email: string) {
const res = await fetch('https://pingutil.com/v1/lookup', {
method: 'POST',
headers: {
authorization: `Bearer ${process.env.PINGUTIL_KEY}`,
'content-type': 'application/json',
},
body: JSON.stringify({ input: email, skip_enrichment: true, check_dns: true }),
});
const data = await res.json();
if (data.verdict.is_spam) return { decision: 'block', reason: data.verdict.reason };
if (data.category !== 'company') return { decision: 'skip', reason: data.category };
const dns = data.domain_signals;
if (dns && !dns.domain_exists) return { decision: 'reject_typo', reason: 'domain_missing' };
const auth = await mailAuth(data.domain);
let points = 0;
if (auth.spf === 'valid') points += 1;
if (auth.dmarc === 'quarantine' || auth.dmarc === 'reject') points += 1;
if (dns && !dns.has_website) points -= 1;
return { decision: 'score', points, auth, dns };
}
The point values are illustrative. The idea is that mail authentication adds a little confidence to an unknown company domain, and missing records subtract almost nothing on their own. Put the result into a broader email risk score next to domain age, website presence and local-part signals, rather than acting on it directly.
What not to do with SPF and DMARC
- Don't reject signups for missing records. You'll turn away real customers on small hosted domains, and you'll gain nothing against disposable services that already publish strict policies.
- Don't treat
p=rejectas a trust badge. Copying a strict DMARC record onto a fresh domain takes a minute. - Don't confuse these with deliverability to the user. Whether a domain can receive your confirmation email is an MX question. Checking MX records covers that, including the null MX case.
- Don't fail closed on DNS errors. A
SERVFAILor timeout is "unknown". The code above returnsunknownfor those and the scoring ignores it.
If you're scoring B2B leads, the combination that carries real weight is a company domain that exists, has a website, has a mail setup someone maintains, and isn't brand new. SPF and DMARC are one quiet line in that picture. To see the classification side for any domain, run it through the live demo.
Questions people ask
- How do I check a domain's DMARC record?
- Query the TXT records at _dmarc followed by the domain, for example _dmarc.example.com. A valid record starts with v=DMARC1 and its p tag says none, quarantine or reject. Any DNS tool or a DNS-over-HTTPS JSON endpoint will return it.
- Does a missing DMARC record mean an email address is fake?
- No. Plenty of small businesses and personal domains never set up DMARC, and the mailbox works fine. It only means the owner hasn't published a policy for mail sent in the domain's name.
- What does v=spf1 -all mean?
- It says no host is authorized to send mail for the domain, so receivers should treat any message claiming to come from it as failing SPF. Domains that never send mail publish it to protect against spoofing. Receive-only services, including some disposable email providers, use it too.