# Bulk MTA-STS, TLS-RPT & BIMI Checker (`sysadmin-toolbox/email-transport-security-checker`) Actor

Bulk-check MTA-STS, TLS-RPT and BIMI for thousands of domains. Validates the \_mta-sts record and policy file (RFC 8461), compares policy mx patterns with live MX, checks TLS-RPT rua, BIMI SVG Tiny PS logo, VMC/CMC certificate and DMARC enforcement, with a grade.

- **URL**: https://apify.com/sysadmin-toolbox/email-transport-security-checker.md
- **Developed by:** [SysAdmin Toolbox](https://apify.com/sysadmin-toolbox) (community)
- **Categories:** Developer tools, SEO tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.00 / 1,000 domain checkeds

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

## Bulk MTA-STS, TLS-RPT & BIMI Checker

Check **MTA-STS, TLS-RPT and BIMI** for one domain or ten thousand. For every domain the actor reads the `_mta-sts` record, **downloads and validates the policy file** at `https://mta-sts.<domain>/.well-known/mta-sts.txt` (RFC 8461), **compares the policy's `mx` patterns with the domain's live MX records**, validates the `_smtp._tls` **TLS-RPT** record (RFC 8460), and checks **BIMI**: the `default._bimi` record, the **SVG logo** (SVG Tiny PS rules), the **VMC / CMC mark certificate** and the **DMARC enforcement** BIMI needs. Each domain gets a status, a 0–100 score, an A–F grade and a plain-English issues list.

Use it to:

- **Roll out MTA-STS safely**: catch the MX host your policy doesn't cover *before* you switch from `testing` to `enforce` (one missed MX in enforce mode means compliant senders stop delivering to it).
- **Audit a portfolio** (MSPs, agencies, M\&A, security reviews): which domains enforce MTA-STS, which only test, which have a broken policy file, which send TLS reports nowhere.
- **Monitor** for drift: an expired certificate on `mta-sts.<domain>`, a policy URL that now redirects, an MX change the policy wasn't updated for, a mark certificate about to expire.
- **Check BIMI readiness**: DMARC at quarantine/reject with pct=100, a valid SVG Tiny PS logo and a VMC or CMC that names the domain.

### Why this one

- **The whole job is transport security and BIMI.** Email-audit bundles usually check that the TXT records exist. This actor validates the policy file line by line, follows the RFC rules exactly and explains each finding.
- **Policy vs live MX.** Every MX host is matched against the policy's `mx:` patterns with the RFC 8461 wildcard rule (`*.example.com` covers exactly one extra leftmost label, so `a.b.example.com` is **not** covered). Uncovered MX hosts are CRITICAL in enforce mode and WARN in testing mode. Patterns that match no current MX are listed too.
- **Fetched like a sending server would.** HTTPS with full certificate validation for `mta-sts.<domain>`, **no redirects** (RFC 8461 §3.3 forbids following them), Content-Type checked for `text/plain`, 64 KB policy limit, catch-all HTML pages recognised.
- **Real BIMI checks.** The SVG is downloaded and checked for `baseProfile="tiny-ps"`, `version="1.2"`, `<title>`, scripts, external references, animation, x/y on the root, size and a square viewBox. The PEM at `a=` is parsed: VMC or CMC (mark type), issuer, expiry, the BIMI EKU and whether the certificate names the domain.
- **Honest DNS answers.** Lookups go over DNS-over-HTTPS (Cloudflare, Google fallback) with retries. NXDOMAIN, "no record" and SERVFAIL/timeouts are kept apart, so a failed lookup is reported as `ERROR`, never as "missing".
- **Bulk-friendly input.** Paste domains, email addresses, URLs or record names (`_mta-sts.example.com`, `mta-sts.example.com`, `_smtp._tls.example.com`, `default._bimi.example.com`).

### Input

```json
{
  "domains": ["example.com", "jane@example.org", "https://www.example.net/contact", "_mta-sts.example.io"],
  "checkBimi": true,
  "checkVmc": true,
  "onlyProblems": false
}
```

| Field | Default | Notes |
| --- | --- | --- |
| `domains` | required | Domains, emails, URLs or record names. One per line, or comma/space separated; `#` starts a comment. IDNs are converted. Duplicates are removed and invalid entries are listed in `SUMMARY`. Up to 10,000 per run. |
| `checkBimi` | true | BIMI record, SVG logo and DMARC enforcement |
| `checkVmc` | true | Download and parse the `a=` mark certificate (VMC/CMC) |
| `bimiSelector` | `default` | BIMI selector to look up (`<selector>._bimi.<domain>`) |
| `includePolicyText` | false | Adds the downloaded `mta-sts.txt` (up to 4,000 characters) to each row |
| `onlyProblems` | false | Leave out rows with status `OK` or `NO_MAIL` (all domains are still checked, counted in `SUMMARY` and charged) |
| `concurrency` | 10 | Domains checked in parallel (max 50) |
| `dnsResolver` | `cloudflare` | `cloudflare` or `google` DNS-over-HTTPS; the other one is the fallback |
| `timeoutSecs` | 15 | Per HTTPS request (policy, logo, certificate) |

### Output

One row per domain. The dataset views are **Overview**, **MTA-STS & TLS-RPT details** and **BIMI details**.

```json
{
  "domain": "example.com",
  "status": "CRITICAL",
  "grade": "B",
  "score": 80,
  "issues": [
    "CRITICAL: MX host(s) not matched by any policy mx pattern: backup.mx.example.net (in enforce mode, compliant senders will not deliver to them); remember a wildcard covers one leftmost label only",
    "INFO: no BIMI record (default._bimi.example.com); DMARC is already at enforcement, so BIMI is possible"
  ],
  "mxHosts": ["example-com.mail.protection.outlook.com", "backup.mx.example.net"],
  "nullMx": false,
  "mtaStsStatus": "ENFORCE",
  "mtaStsMode": "enforce",
  "mtaStsId": "20261017T000000",
  "mtaStsRecord": "v=STSv1; id=20261017T000000;",
  "policyUrl": "https://mta-sts.example.com/.well-known/mta-sts.txt",
  "policyHttpStatus": 200,
  "policyContentType": "text/plain",
  "policyRedirectTo": null,
  "policyError": null,
  "policyVersion": "STSv1",
  "policyMaxAge": 1209600,
  "policyMx": ["*.mail.protection.outlook.com"],
  "mxNotCovered": ["backup.mx.example.net"],
  "policyPatternsUnused": [],
  "tlsRptStatus": "OK",
  "tlsRptRecord": "v=TLSRPTv1; rua=mailto:tlsrpt@example.com",
  "tlsRptRua": ["mailto:tlsrpt@example.com"],
  "dmarcRecord": "v=DMARC1; p=reject; rua=mailto:dmarc@example.com",
  "dmarcDomain": "example.com",
  "dmarcPolicy": "reject",
  "dmarcPct": null,
  "dmarcBimiReady": true,
  "bimiStatus": "MISSING",
  "bimiLogoUrl": null,
  "vmcType": null,
  "vmcValidTo": null,
  "lookupErrors": 0,
  "checkedAt": "2026-10-17T19:40:00.000Z"
}
```

**Domain status:** `OK`, `WARN`, `CRITICAL` (at least one CRITICAL issue), `NOT_CONFIGURED` (no MTA-STS, TLS-RPT or BIMI at all), `NO_MAIL` (null MX, nothing published, no score), `ERROR` (DNS lookups failed), `DOMAIN_NOT_FOUND` (NXDOMAIN).
**MTA-STS status:** `ENFORCE`, `TESTING`, `NONE` (mode none), `INVALID` (record or policy broken or unreachable), `INACTIVE` (policy file but no `_mta-sts` record), `MISSING`, `ERROR`.
**TLS-RPT status:** `OK`, `INVALID`, `MISSING`, `ERROR`. **BIMI status:** `OK`, `WARN`, `INVALID`, `DECLINED` (empty `l=` and `a=`), `MISSING`, `ERROR`.

**Score (0–100):** MTA-STS 60 (valid record 10, valid policy 20, enforce 20 / testing 10, every MX covered 10), TLS-RPT 25, BIMI 15 (DMARC at enforcement 5, valid logo 5, valid mark certificate 5). With `checkBimi` off the score is scaled to 100 without BIMI. Grade: A ≥ 90, B ≥ 75, C ≥ 60, D ≥ 40, F below.

**Issue rules:**

- **CRITICAL**: `_mta-sts` record malformed, without `id=`, more than one, or not starting with exactly `v=STSv1;`; `mta-sts.<domain>` missing, certificate invalid, HTTP status not 200, **any redirect**, HTML instead of a policy; policy without `version: STSv1`, a valid `mode`, `max_age` (≤ 31557600) or `mx` lines; MX not covered in enforce mode; TLS-RPT record malformed, duplicated or without a usable `rua=`; BIMI published while DMARC is not at enforcement; logo or certificate URL not https, unreachable or not 200; SVG with `<script>`; certificate expired, not a BIMI mark certificate, or not naming the domain.
- **WARN**: no MTA-STS on a domain that receives mail; policy not served as `text/plain`; `max_age` under 1 day; trailing dot in an `mx` pattern; repeated fields; MX not covered in testing mode; policy file without a `_mta-sts` record; no TLS-RPT while MTA-STS is set up; a `rua` with an unsupported scheme next to a valid one; BIMI without `a=`; SVG not Tiny PS, no `<title>`, external references, over 32 KB, not square, wrong Content-Type; mark certificate expiring within 30 days; failed lookups.
- **INFO**: testing or none mode, `max_age` under 1 week in enforce mode, patterns that match no MX, null MX, delegated `_mta-sts` CNAME, unknown fields, BIMI declination, CMC (Gmail yes, Apple Mail needs a VMC), leaf-only PEM.

The key-value store record **`SUMMARY`** has status, grade, MTA-STS, TLS-RPT and BIMI counts, the average score, every domain with MX hosts not covered by its policy, mark certificates expiring within 60 days, the problem list, invalid inputs and DNS statistics.

### Limits (please read)

- **No SMTP test.** Port 25 is not reachable from the Apify platform, so the actor does not connect to your MX hosts: STARTTLS support and the MX certificates themselves are not verified. MTA-STS also requires every MX to offer STARTTLS with a certificate that is valid for the MX name; check that separately (the [Bulk SSL Certificate Expiry Monitor](https://apify.com/sysadmin-toolbox/bulk-ssl-expiry-monitor) can test STARTTLS on port 587 and other ports, but port 25 is blocked there too).
- The mark certificate is parsed, not chain-validated against the BIMI root CAs, and the logo embedded in the certificate is not compared with the `l=` SVG.
- DMARC for a subdomain is found by walking up the tree (`_dmarc.sub.example.com`, then `_dmarc.example.com`), without a public-suffix list. BIMI falls back to the organisational domain the same way.
- Results reflect public DNS through Cloudflare/Google DNS-over-HTTPS. Internal or split-horizon zones are not visible. Policy, logo and certificate hosts that resolve to private addresses are not fetched.
- No mail is sent.

### Pricing

Pay per event: **$2 per 1,000 domains** ($0.002 per domain checked, including the policy, logo and certificate downloads). Invalid and duplicate inputs aren't charged.

### FAQ

**My policy says `mx: *.example.com` and my MX is `mx1.mail.example.com`. Why is it "not covered"?**
In MTA-STS a wildcard matches exactly one label. `*.example.com` covers `mx1.example.com` but not `mx1.mail.example.com` or `example.com` itself. Add `mx: *.mail.example.com` or the full host name.

**Can I redirect `mta-sts.example.com` to another site?**
No. RFC 8461 says senders must not follow redirects for the policy URL, so a 301/302 means no policy at all. Serve the file directly with HTTP 200 (a CNAME to a host that serves it with a valid certificate for `mta-sts.example.com` is fine).

**Should I start in enforce mode?**
Start with `mode: testing` and a TLS-RPT record, read the reports for a week or two, fix any MX the reports or this actor flag, then switch to `enforce` and bump the `id` in the `_mta-sts` record.

**Why does BIMI say DMARC is not at enforcement when I have p=quarantine?**
BIMI also needs `pct=100` (or no `pct`) and no test mode (`t=y`). For a subdomain without its own DMARC record, the parent's `sp=` applies.

**Do I need a VMC?**
Gmail shows logos backed by a VMC or a CMC; Apple Mail needs a VMC. Yahoo can show logos for some senders without one. Without `a=` the actor reports a WARN.

**Can I schedule it?**
Yes. Save your domain list as a task and schedule it (for example weekly) with `onlyProblems` on to catch policy, MX and certificate drift.

### Related

- [Bulk SPF / DMARC / MX Auditor](https://apify.com/sysadmin-toolbox/email-security-audit): email-spoofing risk scores for domain lists.
- [Bulk DKIM Selector Finder](https://apify.com/sysadmin-toolbox/dkim-selector-finder): discover DKIM selectors, key sizes and sending providers.
- [Microsoft 365 Tenant Finder](https://apify.com/sysadmin-toolbox/m365-tenant-finder): tenant ID, sign-in type, identity provider and mail provider for company domains.
- [Bulk SSL Certificate Expiry Monitor](https://apify.com/sysadmin-toolbox/bulk-ssl-expiry-monitor): certificate expiry on any port, including STARTTLS mail servers.
- [Subdomain Takeover Checker](https://apify.com/sysadmin-toolbox/subdomain-takeover-checker): find dangling CNAMEs and takeover-prone subdomains.
- [Cloud IP Range Lookup](https://apify.com/sysadmin-toolbox/cloud-ip-range-lookup): match IPs to AWS, Azure, GCP, Cloudflare, GitHub and verified crawlers.
- [Bulk ASN IP Lookup](https://apify.com/sysadmin-toolbox/asn-ip-lookup): IP to ASN, organisation, country and BGP prefix.

# Actor input Schema

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

Domains to check. One per line (comma- or space-separated lists work too). Email addresses and URLs are reduced to their domain (user@example.com, https://www.example.com/page). Record names such as \_mta-sts.example.com, mta-sts.example.com, \_smtp.\_tls.example.com or default.\_bimi.example.com are reduced to the domain too. Duplicates are removed; invalid entries are listed in SUMMARY. Max 10,000 domains per run.

## `checkBimi` (type: `boolean`):

Look up the BIMI record, fetch the SVG logo and check that DMARC is at enforcement (quarantine or reject, pct=100).

## `checkVmc` (type: `boolean`):

Download the a= certificate and report its type, mark type, issuer, expiry and whether it lists the domain.

## `bimiSelector` (type: `string`):

Selector to look up at <selector>.\_bimi.<domain>. Mailbox providers use 'default' unless a message carries a BIMI-Selector header.

## `includePolicyText` (type: `boolean`):

Adds the downloaded mta-sts.txt (up to 4,000 characters) to each row.

## `onlyProblems` (type: `boolean`):

Skip rows whose status is OK or NO_MAIL. All domains are still checked, counted in SUMMARY and charged.

## `concurrency` (type: `integer`):

How many domains are checked at once. DNS lookups are capped at 4 per parallel domain (max 100).

## `dnsResolver` (type: `string`):

Public resolver used for lookups. SERVFAIL, timeouts and HTTP errors are retried and then sent to the other resolver.

## `timeoutSecs` (type: `integer`):

Per HTTPS request (policy file, SVG logo, certificate). RFC 8461 suggests senders wait up to one minute for the policy.

## Actor input object example

```json
{
  "domains": [
    "google.com",
    "microsoft.com",
    "paypal.com"
  ],
  "checkBimi": true,
  "checkVmc": true,
  "bimiSelector": "default",
  "includePolicyText": false,
  "onlyProblems": false,
  "concurrency": 10,
  "dnsResolver": "cloudflare",
  "timeoutSecs": 15
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `summary` (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",
        "paypal.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("sysadmin-toolbox/email-transport-security-checker").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",
        "paypal.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("sysadmin-toolbox/email-transport-security-checker").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",
    "paypal.com"
  ]
}' |
apify call sysadmin-toolbox/email-transport-security-checker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,sysadmin-toolbox/email-transport-security-checker"
        }
    }
}
```

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/Ss8RxHStJA9f9A5PE/builds/rfpnFOAsvLjzsl6L4/openapi.json
