How to check MTA-STS
Two public documents, and both have to answer. Read the TXT record at _mta-sts.<domain>, then fetch https://mta-sts.<domain>/.well-known/mta-sts.txt over HTTPS and read the mode line. A domain has working MTA-STS only when both succeed, because the record announces a policy and the file is the policy.
Why check this
Run this after a mail platform migration, an MX change, or a certificate renewal on the mail hosts. MTA-STS tells other senders to refuse delivery to your MX unless it offers STARTTLS with a valid certificate for a name the policy lists. The failure it prevents is one-sided and silent: your MX set changes, the policy still names the old hosts, and every sender in enforce mode stops delivering to you while your own test sends keep working.
The check has an asymmetry worth stating. The TXT record is cheap to publish and is what most tools look at. The policy file needs a host, a certificate and a web server, and it is where the check usually fails. Step 4 shows a domain in exactly that state.
Prerequisites
- RFC 8461 for the record and policy grammar. Section 3.2 covers
max_age, section 3.3 covers fetching. - curl 7.0 or later. The fetch is a plain HTTPS GET, and redirects must not be followed.
- Node 22 for step 3. Save this as
mta-sts.js:
const { Resolver } = require('dns');
const https = require('https');
const r = new Resolver();
r.setServers(['8.8.8.8']); // Node's default resolver is not always usable
const domain = process.argv[2];
const txt = (n) => new Promise((res) => r.resolveTxt(n, (e, x) => res(e ? e.code : x[0].join(''))));
const mx = (n) => new Promise((res) => r.resolveMx(n, (e, x) => res(e ? [] : x)));
function fetchPolicy(host) {
return new Promise((res) => {
const req = https.get({ host, path: '/.well-known/mta-sts.txt', servername: host, timeout: 10000 },
(r2) => { let b = ''; r2.on('data', (d) => (b += d)); r2.on('end', () => res({ status: r2.statusCode, type: r2.headers['content-type'], body: b })); });
req.on('error', (e) => res({ error: e.code }));
req.on('timeout', () => { req.destroy(); res({ error: 'ETIMEDOUT' }); });
});
}
const covers = (pattern, host) => pattern.startsWith('*.')
? host.toLowerCase().split('.').slice(1).join('.') === pattern.slice(2).toLowerCase()
: host.toLowerCase() === pattern.toLowerCase();
(async () => {
const rec = await txt('_mta-sts.' + domain);
console.log('_mta-sts TXT : ' + rec);
if (!String(rec).includes('STSv1')) return console.log('policy : not announced, nothing to fetch');
const p = await fetchPolicy('mta-sts.' + domain);
if (p.error) return console.log('policy fetch : FAILED, ' + p.error + '\n RFC 8461 section 3.3: the sender delivers as if the domain had no policy');
console.log('policy status : ' + p.status + ' content-type ' + p.type);
const fields = {};
const mxPatterns = [];
for (const line of p.body.split(/\r?\n/)) {
const m = /^\s*([a-z_]+)\s*:\s*(.+?)\s*$/.exec(line);
if (!m) continue;
if (m[1] === 'mx') mxPatterns.push(m[2]); else fields[m[1]] = m[2];
}
console.log('version : ' + fields.version);
console.log('mode : ' + fields.mode);
console.log('max_age : ' + fields.max_age + ' s (' + Math.round(fields.max_age / 86400) + ' d)');
console.log('mx patterns : ' + mxPatterns.join(', '));
const hosts = (await mx(domain)).map((x) => x.exchange);
console.log('published MX : ' + (hosts.join(', ') || 'none'));
for (const h of hosts) {
const hit = mxPatterns.find((pat) => covers(pat, h));
console.log(' ' + h.padEnd(24) + (hit ? 'covered by ' + hit : 'NOT COVERED, delivery to it fails under enforce'));
}
})();
Steps
- Step 1.
Read the announcement record.
nslookup -type=TXT _mta-sts.google.com 8.8.8.8Non-authoritative answer: Server: dns.google Address: 8.8.8.8 _mta-sts.google.com text = "v=STSv1; id=20210803T010101;"The record carries no policy.
idis an opaque string that a sender compares with the id it cached, and refetches the file when the two differ. - Step 2.
Fetch the policy file itself.
curl -s https://mta-sts.google.com/.well-known/mta-sts.txtversion: STSv1 mode: enforce mx: smtp.google.com mx: aspmx.l.google.com mx: *.aspmx.l.google.com max_age: 86400mode: enforceis the answer to the question the check asks.testingreports failures and still delivers,nonewithdraws the policy. - Step 3.
Compare the policy against the MX hosts the domain actually publishes.
node mta-sts.js google.com_mta-sts TXT : v=STSv1; id=20210803T010101; policy status : 200 content-type text/plain version : STSv1 mode : enforce max_age : 86400 s (1 d) mx patterns : smtp.google.com, aspmx.l.google.com, *.aspmx.l.google.com published MX : smtp.google.com smtp.google.com covered by smtp.google.comEvery published MX must match a pattern. The policy may list more names than the MX set holds, and two of the three here are not currently in use.
- Step 4.
Run the same check on a second domain that announces a policy.
node mta-sts.js cloudflare.com_mta-sts TXT : v=STSv1;id=1769609691387; policy fetch : FAILED, ENOTFOUND RFC 8461 section 3.3: the sender delivers as if the domain had no policyThe TXT record is correct and current. The policy cannot be fetched, so no sender is holding this domain to anything.
- Step 5.
Find out why the fetch failed by resolving the policy host.
nslookup mta-sts.cloudflare.com 8.8.8.8Server: dns.google Address: 8.8.8.8 Name: mta-sts.cloudflare.comA name and no address. The same answer came back from 1.1.1.1 and 9.9.9.9 on 2026-09-11, so it is the zone and not one resolver.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| mode: enforce and every MX covered | The policy is live and correct | Nothing. Re-check after any MX change. |
| mode: testing | Failures are reported, mail still flows | Correct during rollout. Move to enforce once reports are clean. |
| mode: none | The policy is withdrawn | Senders drop any cached policy. Use it to retire MTA-STS deliberately. |
| TXT present, policy fetch fails | The domain looks protected and is not | Publish the host, the certificate and the file, or remove the TXT record. |
| An MX marked NOT COVERED | Senders in enforce mode refuse that host | Add the name to the policy before it goes into the MX set. |
| content-type other than text/plain | RFC 8461 3.3 tells senders to validate the media type | Serve it as text/plain, not text/html. |
| id unchanged after a policy edit | Senders keep serving the cached policy | Change id on every edit. It is the only cache signal. |
Thresholds
Common mistakes
What to check next
- How to check MX records: the host list a policy has to cover, read from the same zone.
- Test SMTP with STARTTLS: the transport security the policy is there to require.
- How to check DMARC record: the other policy record on a mail domain, and the one senders read first.
- How to check TXT record of a domain: the general form of the lookup in step 1.
- Email testing checklist: where both reads belong in a release that touches mail.
FAQ
What does an MTA-STS checker actually check?
Two things: the TXT record at _mta-sts.<domain>, and the file at https://mta-sts.<domain>/.well-known/mta-sts.txt. Any tool that reports only the first one will call a broken deployment healthy, as step 4 shows.
Does MTA-STS protect mail I send?
No. It protects mail sent to you, by telling other senders to require TLS on your MX. Your own outbound behaviour depends on what your sending MTA does with other domains' policies.
Where do failure reports go?
To the address in the rua= tag of the TLS-RPT record at _smtp._tls.<domain>, which is a separate record from MTA-STS. _smtp._tls.google.com publishes v=TLSRPTv1;rua=mailto:sts-reports@google.com.
Is DNSSEC required for MTA-STS?
No. MTA-STS was designed to work without it, which is why the policy is fetched over HTTPS: the certificate on mta-sts.<domain> is what authenticates the policy.
How long before a policy change takes effect?
Up to max_age seconds for senders holding a cached copy, counted from when they fetched it. Lowering max_age before a planned MX change shortens that window.
Verified
Verified by Maks Vernycurl 8.21.0nslookup Windows 10.0.22631node 22.23.2
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
basic6 minpublished updated Maks Verny