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

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

  1. Step 1.

    Read the announcement record.

    nslookup -type=TXT _mta-sts.google.com 8.8.8.8
    
    Non-authoritative answer:
    Server:  dns.google
    Address:  8.8.8.8
    
    _mta-sts.google.com	text =
    
    "v=STSv1; id=20210803T010101;"

    The record carries no policy. id is an opaque string that a sender compares with the id it cached, and refetches the file when the two differ.

  2. Step 2.

    Fetch the policy file itself.

    curl -s https://mta-sts.google.com/.well-known/mta-sts.txt
    
    version: STSv1
    mode: enforce
    mx: smtp.google.com
    mx: aspmx.l.google.com
    mx: *.aspmx.l.google.com
    max_age: 86400

    mode: enforce is the answer to the question the check asks. testing reports failures and still delivers, none withdraws the policy.

  3. 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.com

    Every 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.

  4. 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 policy

    The TXT record is correct and current. The policy cannot be fetched, so no sender is holding this domain to anything.

  5. Step 5.

    Find out why the fetch failed by resolving the policy host.

    nslookup mta-sts.cloudflare.com 8.8.8.8
    
    Server:  dns.google
    Address:  8.8.8.8
    
    Name:    mta-sts.cloudflare.com

    A 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

google.com publishes max_age 86400 s, one day, where the RFC expects weeks Source: RFC 8461 section 3.2: max_age is a plaintext non-negative integer of seconds with a maximum value of 31557600, and it is expected that this value typically be in the range of weeks or greater.

Common mistakes

Sign: A checker reports MTA-STS as enabled and no sender ever enforces it.Cause: The tool read the TXT record and stopped. cloudflare.com publishes v=STSv1 with a current id on 2026-09-11, and mta-sts.cloudflare.com has no address record at 8.8.8.8, 1.1.1.1 or 9.9.9.9. RFC 8461 section 3.3 says a sender that cannot fetch the policy delivers as though the domain had none.
Sign: The policy file is edited and senders keep applying the old one.Cause: Caching is driven by the id in the TXT record, not by HTTP. A sender that already holds a policy refetches only when the id changes, and it keeps the cached one for max_age seconds regardless. Editing the file without bumping the id changes nothing for anybody.
Sign: Mail to the domain stops after an MX is added, and nothing in the MX records is wrong.Cause: The new host is not in the policy. Under enforce, a sender refuses any MX the policy does not name, so the policy has to be updated before the MX record is, not after. The id has to change with it.
Sign: The policy host serves the file over a redirect and senders reject it.Cause: RFC 8461 section 3.3 forbids following 3xx redirects when fetching a policy. A host that redirects HTTP to HTTPS is fine, because the fetch is HTTPS from the start, but an HTTPS redirect from the policy host to a CDN path breaks the fetch for every sender.

What to check next

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.

basic6 minpublished updated Maks Verny