MTA-STS Policy Publication Drift and Enforce-Mode Readiness
Pricing
from $2.00 / 1,000 run starteds
MTA-STS Policy Publication Drift and Enforce-Mode Readiness
Give it your domains. It reads _mta-sts and _smtp._tls TXT, the live MX set and the MTA-STS policy file, compares each one with the snapshot of the last run, parses your TLS-RPT report files, and tells you if the domain can move to enforce mode. It finds
Pricing
from $2.00 / 1,000 run starteds
Rating
0.0
(0)
Developer
kingii98
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
6 days ago
Last modified
Categories
Share
MTA-STS Policy Publication Drift and Enforce-Mode Readiness Gate
You publish MTA-STS in testing mode. You must move to enforce mode without a
sender that fails delivery, and you must never change the policy file without a
change of the _mta-sts TXT id, because every sender keeps the old cached policy
until max_age runs out.
This Actor checks, for each of your domains:
- the
_mta-stsTXT record (how many, and theidtag), - the policy file at
https://mta-sts.<domain>/.well-known/mta-sts.txt(HTTP status, content type, redirect, certificate name, SHA-256), - the policy body (version, mode,
max_age,mxpatterns), - the live MX set against the
mxpatterns, - the
_smtp._tlsTXT record (the TLS-RPTruavalues), - your TLS-RPT report files (RFC 8460 JSON), grouped into failure groups,
- and everything above against the snapshot that the last run wrote.
It then gives one readiness verdict for each domain: ready, not_ready or
overdue.
The Actor uses DNS over HTTPS and one HTTPS GET for each domain. It opens no SMTP connection, because outbound port 25 is blocked on most cloud hosts. It follows no redirect, as RFC 8461 section 3.3 requires. It refuses a report URL that resolves to a private or reserved address.
What you put in
| Field | What it holds |
|---|---|
domains | 1 to 200 mail domains. |
tlsrpt_report_files | Optional. 0 to 200 RFC 8460 JSON files (.json or .json.gz). Each item is an HTTPS URL, a record key of the default key-value store, or store-name/record-key. Limits for each file: 5 MB download, 50 MB after decompression. |
target_mode | testing or enforce. Default: enforce. |
gate_date | Optional. One ISO date for each domain, for example {"example.com": "2026-08-20"}. The key * holds for every domain. A key that names a domain outside domains is skipped with a note in the run summary. |
clean_window_days | Default 7. The number of report days with no failure session that a domain needs before it can move into enforce mode. |
alert_webhook_url | Optional. One public HTTPS URL. One JSON POST when the run finds a policy defect. |
state_store_name | The named key-value store that holds the snapshot of each domain. 3 to 63 letters, digits and hyphens, and it must not start or end with a hyphen. Default mta-sts-policy-state. |
A run with empty input {} uses the schema defaults and succeeds.
What you get
Four record types in one dataset, and a Markdown digest in the DIGEST record of
the default key-value store.
domain— one record for each domain:stsTxtCount,stsId,policyHttpStatus,contentType,redirectFound,certificateNameMatch,policyVersion,mode,maxAge,mxPatterns,liveMxHosts,mxNotCovered,tlsrptRua,policySha256.policy-defect— one record for each defect, with the previous value and the current value.tlsrpt-failure— one record for each failure group: report date, sending organization, receiving MX, result type, failed session count.readiness— one record for each domain: current mode, target mode, days clean, verdict, and the next required change.
A run also writes one run-summary record.
Defect codes
| Code | What it means |
|---|---|
policy_changed_id_unchanged | The policy file changed, the TXT id did not. Senders keep the old policy until max_age runs out. |
id_changed_policy_unchanged | The TXT id changed, the policy file did not. Every sender fetches the same policy again for nothing. |
mode_changed | The mode changed between two runs, for example enforce back to testing. |
mx_not_covered | A live MX host that no mx pattern of the policy covers. In enforce mode a sender refuses that host. |
policy_redirect | The policy URL answers a redirect. RFC 8461 section 3.3 forbids it. |
wrong_content_type | The policy file is not served as text/plain. |
multiple_sts_records | _mta-sts publishes more than one v=STSv1 record. A sender must find exactly one. |
max_age_out_of_range | max_age is not between 1 and 31557600 seconds. |
tlsrpt_missing | _smtp._tls publishes no v=TLSRPTv1 rua value, so no reporter can tell you about a failed session. |
sts_record_missing | _mta-sts publishes no v=STSv1 record. |
sts_record_no_id | The record has no valid id tag (1 to 32 letters or digits). |
policy_unreachable | The policy URL could not be read. |
certificate_name_mismatch | The certificate of mta-sts.<domain> does not carry that name. |
policy_http_error | The policy URL answers a status other than 200. |
policy_malformed | The policy body does not follow RFC 8461 section 3.2. |
The first three codes need the snapshot of the last run. The first run of a domain writes the baseline and reports only the defects that need no history.
A domain that publishes no v=STSv1 record at all gets sts_record_missing
alone, because a sender never reads the policy file of that domain. A failed DNS
lookup is marked in the domain record and is never read as a missing record.
Verdicts
ready— the published mode is the target mode and the domain has no defect.not_ready— a defect, an unknown mode, or a clean window that is too short.overdue—not_readyafter the gate date of the domain.
A verdict is a business result. The run ends SUCCEEDED for every verdict. Only a malfunction gives a FAILED run.
State between runs
For each domain the Actor keeps one record in the named key-value store
(mta-sts-policy-state by default) with the last TXT id, the last policy
SHA-256, the last mode, and the report days with their failure counts. It also
keeps the ids of the report files that it has parsed, so a file that you pass
again is not parsed twice and not charged twice.
Use the same state_store_name in each run of the same domains.
How to run it
Run it on an Apify schedule once each day: it finds policy drift before the sender caches run out. Run it again after each MX change, certificate change or policy deploy, and before the gate date.
Pricing
This Actor uses pay per event.
| Event | Unit | Price (USD) | When |
|---|---|---|---|
run-started | one Actor run | 0.002 | Once for each run with a valid input. |
domain-checked | one domain | 0.003 | Once for each domain whose DNS records and policy file are checked and compared with its snapshot. |
tlsrpt-report-parsed | one report file | 0.003 | Once for each new report file that is downloaded and parsed. A report that the state already holds is skipped and not charged. A file that fails is not charged. |
policy-defect-flagged | one defect | 0.01 | Once for each policy defect record of one domain. |
What one daily run costs
10 domains, 2 new report files, no defect:
| Event | Count | Price | Cost (USD) |
|---|---|---|---|
run-started | 1 | 0.002 | 0.0020 |
domain-checked | 10 | 0.003 | 0.0300 |
tlsrpt-report-parsed | 2 | 0.003 | 0.0060 |
policy-defect-flagged | 0 | 0.01 | 0.0000 |
| Total | 0.0380 |
That is USD 1.14 for 30 daily runs.
How the size of the run moves the bill
| Domains | Report files | Defects | Uncapped (USD) | Charged (USD) |
|---|---|---|---|---|
| 10 | 2 | 0 | 0.0380 | 0.0380 |
| 50 | 30 | 5 | 0.2920 | 0.2920 |
| 200 | 200 | 40 | 1.6020 | 1.6020 |
| 200 | 200 | 200 | 3.2020 | 3.0000 |
The maximum total charge of a run is USD 3.00 by default. When a run reaches it,
the Actor stops charging, writes what it has, says so in the status message and
in run.chargeLimitReached, and still ends SUCCEEDED. Raise the maximum in the
run options when you check many domains.
Limits
- DNS over HTTPS with public resolvers, at most 1200 lookups in one run.
- One policy GET for each domain, 10 s timeout, 64 KB maximum body, no redirect.
- Report files: 5 MB download, 50 MB after decompression, 100:1 compression ratio.
- The webhook URL and every report URL must be public HTTPS. The Actor checks the address of each hop and refuses a private, loopback, link-local or reserved one.