# MTA-STS Policy Publication Drift and Enforce-Mode Readiness (`kingii98/mta-sts-policy-publication-drift-and-enforce-mode-readiness`) Actor

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

- **URL**: https://apify.com/kingii98/mta-sts-policy-publication-drift-and-enforce-mode-readiness.md
- **Developed by:** [kingii98](https://apify.com/kingii98) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 run starteds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## 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-sts` TXT record (how many, and the `id` tag),
- 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`, `mx` patterns),
- the live MX set against the `mx` patterns,
- the `_smtp._tls` TXT record (the TLS-RPT `rua` values),
- 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.

1. **`domain`** — one record for each domain: `stsTxtCount`, `stsId`,
   `policyHttpStatus`, `contentType`, `redirectFound`, `certificateNameMatch`,
   `policyVersion`, `mode`, `maxAge`, `mxPatterns`, `liveMxHosts`, `mxNotCovered`,
   `tlsrptRua`, `policySha256`.
2. **`policy-defect`** — one record for each defect, with the previous value and
   the current value.
3. **`tlsrpt-failure`** — one record for each failure group: report date, sending
   organization, receiving MX, result type, failed session count.
4. **`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_ready` after 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.

# Actor input Schema

## `domains` (type: `array`):

1 to 200 mail domains, for example example.com. For each domain the Actor reads \_mta-sts TXT, \_smtp.\_tls TXT, the MX set and https://mta-sts.<domain>/.well-known/mta-sts.txt.

## `tlsrpt_report_files` (type: `array`):

Optional. 0 to 200 TLS-RPT report files (RFC 8460 JSON, .json or .json.gz). Each item is an HTTPS URL, a record key of the default key-value store of the run, or 'store-name/record-key'. Limits for each file: 5 MB download, 50 MB after decompression.

## `target_mode` (type: `string`):

The mode that the policy file must reach. The readiness verdict compares it with the published mode.

## `gate_date` (type: `object`):

Optional. One ISO date for each domain, for example {"example.com": "2026-08-20"}. The key '\*' holds for every domain. After the date a domain that is not ready has the verdict 'overdue'. A key that names a domain outside the domain list is skipped with a note.

## `clean_window_days` (type: `integer`):

The number of TLS-RPT report days with zero failure sessions that a domain needs before it can move into enforce mode.

## `alert_webhook_url` (type: `string`):

Optional. One public HTTPS URL. The Actor sends one JSON POST when the run finds a policy defect.

## `state_store_name` (type: `string`):

The named key-value store that keeps one snapshot for each domain (last TXT id, last policy SHA-256, last mode, report days) between runs. Use the same name in each run. 3 to 63 letters, digits and hyphens, and it must not start or end with a hyphen.

## Actor input object example

```json
{
  "domains": [
    "google.com",
    "microsoft.com",
    "example.com"
  ],
  "tlsrpt_report_files": [
    "https://raw.githubusercontent.com/domainaware/parsedmarc/ae1e5adb6609946209278b2bf3f633c752a09383/samples/smtp_tls/smtp_tls.json",
    "https://raw.githubusercontent.com/domainaware/parsedmarc/ae1e5adb6609946209278b2bf3f633c752a09383/samples/smtp_tls/mail.ru.json"
  ],
  "target_mode": "enforce",
  "gate_date": {},
  "clean_window_days": 7,
  "state_store_name": "mta-sts-policy-state"
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

## `digest` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "domains": [
        "google.com",
        "microsoft.com",
        "example.com"
    ],
    "tlsrpt_report_files": [
        "https://raw.githubusercontent.com/domainaware/parsedmarc/ae1e5adb6609946209278b2bf3f633c752a09383/samples/smtp_tls/smtp_tls.json",
        "https://raw.githubusercontent.com/domainaware/parsedmarc/ae1e5adb6609946209278b2bf3f633c752a09383/samples/smtp_tls/mail.ru.json"
    ],
    "target_mode": "enforce",
    "clean_window_days": 7,
    "state_store_name": "mta-sts-policy-state"
};

// Run the Actor and wait for it to finish
const run = await client.actor("kingii98/mta-sts-policy-publication-drift-and-enforce-mode-readiness").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "domains": [
        "google.com",
        "microsoft.com",
        "example.com",
    ],
    "tlsrpt_report_files": [
        "https://raw.githubusercontent.com/domainaware/parsedmarc/ae1e5adb6609946209278b2bf3f633c752a09383/samples/smtp_tls/smtp_tls.json",
        "https://raw.githubusercontent.com/domainaware/parsedmarc/ae1e5adb6609946209278b2bf3f633c752a09383/samples/smtp_tls/mail.ru.json",
    ],
    "target_mode": "enforce",
    "clean_window_days": 7,
    "state_store_name": "mta-sts-policy-state",
}

# Run the Actor and wait for it to finish
run = client.actor("kingii98/mta-sts-policy-publication-drift-and-enforce-mode-readiness").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "domains": [
    "google.com",
    "microsoft.com",
    "example.com"
  ],
  "tlsrpt_report_files": [
    "https://raw.githubusercontent.com/domainaware/parsedmarc/ae1e5adb6609946209278b2bf3f633c752a09383/samples/smtp_tls/smtp_tls.json",
    "https://raw.githubusercontent.com/domainaware/parsedmarc/ae1e5adb6609946209278b2bf3f633c752a09383/samples/smtp_tls/mail.ru.json"
  ],
  "target_mode": "enforce",
  "clean_window_days": 7,
  "state_store_name": "mta-sts-policy-state"
}' |
apify call kingii98/mta-sts-policy-publication-drift-and-enforce-mode-readiness --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kingii98/mta-sts-policy-publication-drift-and-enforce-mode-readiness"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/EY0nYRnErNNJPHDcw/builds/TPJL3aNFAv8Ocw8kF/openapi.json
