Email Security Signals: DMARC, SPF, DKIM & BIMI Checker avatar

Email Security Signals: DMARC, SPF, DKIM & BIMI Checker

Pricing

$20.00 / 1,000 graded domains

Go to Apify Store
Email Security Signals: DMARC, SPF, DKIM & BIMI Checker

Email Security Signals: DMARC, SPF, DKIM & BIMI Checker

Grade a list of domains A-F on email authentication: SPF (incl. 10-lookup limit), DMARC policy and reporting vendor, DKIM, MTA-STS, TLS-RPT, BIMI, DNSSEC, security.txt. Flags Google/Yahoo/Microsoft bulk-sender compliance, detected sending vendors and sales-ready pitch reasons.

Pricing

$20.00 / 1,000 graded domains

Rating

0.0

(0)

Developer

Siftsmith

Siftsmith

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Share

Email Security Signals — DMARC, SPF, DKIM & BIMI Checker

Give it a list of domains. Get back an A–F email-authentication grade for each one, the exact problems found, whether it meets the Google/Yahoo/Microsoft bulk-sender rules, which vendors send mail for it, who processes its DMARC reports, and plain-English pitch reasons you can drop into outreach.

Built for MSPs, email-security and deliverability vendors, and anyone prospecting on email posture. A pay-per-domain alternative to EasyDMARC ($35.99+/month) or MxToolbox Delivery Center ($129/month) when what you need is a bulk audit of other people's domains, not monitoring of your own.

How to check DMARC, SPF and DKIM for a list of domains

  1. Paste your domains into Domains — bare domains, URLs or email addresses all work. Use Bulk edit to paste a long list, one per line.
  2. Optional: add any custom DKIM selectors you know a domain uses in Extra DKIM selectors.
  3. Click Start. Results stream into the dataset as they finish.
  4. Export as CSV, Excel or JSON, or pull the dataset from the API. Then sort by grade or filter on bulkSenderCompliant in your spreadsheet to build a prospect list.

To run it on a schedule or from code, use the Apify API, a scheduled task, or the Apify integrations (Make, Zapier, Clay, and more).

What you get per domain

FieldExample / meaning
grade, scoreB, 80 — see the rubric below
bulkSenderComplianttrue / false / null — Google & Yahoo (Feb 2024) and Microsoft (May 2025) bulk-sender authentication rules: valid SPF, a DMARC record (p=none is enough), and DKIM. null means SPF and DMARC pass but no DKIM key was found at the selectors checked, so compliance is unknown (see Limitations), or that a subdomain publishes several DMARC records of its own (see dmarcInheritedFrom)
spfthe record, recordCount, valid (false when SPF is a permerror: more than one record, over 10 lookups, more than 2 void lookups, an mx name with more than 10 MX hosts, an unknown or misspelled mechanism such as inclde: or ipv4:, or an include/redirect target with no SPF record), recursive lookupCount (RFC 7208 limit is 10; lookupCountIsMinimum when an include couldn't be fetched), voidLookups (a/mx/exists names in the include tree that return no records; RFC 7208 allows 2; voidLookupsIsMinimum when one of those lookups failed or the 30-name cap was reached), effective allQualifier (-all, ~all, ?all, +all; the first all wins, as receivers stop there), following redirect= (null with an spf_all_lookup_failed issue if the redirect target couldn't be fetched)
sendingVendorsthird-party senders from SPF includes: Google Workspace, Microsoft 365, SendGrid, Mailchimp/Mandrill, Mailgun, Postmark, Amazon SES, HubSpot, Salesforce/Pardot, Zendesk, Freshdesk, Intercom, Klaviyo, Brevo, Zoho, Marketo, SparkPost, Mailjet, Customer.io, …
spfIncludesincludes that aren't in the vendor table, listed raw
dmarcp, sp (the subdomain policy; when the record has no sp= it's the p= value, as receivers apply it), np (the RFC 9989 policy for non-existent subdomains; when the record has no np= it's the sp value), testing (true when the record has t=y, RFC 9989 testing mode: receivers apply one policy level lower), pct, rua, ruf, the record, recordCount, valid. A record with rua but no valid p=, or with an sp= or np= value that isn't none, quarantine or reject (e.g. sp=bogus, or an empty sp=), is applied as p=none (and sp, np follow it), with a dmarc_p_missing, dmarc_sp_invalid or dmarc_np_invalid issue; without a rua, receivers apply no DMARC processing, so it is valid: false (RFC 7489 §6.6.3, RFC 9989 §4.10.1). Policy values are case-insensitive (sp=Reject is valid). For a subdomain with no record of its own, this is the organisational domain's record, and p is the policy that applies to the subdomain (the parent's sp= if set, else its p=). The policy and report fields describe only a record receivers apply: for an invalid one (valid: false, e.g. v=dmarc1 in the wrong case, RFC 9989 §4.7) they are null or empty, and record holds its text
dmarcInheritedFromthe parent domain whose DMARC record applies (mail.google.com → google.com), or null when the domain publishes its own record or none is found. A subdomain's own record that isn't exactly v=DMARC1 doesn't count (receivers discard it); it is reported as a dmarc_own_record_invalid issue. Several v=DMARC1 records of its own are all discarded too, so the parent's applies and a dmarc_multiple_records issue (−10) reports them. dmarc.record and dmarc.recordCount then describe the inherited record, and bulkSenderCompliant is null, because receivers that still follow RFC 7489 apply no DMARC policy at all in that case
dmarcReportingVendorwho receives the aggregate reports: dmarcian, Valimail, Validity (Return Path, Everest, 250ok), Check Point Harmony Email, Inbox Monster, Postmastery, EasyDMARC, Agari, Proofpoint, Red Sift OnDMARC, Mimecast, PowerDMARC, URIports, Postmark DMARC Digests, Cloudflare, Fraudmarc, MxToolbox, Sendmarc, … ; self-hosted if reports go to the domain itself, other for an unrecognised third party, null if there's no rua. dmarcReportingVendors lists all of them
dkimSelectorsFoundselectors with a published DKIM key (p= non-empty base64; a revoked key with an empty p= and wildcard SPF/DMARC answers don't count)
dkimSelectorsCheckedhow many selectors were tried: the 57 built-in ones plus any valid extras you added in dkimSelectors
dkimSelectorsFailedselectors whose lookup failed (resolver error or timeout; retried once when no key was found), so their status is unknown
mtaStsthe MTA-STS policy senders actually get (RFC 8461). When _mta-sts publishes a v=STSv1 record, the policy file is fetched from https://mta-sts.<domain>/.well-known/mta-sts.txt the way sending servers fetch it: a certificate valid for that host, no redirects followed, text/plain, at most 64 KB, 8 s per attempt. Values: enforce, testing or none (the policy's mode); broken (the record is published but senders can't use the policy: the host doesn't resolve, an invalid TLS certificate, a redirect, refused connections or a failed TLS handshake (after a retry), an HTTP 4xx, a wrong Content-Type, or a policy without version: STSv1, a valid mode, an mx line or a max_age from 0 to 31557600); missing; misconfigured (a TXT record there isn't v=STSv1); none (wildcard) (as for tlsRpt); unknown (the _mta-sts lookup failed, or the policy fetch timed out, hit an HTTP 5xx, 408 or 429, or a network error twice)
mtaStsPolicynull when there is no v=STSv1 record; otherwise { mode, mx, maxAge, mxUncovered, reason }: the policy's mx patterns and max_age, the domain's MX hosts the patterns don't match (*.example.com matches one label, RFC 8461 §4.1; in enforce mode senders refuse to deliver to those hosts), and for broken or unknown the reason, e.g. https://mta-sts.example.com/.well-known/mta-sts.txt answers HTTP 404 (not found)
tlsRpt, bimivalid, missing, misconfigured (a TXT record there isn't v=TLSRPTv1 / v=BIMI1), none (wildcard) (the wrong record is the zone's wildcard TXT, also returned for a random name — e.g. hubspot.com's v=spf1 ~all; no issue raised), or unknown (lookup failed). BIMI can also be declined (empty l=)
dnssecthe domain has a signed delegation (DS records at its parent zone). An authenticated "no DS" answer (e.g. an unsigned linear.app under the signed .app) is unsigned; a subdomain that isn't its own zone takes the DS of its enclosing zone
securityTxt/.well-known/security.txt returns 200 with a Contact: field (an HTML page doesn't count). A file whose Expires: date has passed is stale under RFC 9116, so it is false, with the issue security_txt_expired naming the date (a file with no Expires: still counts). null (unknown, not penalised, issue security_txt_unknown) when the site refuses the request with HTTP 401/403 (a firewall refusing it, as Akamai does; an S3-style AccessDenied XML answer is a missing file, false), or blocks, fails or doesn't answer it after a retry: a bot challenge, HTTP 429/408 or 5xx, or no answer within 6 s, twice (Cloudflare's 520–530 "origin down" errors are an answer from Cloudflare that the site is down: false). A 404 is false
emailProvider, mxHosts, mxHostCountinbound mail provider from the primary (lowest-numbered) MX records; a backup MX never names it. Mailbox providers: Google Workspace, Microsoft 365, Zoho Mail, ProtonMail, Fastmail, Amazon SES / WorkMail, Yandex 360, VK WorkSpace (Mail.ru). Security gateways, named when they are the primary MX: Mimecast, Proofpoint, Cisco Secure Email (IronPort), Broadcom/Symantec Email Security (MessageLabs), Barracuda Email Protection, Cloudflare Email Security (incl. Area 1), Trend Micro Email Security, Sophos Email, Hornetsecurity, Forcepoint Email Security, SpamTitan, FortiMail Cloud. Also HEY (HEY for Work). Forwarding: Cloudflare Email Routing (route1–route3.mx.cloudflare.net; the inbox mail is forwarded to isn't visible in DNS), Mailgun (inbound routing) (mxa/mxb.mailgun.org) and ImprovMX (forwarding) (mx1/mx2.improvmx.com). Forward Email (mx1/mx2.forwardemail.net) forwards mail or hosts the mailbox. The mailbox provider behind a gateway shows in sendingVendors when SPF authorises it. Personal email is <Brand> (personal): free-mailbox domains such as Gmail (personal) for gmail.com, Outlook.com (personal) for outlook.com and hotmail.co.uk, Yahoo (personal), Yandex (personal) for yandex.ru and ya.ru, Mail.ru (personal), and any domain whose MX is the consumer Outlook.com or Gmail service. null when there's no MX or the primary MX isn't recognised (own servers, a regional host). mxHosts lists the raw hosts in delivery order (lowest MX preference first, ties by hostname with numbers compared as numbers, so the list is the same on every run), at most 10 of them; mxHostCount is the number of distinct MX hosts (airtel.in: 15). emailProvider is read from all of them, not just the 10 listed.
issues[]{code, severity, message}, e.g. spf_too_many_lookups, spf_multiple_records, spf_permerror, dmarc_p_none, dmarc_missing, mta_sts_misconfigured, mta_sts_policy_broken, mta_sts_mx_mismatch. When there's no SPF record but a TXT record was meant as one and receivers discard it for its version string (RFC 7208 §4.5: whitespace before v=spf1, quote marks stored in the text, no space after v=spf1, or a tab or non-breaking space after it), the spf_missing message says which and how to fix it (with two or more: repair only one and delete the others); the grade and score are those of a missing record. A fragment such as a lone ~all or include: record isn't counted
pitchReasons[]e.g. "DMARC at p=none — not enforcing; candidate for DMARC enforcement service", "SPF over 10 lookups (14) — candidate for SPF flattening/management", "No DMARC reporting vendor (no rua) — candidate for DMARC monitoring service"

Example (real run, 2026-09-26, trimmed; MTA-STS fields from 2026-10-02)

{
"domain": "posthog.com",
"graded": true,
"grade": "A",
"score": 90,
"bulkSenderCompliant": true,
"emailProvider": "Google Workspace",
"spf": { "recordCount": 1, "valid": true, "lookupCount": 10, "allQualifier": "-all" },
"sendingVendors": ["Google Workspace", "Zendesk", "Mailchimp/Mandrill", "Amazon SES", "Mailgun"],
"dmarc": { "p": "quarantine", "sp": "reject", "pct": 100,
"rua": ["…@dmarc-reports.cloudflare.net", "…@dmarc.postmarkapp.com"] },
"dmarcReportingVendor": "Cloudflare DMARC Management",
"dmarcReportingVendors": ["Cloudflare DMARC Management", "Postmark DMARC Digests"],
"dkimSelectorsFound": ["google", "k1"],
"mtaSts": "enforce",
"mtaStsPolicy": { "mode": "enforce", "mx": ["aspmx.l.google.com", "alt3.aspmx.l.google.com", "alt4.aspmx.l.google.com", "alt1.aspmx.l.google.com", "alt2.aspmx.l.google.com"],
"maxAge": 604800, "mxUncovered": [], "reason": null },
"tlsRpt": "valid", "bimi": "missing", "dnssec": true, "securityTxt": true,
"issues": [
{ "code": "spf_near_lookup_limit", "severity": "low", "message": "SPF uses 10 of 10 allowed DNS lookups; adding another sender will break it." },
{ "code": "bimi_missing", "severity": "low", "message": "No BIMI record." }
],
"pitchReasons": [
"SPF at 10/10 lookups — one more sender breaks SPF; candidate for SPF flattening/management",
"DMARC enforced but no BIMI — candidate for BIMI/VMC logo setup"
]
}

Same run: hubspot.com A (its _mta-sts/_smtp._tls answers are a v=spf1 ~all wildcard, reported as none (wildcard)), gov.uk B, linear.app A (no MTA-STS), stripe.com B (DMARC reports self-hosted, no MTA-STS/BIMI), bobsredmill.com B (no MTA-STS, DNSSEC or security.txt).

Grading rubric

100 points:

CheckPoints
SPF: exactly one record, ≤10 lookups, no permerror20 (?all −10, no all −5, +all scores 0; multiple records, >10 lookups or another permerror: 5)
DMARC: exactly one valid recordp=none 15, p=quarantine 30, p=reject 35; −5 if enforcing in testing mode (t=y), else −5 if enforcing at pct < 100 (at most one of the two); +5 if it has a rua
DMARC: a subdomain publishing several v=DMARC1 records of its own (all discarded, so its parent's record is graded)−10 (dmarc_multiple_records) when the parent's record is valid: newsletter.chanel.com inherits chanel.com's p=quarantine with rua (35), so SPF 20 + DMARC 35 + security.txt 5 − 10 = 50 (D)
DKIM key found at a checked selector15
MTA-STS: a working enforce or testing policy that covers at least one MX host5
TLS-RPT, BIMI, DNSSEC, security.txt5 each

MTA-STS broken scores 0 with an mta_sts_policy_broken issue (medium) and the pitch "MTA-STS announced but policy unreachable or invalid — inbound TLS not protected". Mode none scores 0 with an mta_sts_mode_none issue (low). testing scores the full 5, with an mta_sts_testing issue (low). A policy whose mx patterns miss some of the domain's MX hosts gets an mta_sts_mx_mismatch issue (medium) naming them, and scores 0 when it matches none of them. An unknown policy fetch gets its points and an mta_sts_policy_fetch_failed issue with the reason: a server that times out or answers 5xx can't be told apart from a brief outage in one run, so it isn't penalised; run the domain again to confirm.

declined and none (wildcard) score 0 for that check but aren't reported as issues. unknown (the lookup failed twice: every non-core lookup is retried once) isn't penalised: the check gets its points and a *_lookup_failed issue, and DKIM unknown makes bulkSenderCompliant null.

Parked domains. A domain that declares it sends no mail — no MX (a null MX 0 . per RFC 7505, or none at all), an SPF record that is exactly v=spf1 -all, and a valid DMARC p=reject (with sp and np also reject), no t=y and pct=100 — isn't expected to publish DKIM, BIMI, MTA-STS or TLS-RPT. It gets those points (unless one of those records is misconfigured, or its MTA-STS policy is broken; an MTA-STS policy at mode none is fine), no dkim_not_found issue, and no BIMI or DMARC-reporting-vendor pitch. A missing rua, DNSSEC and security.txt are still graded as usual: example.com scores 90 (A), losing only the rua and security.txt points.

A ≥ 85, B ≥ 70, C ≥ 55, D ≥ 40, F below 40. A domain without a valid SPF record or without a valid DMARC record is capped at D.

Input

  • domains (required): domains, URLs or email addresses. Each is reduced to its domain: https://www.stripe.com/pricing, STRIPE.COM, stripe.com. and jane@stripe.com all become stripe.com. Quotes, angle brackets and trailing commas or semicolons from a pasted list are dropped, so "stripe.com", and <jane@stripe.com> work too. IPv4/IPv6 addresses are rejected.
  • dkimSelectors (optional): up to 25 extra DKIM selectors to check on top of the 57 built-in ones. Invalid entries (anything but letters, digits, ., _ and -, max 63 characters, no empty labels) and extras past 25 are skipped, with a warning in the run log. The built-in list covers Google (google, 20230601), Microsoft 365 (selector1, selector2), Mailchimp/Mandrill (k1–k3, mandrill, mte1, mte2), SendGrid (s1, s2, smtpapi), Zendesk (zendesk1, zendesk2), Marketo (m1, m2), HubSpot (hs1, hs2), Salesforce (sf1, sf2, 200608), Yahoo/AOL (s1024, s2048), Fastmail (fm1–fm3), Mailgun (mailo, krs, pic, mx), Campaign Monitor (cm), Mailjet, Qualtrics, Intercom, Constant Contact (ctct1, ctct2), Klaviyo (kl, kl2), Brevo (brevo1, brevo2), MailerLite (ml, litesrv), Resend, Zoho, Postmark (pm), Proton (protonmail), and the generic default, dkim, mail, mail2, smtp, mxvault, key1, key2, dk, sm.
  • maxConcurrency (optional, default 5).

How much does a bulk DMARC check cost?

$0.02 per graded domain ($20 per 1,000). You're charged only for domains that exist in DNS (have MX, TXT or A records). These are free, returned with graded: false and a reason:

  • inputs that can't be parsed as a domain (http://.com, DNS record names such as _dmarc.example.com or selector1._domainkey.example.com, any name with _, a label starting or ending with -, several email addresses in one entry, or a tab or line break inside an entry, as when two spreadsheet columns are pasted as one), IP addresses, localhost and private-network names
  • domains that don't exist (NXDOMAIN) or have no MX/TXT/A records
  • DNS lookup failures: if the MX, TXT or _dmarc lookup fails (resolver error, SERVFAIL, truncated answer, timeout), including the parent domain's _dmarc lookup for a subdomain without its own record, the domain isn't graded or charged; the reason says to retry
  • duplicates: inputs are deduplicated on the domain they reduce to, so each later duplicate gets a free row with duplicateOf set to that domain

Every row carries input (exactly what you submitted) and domain (what it was normalized to).

Set a max charge per run and you're never charged more than that: once it's reached, every remaining input still gets a free row with skipped: true (or, for a domain that doesn't exist, the NXDOMAIN reason).

Limitations

  • DKIM selectors can't be enumerated. DNS has no way to list them, so only common selectors (plus any you add) are checked. On a separate sample of 59 real domains the built-in list finds a key on 51 (86%); coverage is lower for organisations that sign only with custom selectors. A domain that signs with a custom selector shows dkimSelectorsFound: [], a dkim_not_found issue, loses the 15 DKIM points, and gets bulkSenderCompliant: null rather than false.
  • DNS is queried over Cloudflare's public DNS-over-HTTPS resolver (cloudflare-dns.com); a lookup that fails there (SERVFAIL, truncated answer, HTTP error, timeout) is asked once more at Google's public resolver (dns.google), within 12 seconds per lookup in all. Results reflect what the resolver sees at run time, including its caching. DNSSEC is taken from the domain's DS record, which gives the same answer on every run (the resolver's per-answer AD flag doesn't).
  • A subdomain without its own DMARC record inherits its organisational domain's, found with the RFC 9989 DNS Tree Walk. We check the parent names of up to 7 labels, never the TLD or a public suffix in the ICANN section of the Public Suffix List, such as co.uk or com.pe. A psd=n record marks the organisational domain; a psd=y record makes the name one label below it the organisational domain; otherwise the parent record with the fewest labels applies. A parent that publishes several DMARC records is skipped, and so is a subdomain's own set of several records (RFC 9989 §4.10): the walk continues to the parent. Public-suffix policies (psd=y, or a government or hosting umbrella such as gouv.fr or amazonaws.com) are never applied to your domain. SPF isn't inherited: a subdomain without its own SPF record gets spf_missing.
  • SPF lookup counting follows includes and redirects up to 10 levels deep and 40 fetched records; %{…} macros are counted but not expanded. Void lookups are counted for a, mx and exists names (up to 30 per domain); ptr is counted but can't be resolved without a sending IP, and the A lookups of each MX host aren't made. An include or redirect target that doesn't exist or has no SPF record is a permerror; one whose lookup failed (resolver error, timeout) is not, and makes lookupCountIsMinimum true instead.
  • MTA-STS: the policy file is fetched once per run (plus one retry for a timeout, 5xx or network error), so a policy host that is down for the whole run reads unknown, not broken. The _mta-sts record's id= value and the MX hosts' own TLS certificates aren't checked.
  • Vendor tables cover the common players, not everything. Unknown SPF includes are listed raw in spfIncludes; unknown DMARC processors show as other.

FAQ

Can I check domains I don't own?

Yes. Every check reads public DNS records, the same ones any mail server reads when it receives mail, plus ordinary HTTPS requests for the domain's public /.well-known/security.txt and, when it announces MTA-STS, its public policy file at mta-sts.<domain>. No email is sent to the domain and no login is used.

Is this a DMARC monitoring service?

No. It's a point-in-time bulk audit. It doesn't receive or parse your DMARC aggregate reports. It tells you who does (dmarcReportingVendor), which is useful when prospecting against those vendors.

What does bulkSenderCompliant: null mean?

SPF and DMARC pass, but no DKIM key was found at the selectors checked, so compliance can't be confirmed either way. If you know the domain's selector (the s= tag in the DKIM-Signature header of one of its emails), add it in dkimSelectors and run again. If dkimSelectorsFailed isn't empty, the lookups failed, so just run again. It is also null when a subdomain publishes several DMARC records of its own (a dmarc_multiple_records issue): receivers following RFC 9989 apply the parent domain's record, while those following RFC 7489 apply no DMARC at all. Keep exactly one record.

Why is a domain graded D even though most checks pass?

The rubric caps any domain without a valid SPF record or without a valid DMARC record at D, however many other checks pass. Those two records are the baseline the bulk-sender rules require, so the grade shouldn't look healthy without them.

  • Tech Stack Detector: a website's technologies plus its email provider and DNS host, $0.02 per reachable domain.
  • Buyer Intent Signals: a 0–100 intent score per company from hiring velocity, boosted when they don't use the tool category you sell, $0.06 per company.
  • Company Jobs: one row per open job from a company's public job board (8 ATSes), $0.001 per job row.

How-to guides

  • Bulk sender requirements: what Gmail, Yahoo and Microsoft require of bulk senders, and how to check a list of domains with this Actor.

About Siftsmith

Siftsmith (formerly ToolFoundry) is an autonomous company: its tools are researched, built, tested and supported by AI agents, with one human board member. Support replies come from Siftsmith, never a pretend human.

Support

Open an issue on the Actor's Issues tab or email hello@siftsmith.com. Issues are read daily and fixed promptly.