# DMARC, SPF & DKIM Checker: Bulk Email Domain Health Audit (`signalloom/email-domain-health-audit`) Actor

Bulk DMARC, SPF and DKIM checker for email deliverability. Input domains or emails; get an A-F grade per domain, MX and mail provider, SPF 10-lookup check, DMARC policy, MTA-STS, BIMI, SSL and domain expiry, Gmail/Yahoo bulk-sender status, plain-English issues and copy-paste DNS fixes.

- **URL**: https://apify.com/signalloom/email-domain-health-audit.md
- **Developed by:** [Signalloom](https://apify.com/signalloom) (community)
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 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

## DMARC, SPF & DKIM Checker: Bulk Email Domain Health Audit

Check hundreds of domains for **email deliverability and domain health** in one run. For every domain you get an **A–F grade**, a **0–100 score**, a **plain-English list of problems** and **copy-paste DNS records that fix them**.

Since 2024, **Gmail and Yahoo require SPF, DKIM and DMARC** from bulk senders. Mail from domains that fail is sent to spam or rejected. This Actor shows which domains pass and exactly what to change.

Checks per domain:

- **MX**: mail servers present, null MX, mail provider (Google Workspace, Microsoft 365, Zoho…)
- **SPF**: present, one record only, valid syntax, **DNS lookup count (limit 10)** following every `include:` and `redirect=`, the `all` ending (`-all`, `~all`, `?all`, `+all`), and which sending services it allows (SendGrid, Mailchimp, HubSpot…)
- **DMARC**: present, policy (`none`, `quarantine`, `reject`), subdomain policy, `pct`, `rua`/`ruf` report addresses, alignment, and inheritance from the parent domain
- **DKIM**: your own selectors plus about 30 common ones (Google, Microsoft 365, Mailchimp, SendGrid, Zoho, Proton…), with the **real key size** (1024 vs 2048-bit) and revoked keys
- **MTA-STS**: DNS record, the policy file (mode, max age), and whether it covers every MX host
- **TLS-RPT** and **BIMI** records
- **SSL certificate** of the website: days left, issuer, trusted, hostname match
- **DNSSEC**: whether the domain is signed
- **Domain age and expiry** from the registry's public RDAP service (registrar, registration and expiry dates)

**Price: $0.003 per domain audited** ($3 per 1,000). Invalid and non-existent domains are free. The prefilled example (5 domains) finishes in under 10 seconds.

### Use this when…

- you run **cold email** and need to check that every sending domain has SPF, DKIM and DMARC before a campaign
- you're an **agency or MSP** auditing email security for many clients or prospects at once
- you need a **bulk DMARC checker** or **SPF checker** that also tells you how to fix what it finds
- you want to catch an **SPF record over the 10-lookup limit**, which quietly breaks SPF
- you need to find domains whose **SSL certificate or registration is about to expire**
- you want to know if a list of domains meets the **Google and Yahoo bulk sender requirements**

### For AI agents

- **Run** `signalloom/email-domain-health-audit` (Apify MCP server `call-actor`, or the Apify API) with input like `{"domains": ["github.com", "python.org"]}`
- **Returns** one JSON row per domain in the default dataset: `grade`, `score`, `bulkSenderReady`, `mailProvider`, `spf`, `dkim`, `dmarc`, `mx`, `issues` (plain English) and `fixRecords` (copy-paste DNS fixes).
- **Typical cost:** 2 domains = **$0.006** ($0.003 per domain).

### Who is it for?

- **Cold-email agencies and lead-gen teams**: check every inbox domain before warming it up
- **MSPs and IT teams**: a client-ready email security report for every domain you manage
- **Email marketers and deliverability consultants**: find out why mail lands in spam
- **Sales teams selling email security, DMARC or IT services**: grade prospect domains and lead with a specific, fixable problem

### How to use it

1. Add domains or email addresses, one per line: `example.com`, `sales@example.com`, `https://www.example.com`. The domain is taken from each entry, and duplicates are audited once.
2. Optional: add your **DKIM selectors** (the `s=` value in a sent email's `DKIM-Signature` header). DNS can't list selectors, so this makes the DKIM check certain.
3. Click **Start**. Sort by `score`, filter by `grade` or `issueCodes`, and copy the records in `fixRecords`.

**Recipe: prospect list for an IT or email-security service.** Add 1,000 prospect domains, then filter to `issueCodes` containing `NO_DMARC` or `SPF_TOO_MANY_LOOKUPS`. Each one is a concrete, fixable reason to get in touch.

### Output

One record per domain. A real record from the prefilled run (python.org; shortened: some fields, keys and issues left out):

```json
{
  "domain": "python.org",
  "grade": "B",
  "score": 81,
  "bulkSenderReady": true,
  "receivesMail": true,
  "mailProvider": "Other / self-hosted",
  "sendingServices": ["Google Workspace", "Mailgun"],
  "mx": { "present": true, "nullMx": false, "records": [{ "priority": 50, "host": "mail.python.org" }] },
  "spf": {
    "present": true,
    "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",
    "validSyntax": true,
    "allQualifier": "softfail",
    "dnsLookups": 8,
    "lookupLimitExceeded": false
  },
  "dmarc": {
    "present": true,
    "record": "v=DMARC1; p=none; pct=100; rua=mailto:re+shb8ybr70a3@dmarc.postmarkapp.com; sp=none; aspf=r;",
    "policy": "none",
    "effectivePolicy": "none",
    "pct": 100,
    "rua": ["mailto:re+shb8ybr70a3@dmarc.postmarkapp.com"]
  },
  "dkim": {
    "found": true,
    "selectorsChecked": 30,
    "keys": [
      { "selector": "google", "keyType": "rsa", "revoked": false, "keyBits": 1024, "record": "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQ..." },
      { "selector": "k2", "keyType": "rsa", "revoked": false, "keyBits": 2048, "record": "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQ..." }
    ]
  },
  "mtaSts": { "present": false },
  "tlsRpt": { "present": false },
  "bimi": { "present": false },
  "dnssec": { "signed": false, "validated": false, "checkedAt": "python.org" },
  "webCertificate": { "reachable": true, "host": "python.org", "validTo": "2027-02-14T13:03:45.000Z", "daysLeft": 94, "issuer": "GlobalSign nv-sa", "trusted": true, "hostnameMatch": true },
  "registration": { "domain": "python.org", "available": true, "registeredAt": "1995-03-27T05:00:00.545Z", "expiresAt": "2033-03-28T05:00:00Z", "domainAgeDays": 11552, "daysUntilExpiry": 2328, "registrar": "Gandi SAS" },
  "issues": [
    { "severity": "warning", "code": "DMARC_POLICY_NONE", "message": "DMARC policy is p=none (monitor only): spoofed mail is still delivered. Move to p=quarantine, then p=reject, once reports show your real senders pass." },
    { "severity": "notice", "code": "DKIM_WEAK_KEY", "message": "DKIM keys \"google\" (1024-bit), \"mailjet\" (1024-bit) use RSA keys shorter than the recommended 2048 bits. Rotate them at your email provider, or delete them if no longer used." },
    { "severity": "notice", "code": "NO_MTA_STS", "message": "No MTA-STS: other mail servers aren't told to require encrypted (TLS) delivery to you." }
  ],
  "fixRecords": [
    { "type": "TXT", "host": "_dmarc.python.org", "value": "v=DMARC1; p=quarantine; rua=mailto:re+shb8ybr70a3@dmarc.postmarkapp.com; pct=100", "note": "Next step after reviewing reports: quarantine mail that fails DMARC." },
    { "type": "TXT", "host": "_smtp._tls.python.org", "value": "v=TLSRPTv1; rua=mailto:tls-reports@python.org", "note": "Create the report mailbox first." }
  ]
}
```

A domain that sends no mail and has no protection gets records that lock it down. Real record from the same run (neverssl.com, grade **F**, score 48):

```json
"fixRecords": [
  { "type": "MX", "host": "neverssl.com", "value": "0 .", "note": "Null MX (RFC 7505), only if this domain should never receive email. Otherwise add your mail provider's MX records." },
  { "type": "TXT", "host": "neverssl.com", "value": "v=spf1 -all", "note": "This domain has no mail servers, so this record says no server may send mail as it." },
  { "type": "TXT", "host": "_dmarc.neverssl.com", "value": "v=DMARC1; p=reject; adkim=s; aspf=s", "note": "This domain sends no mail, so a strict reject policy is safe." }
]
```

**Run summary.** The `OUTPUT` record in the key-value store holds the grade distribution, how many domains are bulk-sender ready, the most common issues, and the entries that were skipped. From the prefilled run: `{"A":1,"B":2,"C":0,"D":1,"F":1}`, 3 of 5 bulk-sender ready.

#### Score and grade

The score starts at 100 and loses points per issue, for example: no DMARC −25, no SPF −20, SPF `+all` −20, SPF over 10 lookups −15, DMARC `p=none` −10, no DKIM found −10, no MTA-STS −2. Grades: A ≥ 90, B ≥ 80, C ≥ 70, D ≥ 60, F below. A domain without SPF or DMARC (both required by Gmail and Yahoo for bulk senders) can't grade better than D.

`bulkSenderReady` is `true` when SPF is valid, a DMARC record exists (any policy) and at least one DKIM key was found.

### Method

- **DNS only, read-only.** All records are fetched over DNS-over-HTTPS from Cloudflare's public resolver (1.1.1.1), with Google Public DNS as a fallback. Both are documented public APIs. A missing SPF or DMARC record is confirmed with the second resolver before it's reported, and DNS queries are limited to 10 at a time.
- **No email is sent and no mailbox is probed.** The optional MX certificate check opens port 25 and sends only `EHLO`, `STARTTLS` and `QUIT`.
- **SSL certificates** are read with a normal TLS handshake on port 443.
- **MTA-STS policy** is fetched from the standard `https://mta-sts.<domain>/.well-known/mta-sts.txt` address.
- **Domain age and expiry** come from the registry's RDAP service (RFC 9083), found through IANA's public RDAP bootstrap file. Only dates, statuses and the registrar's name are read. Registrant contact details are never read or output.

### Pricing

- **$0.003 per domain** audited
- Invalid entries and domains that don't exist in DNS: **free**

### Data handling

- Only public DNS records, public TLS certificates, the public MTA-STS policy file and public registry data are read.
- No personal data is collected. Report addresses (`rua`) are shown exactly as the domain publishes them in DNS.

### Limitations

- **DKIM selectors can't be listed from DNS.** The Actor tries about 30 common selectors. If your provider uses a custom or date-based selector (for example Google's newer `20230601`-style ones), add it in **DKIM selectors**, or `DKIM_NOT_FOUND` may be a false alarm.
- **MX certificate check:** many cloud networks, including Apify's, block outbound port 25, so this optional check usually reports "not reachable". It's off by default.
- **RDAP coverage:** some country domains (and `.edu`, `.us`) have no public RDAP service listed by IANA, so `registration.available` is `false` for them.
- Subdomains are mapped to their parent domain with a built-in rule (e.g. `mail.example.co.uk` → `example.co.uk`), not the full public suffix list.
- It checks configuration, not reputation: no blacklist lookups and no inbox-placement tests.

### FAQ

**How do I check a domain's SPF, DKIM and DMARC records?**

Add the domain (or any email address at it) and run. You get each record, whether it is valid, and an A–F grade, for one domain or thousands.

**Is my domain ready for the Gmail and Yahoo bulk-sender rules?**

`bulkSenderReady` is true when SPF, DKIM and DMARC are in place and aligned. Missing pieces appear in `issues` with the DNS record to add in `fixRecords`.

**Why are my emails going to spam?**

Common DNS causes are a missing or `p=none` DMARC policy, an SPF record over the 10-lookup limit, or no DKIM key. The audit flags each one in plain English. It doesn't test message content or sender reputation.

**Can I monitor my domains automatically?**

Yes. Schedule a weekly run on your domain list to catch expiring certificates or domains, and DNS changes that break SPF, DKIM or DMARC.

### Feedback

Want another check added? Open an issue on the **Issues** tab.

# Actor input Schema

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

One per line, e.g. "example.com" or "sales@example.com" (the domain is taken from the email). URLs work too. Duplicates are audited once. Example: \["example.com", "sales@example.org"].

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

Your DKIM selectors, if you know them (the s= value in a sent email's DKIM-Signature header, e.g. "google" or "selector1"). DNS can't list selectors, so giving yours makes the DKIM check certain. Example: \["google", "selector1"].

## `guessDkimSelectors` (type: `boolean`):

Check about 30 selectors used by Google Workspace, Microsoft 365, Mailchimp, SendGrid, Zoho, Proton and others.

## `checkCertificates` (type: `boolean`):

Connect to the domain's website on port 443 and report certificate expiry (days left), issuer and validity.

## `checkMxCertificate` (type: `boolean`):

Connect to the first MX host on port 25 and read its STARTTLS certificate. Sends only EHLO, STARTTLS and QUIT: no mail and no address probing. Many cloud networks block port 25, so this often reports 'not reachable'.

## `checkRegistration` (type: `boolean`):

Look up registration and expiry dates and the registrar from the registry's public RDAP service. Registrant contact details are never read.

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

Check whether the domain is signed with DNSSEC (DS record at the parent zone).

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

Look up the BIMI record (brand logo shown in Gmail, Yahoo and Apple Mail).

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

How many domains to audit at the same time. DNS queries are additionally limited to 10 at a time, to stay polite to the public resolvers.

## Actor input object example

```json
{
  "domains": [
    "apify.com",
    "github.com",
    "python.org",
    "openbsd.org",
    "neverssl.com"
  ],
  "dkimSelectors": [],
  "guessDkimSelectors": true,
  "checkCertificates": true,
  "checkMxCertificate": false,
  "checkRegistration": true,
  "checkDnssec": true,
  "checkBimi": true,
  "maxConcurrency": 5
}
```

# 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": [
        "apify.com",
        "github.com",
        "python.org",
        "openbsd.org",
        "neverssl.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("signalloom/email-domain-health-audit").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": [
        "apify.com",
        "github.com",
        "python.org",
        "openbsd.org",
        "neverssl.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("signalloom/email-domain-health-audit").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": [
    "apify.com",
    "github.com",
    "python.org",
    "openbsd.org",
    "neverssl.com"
  ]
}' |
apify call signalloom/email-domain-health-audit --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,signalloom/email-domain-health-audit"
        }
    }
}
```

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/vTnNrsSnJqyPZ63Un/builds/KWmYv86z8PQMVAjNF/openapi.json
