Email Security Check: SPF, DKIM, DMARC, MX avatar

Email Security Check: SPF, DKIM, DMARC, MX

Pricing

from $2.50 / 1,000 domain auditeds

Go to Apify Store
Email Security Check: SPF, DKIM, DMARC, MX

Email Security Check: SPF, DKIM, DMARC, MX

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.

Pricing

from $2.50 / 1,000 domain auditeds

Rating

0.0

(0)

Developer

Plainsource

Plainsource

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a day ago

Last modified

Share

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 and Yahoo's sender best practices, both checked on 2026-10-26.

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

RuleApplies toHow it is checked
SPF or DKIM set upAll sendersValid SPF record, or a DKIM key at a checked selector
DKIM key of 1024 bits or moreAll senders (Google)Size of every key found
Both SPF and DKIM set upBulk sendersBoth of the above (unverified when SPF is valid and no DKIM key was found)
DMARC with p=none or strongerBulk sendersValid DMARC record on the domain or its organizational domain
From: domain aligned with SPF or DKIMBulk sendersunverified: needs a real message
One-click unsubscribeBulk sendersunverified: a property of each message
Forward and reverse DNS of sending IPsAll sendersunverified: needs the sending IP
Spam rate below 0.3%All sendersunverified: 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:

PartPointsFull points for
SPF25A valid record ending in -all (~all gets 23)
DMARC35p=reject with a rua address (quarantine 29, none 13)
DKIM15A key of 2048 bits or Ed25519 (1024 bits gets 12)
MTA-STS10mode: enforce with every MX listed (only for domains with MX records)
TLS-RPT5A record (only for domains with MX records)
MX5Every MX host resolves (only for domains with MX records)
BIMI5A 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.

FieldWhat 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:

{
"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.

{
"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:

EventPriceWhen
Actor start$0.002Once per run
Domain audited$0.0025Each domain with status ok
Change detected$0.003Monitoring 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).