Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minutePC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11You can build this API with Node’s promise-based DNS module. Query TXT records at the domain for SPF, at _dmarc.<domain> for DMARC, and at <selector>._domainkey.<domain> for DKIM. Each TXT answer comes back as arrays of character strings, so you must join each record’s chunks before parsing. Keep DNS failures separate from missing records. The endpoint can report what a domain publishes, but it cannot prove that a real email passed SPF or DKIM.
What a DNS-only checker can and cannot prove
Before writing code, decide what the API promises. A checker that only reads DNS answers three different questions, and none of them is the same as evaluating a message:
- SPF: the domain publishes a record that begins with the
v=spf1version marker. SPF authorization is decided per sending IP address and SMTP identity, so a DNS-only result cannot say whether a particular server was allowed to send. - DKIM: a key record exists for one selector. Validating that key in DNS is not the same as validating a signature on a message, which needs the signed message and its headers.
- DMARC: a policy record exists for the domain or its organizational domain. A published policy does not tell you whether a given message aligned or passed.
The API should say this in its response, not only in documentation. Label results as published configuration, and reserve “pass” and “fail” language for an evaluator that receives the sending IP, the envelope sender, or the signed message.
One privacy point also matters for a public service. Querying SPF data sends DNS queries that reach infrastructure run for the domain owner. In RFC 7208, Section 11.6, Scott Kitterman, one of the RFC’s authors, puts it plainly: “Checking SPF records causes DNS queries to be sent to the domain owner.” Your API will generate that traffic for every domain a user submits, so it needs the rate limits described later.
Free tools Windows power users keep installed
One-click scans. No signup required.
#1 Best Overall
Where each record lives
All three checks use TXT records, but each is published at a different name. Getting the name right is the most common source of false “missing” results.
| Check | DNS name queried | What the record identifies | Input you need |
|---|---|---|---|
| SPF | <domain> (the apex) |
Hosts authorized to send for the domain | Domain only for publication checks; sending IP and identity for real SPF evaluation |
| DKIM | <selector>._domainkey.<domain> |
The public key for one signing selector | Domain and selector, usually taken from the s= tag of a DKIM-Signature header |
| DMARC | _dmarc.<domain> |
The domain’s policy and reporting settings | Domain; the organizational domain if you implement fallback |
There is no universal DKIM record at the domain level. A domain can use several selectors at once, and you only find a selector by reading it from a message or by guessing common names. A miss on a guessed selector means only that the name did not return a key, not that the domain lacks DKIM.
Reading TXT answers in Node.js
The Node.js DNS documentation for v26.3.1 describes resolveTxt() as returning a two-dimensional array. Each inner array is one TXT record, and its entries are that record’s character strings. A long DKIM key, for example, is usually split into several strings of at most 255 bytes each. Joining the chunks of one record with no separator reconstructs the value. Joining across records would merge unrelated policies into one string, which is wrong.
Here is the difference in practice. Suppose one TXT record is stored as two chunks, "v=DMARC1; p=qu" and "arantine; rua=mailto:[email protected]". The correct value is v=DMARC1; p=quarantine; rua=mailto:[email protected]. Joining it with a space would produce p=qu arantine, which fails the policy check.
Recommended Free Tools
Rank #2
Command-line tools can mislead you here. dig +short TXT prints each chunk in its own quotes, separated by spaces, so you read the same record as several quoted pieces. Use it to compare results, but parse the Node output in your code.
Map DNS errors to states, do not collapse them
A rejected promise from resolveTxt() carries a Node error code. Treat these as different outcomes:
| Node error code | Meaning | State to report |
|---|---|---|
| ENODATA | The name exists but has no TXT data | no_txt_data |
| ENOTFOUND | The name does not exist | name_not_found |
| EREFUSED | The resolver refused the query | refused |
| ETIMEOUT | No answer arrived within the resolver’s timeout and retries | timeout |
| ESERVFAIL | The resolver reported a server failure | server_failure |
The last three are not evidence that a record is absent. If the answer is a timeout, the honest response is “could not determine,” and your API should say so. Only a successful answer without a matching record supports “absent.”
The checker module
The module below uses Resolver from node:dns/promises so you can set a timeout and retry count per resolver instance. It returns raw values with every parsed finding, which lets users see what you matched. Save it as an ES module (for example check-email-dns.mjs, or use "type": "module" in package.json).
Rank #3
import { Resolver } from 'node:dns/promises';
const resolver = new Resolver({ timeout: 2000, tries: 2 });
class ValidationError extends Error {}
// ASCII host names only. Convert internationalized names to punycode before calling.
const DOMAIN_RE = /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?.)+[a-z]{2,63}$/;
const SELECTOR_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/;
const B64_RE = /^[A-Za-z0-9+/]+=*$/;
const DNS_STATES = {
ENODATA: 'no_txt_data',
ENOTFOUND: 'name_not_found',
EREFUSED: 'refused',
ETIMEOUT: 'timeout',
ESERVFAIL: 'server_failure',
};
function normalizeDomain(input) {
const domain = String(input ?? '').trim().toLowerCase().replace(/.$/, '');
if (!DOMAIN_RE.test(domain)) throw new ValidationError('domain is not a valid ASCII host name');
return domain;
}
function normalizeSelector(input) {
const selector = String(input ?? '').trim().toLowerCase();
if (!SELECTOR_RE.test(selector)) throw new ValidationError('selector is not a valid DNS label sequence');
return selector;
}
// Returns raw values and a state. Never throws for DNS conditions.
async function lookupTxt(name) {
try {
const answers = await resolver.resolveTxt(name);
// Each answer is one TXT record made of character-string chunks.
return { state: 'answer', name, records: answers.map((chunks) => chunks.join('')) };
} catch (err) {
return { state: DNS_STATES[err.code] ?? 'lookup_failed', name, records: [], code: err.code ?? null };
}
}
function parseTags(record) {
const tags = {};
for (const part of record.split(';')) {
const eq = part.indexOf('=');
if (eq === -1) continue;
tags[part.slice(0, eq).trim().toLowerCase()] = part.slice(eq + 1).trim();
}
return tags;
}
const SPF_VERSION = /^v=spf1(s|$)/i;
function checkSpf(lookup) {
if (lookup.state !== 'answer') return { state: lookup.state, code: lookup.code };
const matches = lookup.records.filter((r) => SPF_VERSION.test(r.trim()));
if (matches.length === 0) return { state: 'absent' };
if (matches.length > 1) return { state: 'multiple_records', raw: matches };
const terms = matches[0].trim().split(/s+/).slice(1);
const all = terms.find((t) => /^[+-?~]?all$/i.test(t)) ?? null;
return { state: 'published', raw: matches[0], allMechanism: all, termCount: terms.length };
}
async function checkDkim(domain, selector) {
const name = `${selector}._domainkey.${domain}`;
const lookup = await lookupTxt(name);
if (lookup.state !== 'answer') return { name, state: lookup.state, code: lookup.code };
const candidates = lookup.records.filter((r) => {
const tags = parseTags(r);
return 'v' in tags || 'p' in tags;
});
if (candidates.length === 0) return { name, state: 'absent' };
if (candidates.length > 1) return { name, state: 'multiple_records', raw: candidates };
const tags = parseTags(candidates[0]);
if (!('p' in tags)) return { name, state: 'no_key_tag', raw: candidates[0] };
if (tags.p === '') return { name, state: 'revoked', raw: candidates[0] };
if (!B64_RE.test(tags.p)) return { name, state: 'malformed_key', raw: candidates[0] };
// RFC 6376 defaults the key type to rsa when k= is absent.
return { name, state: 'key_published', keyType: tags.k ?? 'rsa', raw: candidates[0] };
}
const DMARC_VERSION = /^v=DMARC1s*(;|$)/i;
const NO_RECORD_STATES = new Set(['absent', 'no_txt_data', 'name_not_found']);
async function checkDmarcAt(domain) {
const lookup = await lookupTxt(`_dmarc.${domain}`);
if (lookup.state !== 'answer') return { name: lookup.name, state: lookup.state, code: lookup.code };
const matches = lookup.records.filter((r) => DMARC_VERSION.test(r.trim()));
if (matches.length === 0) return { name: lookup.name, state: 'absent' };
if (matches.length > 1) return { name: lookup.name, state: 'multiple_records', raw: matches };
const tags = parseTags(matches[0]);
const policy = tags.p?.toLowerCase();
if (!['none', 'quarantine', 'reject'].includes(policy)) {
return { name: lookup.name, state: 'invalid_policy', raw: matches[0] };
}
return { name: lookup.name, state: 'published', policy, subdomainPolicy: tags.sp ?? null, raw: matches[0] };
}
async function checkDmarc(domain, organizationalDomain) {
const own = await checkDmarcAt(domain);
if (!NO_RECORD_STATES.has(own.state) || !organizationalDomain || organizationalDomain === domain) {
return { ...own, source: domain, fallback: false };
}
const org = await checkDmarcAt(organizationalDomain);
return { ...org, source: organizationalDomain, fallback: true };
}
export async function checkEmailAuthDns(domainInput, { selector, organizationalDomain } = {}) {
const domain = normalizeDomain(domainInput);
const sel = selector ? normalizeSelector(selector) : null;
const orgDomain = organizationalDomain ? normalizeDomain(organizationalDomain) : null;
const [spfLookup, dkim, dmarc] = await Promise.all([
lookupTxt(domain),
sel ? checkDkim(domain, sel) : Promise.resolve({ state: 'selector_required' }),
checkDmarc(domain, orgDomain),
]);
return {
domain,
checkedAt: new Date().toISOString(),
scope: 'dns_publication_only',
spf: checkSpf(spfLookup),
dkim,
dmarc,
};
}
The organizationalDomain option is deliberate. Deriving the organizational domain requires a Public Suffix List, because a naive “last two labels” rule gets country-code and multi-label suffixes wrong. Compute it with a maintained PSL library in your application and pass it in.
Interpreting the results
Each check returns a state. The table below lists what each state means and how to present it. Show the raw value alongside every state so users can compare it with their own DNS.
| Check | State | Meaning |
|---|---|---|
| SPF | published |
One record begins with v=spf1. Check allMechanism: a missing all term is a configuration finding, not a DNS error. |
| SPF | multiple_records |
More than one SPF record exists at the apex. RFC 7208 treats this as an error, so a receiver may return a permanent error. |
| DKIM | key_published |
A parseable key tag exists for the selector. The key has not been used to verify any message. |
| DKIM | revoked |
The record has an empty p= tag, which RFC 6376 uses to indicate a revoked key. |
| DKIM | malformed_key |
The p= value contains characters outside base64. Check for a chunk-joining error before reporting this to the user. |
| DMARC | published |
A valid v=DMARC1 record with a p= value of none, quarantine, or reject. The source field shows whether it came from fallback. |
| Any | timeout, server_failure, refused |
Undetermined. Do not report as absent. |
For DMARC, the discovery procedure is defined in the current standard, RFC 9989 (2026), which supersedes RFC 7489. Consult RFC 9989 for current discovery rules, and check the errata before you depend on edge cases. The older text is still available at RFC 7489. The module’s fallback only queries the organizational domain after the record at the domain name is absent. If a lookup fails with a timeout, it does not fall back.
For SPF, the record is the input to evaluation rather than the evaluation itself. RFC 7208 limits the number of DNS-querying mechanisms an evaluator may follow, and a full SPF result requires walking include, redirect, a, and mx terms with the sending IP. The checker should list those terms as data and leave evaluation to a separate component that receives the IP address and sender.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Fix the driver behind crashes, sound loss and screen glitches3Repair Windows errors before they cause bigger problemsRank #4
Expose the checker over HTTP
The server below is intentionally small. It accepts one GET route, returns JSON, and maps validation errors to 400. Save it next to the module as server.mjs and import checkEmailAuthDns from the module above, adding export is already done. Add the import line at the top:
import { createServer } from 'node:http';
import { checkEmailAuthDns, ValidationError } from './check-email-dns.mjs';
const server = createServer(async (req, res) => {
const url = new URL(req.url ?? '/', 'http://localhost');
if (req.method !== 'GET' || url.pathname !== '/v1/email-auth') {
res.writeHead(404, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: 'not_found' }));
return;
}
try {
const result = await checkEmailAuthDns(url.searchParams.get('domain'), {
selector: url.searchParams.get('selector') ?? undefined,
});
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify(result));
} catch (err) {
const isInput = err instanceof ValidationError;
res.writeHead(isInput ? 400 : 500, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: isInput ? 'invalid_request' : 'internal_error', message: isInput ? err.message : undefined }));
}
});
server.listen(3000);
This requires one small change to the module: add export to the ValidationError class declaration so the server can import it. Then start the server with node server.mjs and query it:
curl "http://localhost:3000/v1/email-auth?domain=example.com&selector=default"
To compare the same records outside your code, run these commands and check that the chunks match what your API returns:
dig +short TXT example.com
dig +short TXT _dmarc.example.com
dig +short TXT default._domainkey.example.com
Hardening a public endpoint
Every request can trigger several DNS queries, so a public deployment needs controls before launch:
- Validate before querying. The module rejects malformed domains and selectors. Keep it that way: never pass raw query strings to the resolver.
- Limit query volume. Apply a per-client rate limit and a separate limit per queried domain, so one popular or hostile domain cannot consume the resolver budget. The privacy point above applies here: repeated checks of one domain send repeated queries to that owner’s infrastructure.
- Cache with a fixed lifetime. The
resolveTxt()result shape in the Node.js documentation carries no TTL, so choose a short fixed cache period, such as a few minutes, and label cached responses with theircheckedAttime. - Bound selector discovery. Do not expose an endpoint that loops over a list of selectors per request. One selector per call keeps the cost predictable.
- Tune the resolver. The
timeoutandtriesvalues in the module are starting points. Measure your resolver’s behavior under load before choosing final values.
Add an authentication layer or an API gateway if the service is not meant to be open. The checker itself does not decide who may call it.
Where to go from here
Once the publication checker works, the next step is a separate evaluator for SPF that accepts a sending IP address, the HELO identity, and the MAIL FROM address. For DKIM, a verifier needs the full signed message and must canonicalize the headers and body exactly as the signature specifies. RFC 6376 at RFC 6376 defines those rules, and RFC 7208 at RFC 7208 defines SPF evaluation. Keep those components separate from the DNS checker so each response states exactly what was established.
The DNS module in this article is a sound foundation for the first two checks in the list above. Its results are accurate to what is published, and that is the boundary the API should state to its users.
Source references for the Node.js behavior used here are in the Node.js DNS documentation for v26.3.1. Check the same page for your own Node release before deploying, because resolver options and error codes can change between versions.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




