# Email Security Check: SPF, DKIM, DMARC, MX (`fernandosord/email-security-audit-spf-dkim-dmarc`) Actor

Audit SPF (with its DNS lookup count), DMARC, DKIM key sizes, MTA-STS, TLS-RPT, BIMI and MX for a list of domains. Get a grade from A to F, issues with exact fixes, and a Google/Yahoo bulk sender check. Optional monitoring with change alerts.

- **URL**: https://apify.com/fernandosord/email-security-audit-spf-dkim-dmarc.md
- **Developed by:** [Plainsource](https://apify.com/fernandosord) (community)
- **Categories:** Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.50 / 1,000 domain auditeds

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

### What this Actor does

Email Security Audit checks the email authentication records of a list of domains and tells you, in plain language, what is wrong and the exact DNS change that fixes it. It is a bulk DMARC checker, SPF checker and DKIM lookup in one run, with one row per domain.

For each domain you get:

- **A grade from A to F and a score out of 100.**
- **Issues with a severity and a fix line**, for example `Add a TXT record at _dmarc.example.com: "v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com"`.
- **A Google and Yahoo sender requirements check**: which of the February 2024 rules for all senders and for bulk senders the domain's DNS passes.
- **The details behind the grade**: the full SPF include tree, DMARC tags, DKIM keys with their size, MX records and mail provider, MTA-STS policy, TLS-RPT, BIMI and DNSSEC.

Typical uses:

- Check your own domains before a cold email or newsletter campaign
- Audit every client domain at once if you run an agency or an MSP
- Find SPF records that break the 10-lookup limit after a new tool was added
- Re-run on a schedule in monitoring mode and get a Slack or Discord message when a record changes

### What it checks

**SPF**

- Parses the record and every `include:` and `redirect=` below it.
- Counts DNS lookups (`include`, `a`, `mx`, `ptr`, `exists`, `redirect`) across the whole tree against the RFC 7208 limit of 10, and counts void lookups (names that return nothing; the limit is 2).
- Flags `+all`, `?all` and a missing `all`, duplicate SPF records, the deprecated `ptr` mechanism, includes that have no SPF record, loops, `mx` terms with more than 10 hosts, syntax errors (including malformed `ip4:`/`ip6:` ranges) and typos such as `include=` that receivers silently ignore.
- Returns the include tree and a flattened list of every IPv4 and IPv6 range the record authorizes (`a` and `mx` terms are resolved).

**DMARC**

- Policy (`p`), subdomain policy (`sp`), `pct`, `rua` and `ruf` report addresses, alignment modes (`adkim`, `aspf`).
- Falls back to the organizational domain for subdomains, as receivers do.
- Flags a missing record, `p=none`, `pct` under 100, `sp=none`, missing `rua`, duplicate records, syntax errors, and report addresses on another domain that has not published the authorization record receivers require (RFC 7489, section 7.1).
- In monitoring mode, flags `p=none` that has stayed in place for 30 days or more.

**DKIM**

- Looks up 52 common selectors (google, selector1, selector2, k1, k2, s1, s2, default, mandrill, zoho, protonmail and others) plus up to 20 of your own.
- Reads each key's type (RSA or Ed25519) and size. Keys under 1024 bits are a high-severity issue; 1024-bit keys are a low-severity note recommending 2048.
- Also flags revoked keys, test-mode keys (`t=y`), SHA-1-only keys, keys that cannot be decoded, and wildcard records that answer for every selector.

**MTA-STS, TLS-RPT, BIMI and MX**

- MTA-STS: the `_mta-sts` TXT record, then the policy file at `https://mta-sts.<domain>/.well-known/mta-sts.txt`. The policy is validated (version, mode, max_age, mx lines, text/plain type) and every MX host is matched against its `mx:` patterns. Redirects are refused, as RFC 8461 requires of senders.
- TLS-RPT: the `_smtp._tls` record and its report destinations.
- BIMI: the `default._bimi` record, the logo URL, and whether a certificate URL (`a=`, a VMC or CMC) is present. The certificate itself is not downloaded.
- MX: the records, whether each host resolves, null MX (RFC 7505), and the mail provider (Google Workspace, Microsoft 365, Proofpoint, Mimecast and about 40 others).
- DNSSEC (optional, on by default): whether answers for the domain are DNSSEC-validated, from Google Public DNS. Broken signatures (validating resolvers return SERVFAIL) are a high-severity issue. DNSSEC does not change the grade.

### Google and Yahoo sender requirements

Since February 2024, Gmail and Yahoo Mail require every sender to set up SPF or DKIM, and bulk senders (for Google, more than 5,000 messages a day to Gmail accounts) to set up both SPF and DKIM and publish a DMARC record (`p=none` is enough). Google also requires DKIM keys of at least 1024 bits. Sources: [Google's email sender guidelines](https://support.google.com/a/answer/81126) and [Yahoo's sender best practices](https://senders.yahooinc.com/best-practices/), both checked on 2026-10-26.

The `bulkSenderRequirements` field lists each rule with `pass`, `fail` or `unverified`:

| Rule | Applies to | How it is checked |
|---|---|---|
| SPF or DKIM set up | All senders | Valid SPF record, or a DKIM key at a checked selector |
| DKIM key of 1024 bits or more | All senders (Google) | Size of every key found |
| Both SPF and DKIM set up | Bulk senders | Both of the above (`unverified` when SPF is valid and no DKIM key was found) |
| DMARC with `p=none` or stronger | Bulk senders | Valid DMARC record on the domain or its organizational domain |
| From: domain aligned with SPF or DKIM | Bulk senders | `unverified`: needs a real message |
| One-click unsubscribe | Bulk senders | `unverified`: a property of each message |
| Forward and reverse DNS of sending IPs | All senders | `unverified`: needs the sending IP |
| Spam rate below 0.3% | All senders | `unverified`: only visible in Postmaster Tools |

`allSendersDnsChecks` and `bulkSendersDnsChecks` are `pass` when every rule that DNS can answer passes and `fail` when one fails. `bulkSendersDnsChecks` is `unverified` when SPF is valid but no DKIM key was found at the checked selectors: the domain may sign with a selector this Actor cannot guess, so it is not called a failure. Both are `not_applicable` for a domain that neither sends nor receives mail. A `pass` here does not mean the domain meets every rule; the unverified ones still need checking.

### How the grade works

Domains that send or receive mail are scored out of 100:

| Part | Points | Full points for |
|---|---|---|
| SPF | 25 | A valid record ending in `-all` (`~all` gets 23) |
| DMARC | 35 | `p=reject` with a `rua` address (`quarantine` 29, `none` 13) |
| DKIM | 15 | A key of 2048 bits or Ed25519 (1024 bits gets 12) |
| MTA-STS | 10 | `mode: enforce` with every MX listed (only for domains with MX records) |
| TLS-RPT | 5 | A record (only for domains with MX records) |
| MX | 5 | Every MX host resolves (only for domains with MX records) |
| BIMI | 5 | A record, with DMARC at quarantine or reject |

Caps: no valid DMARC record caps the score at 54 (grade D), a missing or broken SPF record at 69 (C), `+all` at 39 (F). Grades: A 85 and up, B 70 to 84, C 55 to 69, D 40 to 54, F below 40.

A domain with no MX records and no senders in SPF or DKIM is scored as a parked domain: `v=spf1 -all` (40), DMARC `p=reject` (50) and a null MX (10). `scoreBreakdown` in each row shows the points.

### Input

The only required field is **Domains**.

| Field | What it does |
|---|---|
| **Domains** (`domains`) | Domains to audit. URLs and email addresses are reduced to the domain, a leading `www.` is dropped, and subdomains are kept (a sending subdomain has its own SPF and DKIM). Up to 5,000 per run. |
| **Extra DKIM selectors** (`dkimSelectors`) | Your own selector names, checked as well as the 52 common ones. Up to 20. |
| **Check DNSSEC** (`checkDnssec`) | Ask Google Public DNS whether the domain is DNSSEC-validated. On by default. |
| **Monitoring list name** (`stateKey`) | Turns on monitoring mode (see below). Empty by default. |
| **Webhook URL** (`webhookUrl`) | Monitoring mode only. An https:// URL that gets a POST when a domain changed. |
| **Webhook format** (`webhookFormat`) | `json` (default), `slack` or `discord`. |
| **Respect robots.txt** (`respectRobotsTxt`) | Applies to the MTA-STS policy fetch, the only request sent to the domain's own servers. On by default. |
| **Domains checked at once** (`maxConcurrency`) | 1 to 20, default 5. |
| **Request timeout** (`timeoutSecs`) | 5 to 60 seconds, default 15, for the MTA-STS policy fetch. |

Example input:

```json
{
    "domains": ["yourcompany.com", "mail.yourcompany.com", "client-one.com"],
    "dkimSelectors": ["s2024"],
    "stateKey": "agency-clients",
    "webhookUrl": "https://hooks.slack.com/services/...",
    "webhookFormat": "slack"
}
```

### Output

One row per domain. This is a real row from a run on Apify (run iAW7c0hL14NgywOyg). To keep it short, the issue list, the include tree, the flattened IP list, the DKIM keys and the sender checks are cut to their first few entries; the run returned 4 issues (plus 1 informational), 8 include-tree entries, 55 IPv4 and 7 IPv6 entries, 5 DKIM keys and 8 sender checks.

```json
{
    "input": "python.org",
    "domain": "python.org",
    "status": "ok",
    "grade": "C",
    "score": 56,
    "summary": "Mail domain, grade C. No critical or high issues.",
    "mailProvider": "Self-hosted or other",
    "receivesMail": true,
    "sendsMail": true,
    "dmarcPolicy": "none",
    "spfResult": "valid",
    "spfLookupCount": 8,
    "spfAll": "~all",
    "dkimSelectorsFound": ["google", "k2", "k3", "mailjet", "mx"],
    "mtaStsMode": null,
    "hasTlsRpt": false,
    "hasBimi": false,
    "dnssecValidated": false,
    "allSendersDnsChecks": "pass",
    "bulkSendersDnsChecks": "pass",
    "criticalIssues": 0,
    "highIssues": 0,
    "issueCount": 4,
    "issues": [
        {
            "severity": "medium",
            "area": "DMARC",
            "code": "dmarc-p-none",
            "message": "The DMARC policy is \"p=none\" (monitoring only). Mail that fails SPF and DKIM is still delivered, so spoofed mail is not stopped.",
            "fix": "Once the reports show all your real senders passing, change the record at _dmarc.python.org to \"p=quarantine\", then \"p=reject\"."
        },
        {
            "severity": "low",
            "area": "DKIM",
            "code": "dkim-1024",
            "message": "The DKIM keys at \"google\", \"mailjet\", \"mx\" are 1024 bits. They still work, but 2048 bits is the current recommendation (RFC 8301, Google's sender guidelines).",
            "fix": "In the service that signs with each selector, generate a 2048-bit key (usually under a new selector), switch signing to it, then remove the old record."
        },
        {
            "severity": "low",
            "area": "MTA-STS",
            "code": "mtasts-missing",
            "message": "No MTA-STS policy, so an attacker on the network path can make senders deliver mail to this domain without encryption.",
            "fix": "Publish a policy file at https://mta-sts.python.org/.well-known/mta-sts.txt (\"version: STSv1\", \"mode: testing\", one \"mx:\" line per mail server, \"max_age: 604800\") and a TXT record at _mta-sts.python.org: \"v=STSv1; id=2026102601\"."
        }
    ],
    "bulkSenderRequirements": {
        "allSendersDnsChecks": "pass",
        "bulkSendersDnsChecks": "pass",
        "checks": [
            {
                "id": "spf-or-dkim",
                "appliesTo": "all senders",
                "requirement": "SPF or DKIM is set up for the sending domain",
                "status": "pass",
                "detail": "SPF is valid; DKIM key at google, k2, k3, mailjet, mx",
                "sources": ["https://support.google.com/a/answer/81126", "https://senders.yahooinc.com/best-practices/"]
            },
            {
                "id": "dkim-key-1024",
                "appliesTo": "all senders",
                "requirement": "DKIM keys are at least 1024 bits (Google)",
                "status": "pass",
                "detail": "Smallest key: 1024 bits.",
                "sources": ["https://support.google.com/a/answer/81126"]
            },
            {
                "id": "spf-and-dkim",
                "appliesTo": "bulk senders",
                "requirement": "Both SPF and DKIM are set up",
                "status": "pass",
                "detail": "SPF valid; DKIM key found",
                "sources": ["https://support.google.com/a/answer/81126", "https://senders.yahooinc.com/best-practices/"]
            },
            {
                "id": "dmarc-policy",
                "appliesTo": "bulk senders",
                "requirement": "A valid DMARC record with p=none or stronger",
                "status": "pass",
                "detail": "p=none",
                "sources": ["https://support.google.com/a/answer/81126", "https://senders.yahooinc.com/best-practices/"]
            },
            {
                "id": "from-alignment",
                "appliesTo": "bulk senders",
                "requirement": "The From: domain aligns with the SPF or DKIM domain",
                "status": "unverified",
                "detail": "Needs the headers of a real message. DNS can show only that the records exist.",
                "sources": ["https://support.google.com/a/answer/81126", "https://senders.yahooinc.com/best-practices/"]
            }
        ],
        "note": "Checked from DNS only. Requirements marked \"unverified\" need a real message, the sending IP or the provider dashboards."
    },
    "spf": {
        "record": "v=spf1 mx ip4:188.166.95.178/32 ip6:2a03:b0c0:2:d0::71:1 include:stspg-customer.com include:_spf.google.com include:mailgun.org ~all",
        "recordCount": 1,
        "allQualifier": "~all",
        "lookupCount": 8,
        "lookupLimit": 10,
        "lookupLimitExceeded": false,
        "voidLookups": 0,
        "usesPtr": false,
        "includes": ["stspg-customer.com", "_spf.google.com", "mailgun.org"],
        "redirect": null,
        "includeTree": [
            {
                "depth": 0,
                "domain": "python.org",
                "via": null,
                "record": "v=spf1 mx ip4:188.166.95.178/32 ip6:2a03:b0c0:2:d0::71:1 include:stspg-customer.com include:_spf.google.com include:mailgun.org ~all",
                "lookups": 4
            },
            {
                "depth": 1,
                "domain": "stspg-customer.com",
                "via": "include",
                "record": "v=spf1 ip4:23.253.182.103 ip4:23.253.183.145 ip4:23.253.183. ... ~all",
                "lookups": 0
            },
            {
                "depth": 1,
                "domain": "_spf.google.com",
                "via": "include",
                "record": "v=spf1 ip4:74.125.0.0/16 ip4:209.85.128.0/17 ip6:2001:4860:4864::/56 ip6:2404:6800:4864::/56 ip6:2607:f8b0:4864::/56 ip6:2800:3f0:4864::/56 ip6:2a00:1450:4864::/56 ip6:2c0f:fb50:4864::/56 ~all",
                "lookups": 0
            }
        ],
        "flattened": {
            "ip4": ["188.166.95.178", "23.253.182.103", "23.253.183.145", "23.253.183.146"],
            "ip6": ["2a03:b0c0:2:d0::71:1", "2001:4860:4864::/56"],
            "complete": true,
            "note": null
        },
        "result": "valid",
        "authorizesSenders": true
    },
    "dmarc": {
        "record": "v=DMARC1; p=none; pct=100; rua=mailto:***@dmarc.postmarkapp.com; sp=none; aspf=r;",
        "recordLocation": "_dmarc.python.org",
        "source": "domain",
        "policy": "none",
        "effectivePolicy": "none",
        "subdomainPolicy": "none",
        "pct": 100,
        "rua": ["mailto:***@dmarc.postmarkapp.com"],
        "ruf": [],
        "reportDestinations": [{"domain": "dmarc.postmarkapp.com", "external": true, "authorized": true}],
        "adkim": "r",
        "aspf": "r",
        "valid": true
    },
    "dkim": {
        "selectorsChecked": 52,
        "selectorsFound": ["google", "k2", "k3", "mailjet", "mx"],
        "keys": [
            {
                "selector": "google",
                "keyType": "rsa",
                "keyBits": 1024,
                "revoked": false,
                "testing": false,
                "sha1Only": false,
                "parseError": false,
                "custom": false
            },
            {
                "selector": "k2",
                "keyType": "rsa",
                "keyBits": 2048,
                "revoked": false,
                "testing": false,
                "sha1Only": false,
                "parseError": false,
                "custom": false
            }
        ],
        "strongestKeyBits": 2048,
        "wildcard": false,
        "dnsErrors": 0
    },
    "mx": {
        "records": [{"priority": 50, "exchange": "mail.python.org", "resolves": true}],
        "provider": "Self-hosted or other",
        "nullMx": false
    },
    "mtaSts": {
        "record": null,
        "id": null,
        "policyUrl": null,
        "policyChecked": false,
        "policyFetched": false,
        "mode": null,
        "maxAge": null,
        "mx": [],
        "mxCovered": null,
        "policyError": null
    },
    "tlsRpt": {"record": null, "rua": []},
    "bimi": {"record": null, "logoUrl": null, "certificateUrl": null, "hasCertificate": false},
    "dnssec": {"validated": false, "dsRecord": false, "error": null},
    "scoreBreakdown": [
        {"check": "SPF", "points": 23, "max": 25},
        {"check": "DMARC", "points": 13, "max": 35},
        {"check": "DKIM", "points": 15, "max": 15},
        {"check": "MTA-STS", "points": 0, "max": 10},
        {"check": "TLS-RPT", "points": 0, "max": 5},
        {"check": "MX", "points": 5, "max": 5},
        {"check": "BIMI", "points": 0, "max": 5}
    ],
    "monitoring": null,
    "checkedAt": "2026-10-26T20:02:53.128Z",
    "error": null
}
```

Field notes:

- `status` is `ok`, `invalid_input`, `not_found` (the domain does not exist in DNS), `not_allowed`, `failed` (DNS did not answer) or `skipped` (your maximum cost was reached).
- `issues[].severity` is `critical`, `high`, `medium`, `low` or `info`. `issueCount` leaves out `info`.
- `spfResult` is `valid`, `permerror` (receivers fail SPF), `temperror` (a lookup timed out) or `none`.
- `dmarcPolicy` is the policy that applies to the domain, including one inherited from its organizational domain.
- `mtaStsMode` is `enforce`, `testing`, `none`, `policy unreadable`, `not checked` or null when there is no record.
- `monitoring` is null unless monitoring mode is on.

The run's key-value store also holds `OUTPUT`: counts per status and per grade, and the domains that changed.

### Monitoring mode

Set **Monitoring list name** and schedule the Actor (for example weekly) in Apify. The first run saves each domain's result as a baseline (`monitoring.status: "baseline"`). Later runs with the same name compare against it and report:

- the grade, SPF record, SPF lookup count, SPF `all` qualifier, DMARC record and policy, DKIM keys, MX records, MTA-STS mode and id, TLS-RPT and BIMI records and DNSSEC state, before and after
- issues that are new or resolved
- how long DMARC has been at `p=none` (`monitoring.dmarcNoneSince`); from 30 days on, the `p=none` issue becomes high severity

If some DNS lookups fail during a run, that domain is not compared or saved (`monitoring.status: "not_compared"`), so a timeout is never reported as a removed record. Results are kept in your account in a key-value store named `plainsource-email-security-audit-state`. Use a different list name for each client or list.

With a webhook URL, one POST is sent per run when at least one domain changed. Slack and Discord get a short text message, and text from DNS records is escaped so a record cannot mention `@everyone` or `@channel`. `json` sends the list of changes.

### Pricing

Pay per event:

| Event | Price | When |
|---|---|---|
| Actor start | $0.002 | Once per run |
| Domain audited | $0.0025 | Each domain with status `ok` |
| Change detected | $0.003 | Monitoring mode only: each domain whose records changed since the last run |

Invalid, not-found, refused and failed domains are free. Example: 200 domains cost $0.002 + 200 × $0.0025 = $0.502. A weekly monitoring run on 20 domains where 1 changed costs $0.002 + $0.05 + $0.003 = $0.055.

If you set a maximum cost for a run, domains past the limit are returned with `status: "skipped"` and are not charged. In monitoring mode, a change that could not be charged is reported again on the next run.

### Limits

- Up to 5,000 domains per run. Two runs of 140 domains took 26 and 38 seconds on Apify with the default settings.
- DKIM keys can only be found by selector name. A domain that signs with a selector outside the 52 common ones gets "DKIM not found" until you add the selector under **Extra DKIM selectors**. You can read it from the `s=` value in the DKIM-Signature header of a message the domain sent.
- From:-domain alignment, one-click unsubscribe, PTR records of sending IPs and spam rates cannot be read from DNS. They are listed as `unverified`.
- STARTTLS on the MX hosts is not tested. Apify's network blocks outbound connections to port 25 (a test run against four mail servers, including Google's and Microsoft's, timed out on all four), so the Actor only reports what DNS and the MTA-STS policy say about TLS.
- The BIMI certificate (VMC or CMC) is not downloaded or validated; the Actor only reports whether its URL is present.
- DNS answers come from Cloudflare (1.1.1.1) and Google (8.8.8.8) public resolvers. A record changed in the last few minutes may still show the old value.
- Blocklist (RBL) checks are not included.

### Privacy and data use

- The Actor reads public DNS records, one public file per domain (the MTA-STS policy) and, for DNSSEC, one answer from Google Public DNS. It never connects to a mail server, never sends mail and never tests mailboxes.
- Email addresses inside DMARC and TLS-RPT records are returned with the part before `@` masked (`mailto:***@example.com`). If you enter an email address as input, the row shows it masked the same way.
- Names that point to private or local IP addresses are refused, and the MTA-STS policy URL (with any redirect) and the webhook URL pass the same check.

### FAQ

**Is this an SPF checker that counts nested lookups?**
Yes. Every include and redirect is followed and every lookup-causing term is counted, so `spfLookupCount` is the number receivers use. `spf.includeTree` shows where the lookups come from.

**Why does a large company get "DKIM not found"?**
Many large senders use date-based or random selector names that no list can guess. If you know the selector, add it under **Extra DKIM selectors**.

**Does a grade of A mean my mail will reach the inbox?**
No. The grade measures authentication and transport security records. Inbox placement also depends on your sending reputation, content and complaint rate.

**What is the difference between this and a domain profile?**
This Actor does only the email audit, adds the sender requirements check, fix lines and monitoring, and costs less per domain. It does not look up registration (WHOIS/RDAP), hosting or TLS certificates of websites.

**Can I check subdomains?**
Yes. `mail.example.com` is audited as entered. If it has no DMARC record, the organizational domain's policy is used, as receivers do, and the row says so in `dmarc.source`.

**Why was a domain refused?**
`not_allowed` is returned for local or internal names, names that point to private IP addresses, and a few sites whose terms forbid automated access (LinkedIn, Facebook, Instagram, X/Twitter, TikTok).

# Actor input Schema

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

Domains to audit, one per line, such as <code>yourcompany.com</code> or a sending subdomain like <code>mail.yourcompany.com</code>. URLs and email addresses are accepted and reduced to the domain; a leading <code>www.</code> is dropped. Up to 5,000 per run.

## `dkimSelectors` (type: `array`):

DKIM keys can only be found by name. 52 common selectors (google, selector1, selector2, k1, s1, default and others) are always checked. Add your own here: it is the <code>s=</code> value in the DKIM-Signature header of a message you sent. Up to 20.

## `checkDnssec` (type: `boolean`):

Ask Google Public DNS (dns.google) whether the domain's answers are DNSSEC-validated and whether a DS record exists. Informational: it does not change the grade.

## `stateKey` (type: `string`):

Turn on monitoring mode: each domain's result is saved under this name, and later runs with the same name report what changed (grade, SPF, DMARC, DKIM keys, MX, MTA-STS, new and resolved issues). Leave empty for a one-off audit. Use a different name per client or list.

## `webhookUrl` (type: `string`):

Optional, monitoring mode only. An https:// URL that receives a POST when at least one domain changed: a Slack or Discord incoming webhook, or any endpoint that accepts JSON.

## `webhookFormat` (type: `string`):

<code>slack</code> and <code>discord</code> send a short text message; <code>json</code> sends the list of changes.

## `respectRobotsTxt` (type: `boolean`):

Applies to the one web request this Actor makes: the MTA-STS policy file at <code>https://mta-sts.\<domain>/.well-known/mta-sts.txt</code>. Everything else is DNS.

## `maxConcurrency` (type: `integer`):

How many domains are audited in parallel. Each domain takes about 80 DNS queries to public resolvers, so higher values can hit their rate limits.

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

How long to wait for the MTA-STS policy file. DNS queries have their own limit of a few seconds.

## Actor input object example

```json
{
  "domains": [
    "google.com",
    "github.com",
    "python.org"
  ],
  "checkDnssec": true,
  "webhookFormat": "json",
  "respectRobotsTxt": true,
  "maxConcurrency": 5,
  "timeoutSecs": 15
}
```

# Actor output Schema

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

One row per domain: grade, score, issues with fixes, Google/Yahoo sender checks and the SPF, DMARC, DKIM, MX, MTA-STS, TLS-RPT, BIMI and DNSSEC details, or the reason it could not be audited.

## `summary` (type: `string`):

Counts per status and per grade, and the domains that changed in monitoring mode.

# 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",
        "github.com",
        "python.org"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("fernandosord/email-security-audit-spf-dkim-dmarc").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",
        "github.com",
        "python.org",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("fernandosord/email-security-audit-spf-dkim-dmarc").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",
    "github.com",
    "python.org"
  ]
}' |
apify call fernandosord/email-security-audit-spf-dkim-dmarc --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,fernandosord/email-security-audit-spf-dkim-dmarc"
        }
    }
}
```

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/dbDXXKhXabWhk83cr/builds/JxzR0L2dR6qbTZloW/openapi.json
