# Bulk Email Spoofing Checker (SPF, DMARC, DKIM, MX) (`securityzenkai/email-spoof-checker`) Actor

Check a list of domains for email spoofing risk. Passive DNS lookups of SPF, DMARC, DKIM and MX; returns a yes / risk / no verdict and the single most important fix per domain.

- **URL**: https://apify.com/securityzenkai/email-spoof-checker.md
- **Developed by:** [Joshua Pole](https://apify.com/securityzenkai) (community)
- **Categories:** Developer tools, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$5.00 / 1,000 domain checkeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Bulk Email Spoofing Checker — SPF, DMARC, DKIM & MX

**Can someone send email pretending to be your domain?** Paste a list of domains and get one clear answer per domain — `yes`, `risk` or `no` — plus the **one DNS fix that closes the gap**, ready to copy-paste.

✅ Spoofable domains in seconds  ·  ✅ One prioritised fix per domain  ·  ✅ MTA-STS, BIMI, DNSSEC & CAA hardening score  ·  ✅ 10,000 domains per run  ·  ✅ 100% passive — DNS lookups only  ·  ✅ No API keys

***

### ⚡ See it in 5 seconds

```json
// input
{ "domains": ["acme-shop.com"] }
```

```json
// output
{
  "domain": "acme-shop.com",
  "spoofable": "yes",
  "main_fix": "Move DMARC from p=none to p=quarantine, then p=reject (_dmarc.acme-shop.com).",
  "issues": ["DMARC p=none (monitoring only, no enforcement)"]
}
```

No scores to interpret, no 40-field reports to read. **A verdict and the fix.**

***

### 🎯 Who uses this

| You are | You use it to |
|---|---|
| **MSP / IT service provider** | Audit every client domain in one run and turn the `fix_first` list into a monthly report or upsell. |
| **Security consultant / pentester** | Add email-spoofing findings to an assessment in minutes — with the exact remediation per domain. |
| **Agency / web builder** | Check all domains you manage before a client's phishing incident does it for you. |
| **Sales / lead generation** | Find companies whose domain can be spoofed — a concrete, verifiable reason to start a security conversation. |
| **IT admin with many domains** | Find the forgotten parked domains with no SPF/DMARC that attackers love to abuse. |

***

### 🔍 What it checks

| Check | Details |
|---|---|
| **DMARC** | Record present and valid · policy `p=` · subdomain policy `sp=` · `pct=` · reporting address `rua=` · inheritance from the parent domain |
| **SPF** | Record present · single record · `-all` / `~all` / `?all` / `+all` · **recursive DNS-lookup count** (RFC 7208 limit of 10 → silent PermError) |
| **DKIM** | Probes ~35 common selectors: Google Workspace, Microsoft 365, Mailchimp, SendGrid, Mailgun, Zoho, Amazon SES, Proton, Fastmail … plus your own |
| **MX** | Mail servers · RFC 7505 **null MX** (domain that explicitly receives no mail) |
| **Hardening** *(optional, on by default)* | **MTA-STS** (forced encrypted delivery) · **TLS-RPT** (delivery failure reports) · **BIMI** (logo in the inbox) · **DNSSEC** (signed DNS) · **CAA** (which CA may issue certificates) → 0–100 `hardening_score` + next step |
| **Domain** | Non-existent (NXDOMAIN) and invalid domains are flagged — and **not billed** |

Input is forgiving: `https://www.acme-shop.com/contact`, `info@acme-shop.com` and `ACME-SHOP.COM` all become `acme-shop.com`. Duplicates are removed.

***

### 🚦 The verdict

| `spoofable` | Meaning | Typical cause |
|---|---|---|
| 🔴 **`yes`** | Forged mail "from" this domain gets delivered. | No DMARC · invalid DMARC · `p=none` |
| 🟠 **`risk`** | DMARC is enforced, but with a hole. | `pct<100` · `sp=none` (subdomains spoofable) · missing or broken SPF |
| 🟢 **`no`** | Protected against direct spoofing. | DMARC `quarantine`/`reject` at 100% + valid SPF |
| ⚪ **`error`** | Could not be checked — not billed. | Domain doesn't exist · invalid name · DNS timeout |

The hardening checks never change this verdict — they answer a different question: *how well-defended is this domain beyond spoofing?*

Every row also gets **`main_fix`**: the single most important change, written as an actual DNS record. Even `no` domains can get optional hardening (e.g. `quarantine → reject`, add `rua=` reporting).

#### Smart fixes, not generic advice

| Situation | `main_fix` you get |
|---|---|
| Parked domain, no mail at all | Lock it: `TXT "v=spf1 -all"` + `_dmarc TXT "v=DMARC1; p=reject"` |
| Sends mail, no SPF and no DMARC | Publish SPF with `-all` first, then DMARC with `rua=` reporting |
| DMARC `p=none` | Move to `p=quarantine`, then `p=reject` |
| Subdomain inherits a weak `sp=none` | Publish an own DMARC record on the subdomain |
| DMARC `pct=25` | Set `pct=100` so all spoofed mail is blocked |
| SPF over 10 DNS lookups | Reduce SPF below 10 lookups — it now fails silently with PermError |

***

### 📊 Example: a portfolio run

**Input** — 10 domains, mixed formats:

```json
{
  "domains": [
    "acme-shop.com", "https://www.northwind.io/", "info@contoso-bakery.nl",
    "fabrikam.net", "parked-brand.com", "mail.tailspin.org",
    "litware.com", "adatum.eu", "wingtip.co", "doesnotexist-zz9.nl"
  ]
}
```

**Dataset → Overview view:**

| domain | spoofable | SPF | DMARC | DKIM | main\_fix |
|---|---|---|---|---|---|
| acme-shop.com | 🔴 yes | `-all` | `none` | ✓ | Move DMARC from p=none to p=quarantine, then p=reject. |
| contoso-bakery.nl | 🔴 yes | – | – | – | Publish SPF … `-all`. Then: add DMARC … `p=reject; rua=mailto:…` |
| parked-brand.com | 🔴 yes | – | – | – | Domain does not send mail? Lock it: `v=spf1 -all` + `v=DMARC1; p=reject`. |
| mail.tailspin.org | 🔴 yes | – | `none` (inherited) | – | Add an own DMARC record with p=reject — it now inherits sp=none from tailspin.org. |
| northwind.io | 🟠 risk | `~all` | `quarantine` | ✓ | Remove sp=none so subdomains inherit p=quarantine. |
| fabrikam.net | 🟢 no | `-all` | `quarantine` | ✓ | Optional hardening: move DMARC to p=reject. |
| litware.com | 🟢 no | `~all` | `reject` | ✓ | None — domain is protected against direct spoofing. |
| adatum.eu | 🟢 no | `~all` | `reject` | ✓ | None — domain is protected against direct spoofing. |
| wingtip.co | 🟢 no | `-all` | `reject` | – | Optional: add rua= to DMARC to receive abuse reports. |
| doesnotexist-zz9.nl | ⚪ error | – | – | – | domain does not exist (NXDOMAIN) — *not billed* |

**Key-value store → `SUMMARY`** — your work queue, worst first:

```json
{
  "domains": 10,
  "spoofable": 4,
  "at_risk": 1,
  "protected": 4,
  "errors": 1,
  "fix_first": [
    { "domain": "acme-shop.com", "spoofable": "yes", "main_fix": "Move DMARC from p=none to p=quarantine, then p=reject (_dmarc.acme-shop.com)." },
    { "domain": "contoso-bakery.nl", "spoofable": "yes", "main_fix": "Publish SPF: … Then: Add DMARC: …" },
    { "domain": "northwind.io", "spoofable": "risk", "main_fix": "Remove sp=none from _dmarc.northwind.io so subdomains inherit p=quarantine." }
  ]
}
```

*(Example domains are fictional; the verdicts and fixes are real outputs of this Actor on real DNS configurations.)*

***

### 📥 Input

| Field | Type | Default | Description |
|---|---|---|---|
| `domains` | string\[] | — | **Required.** Domains, URLs or email addresses. Max 10,000 per run. |
| `dkimSelectors` | string\[] | `[]` | Extra DKIM selectors to probe on top of the ~35 built-in ones. |
| `nameservers` | string\[] | `1.1.1.1`, `8.8.8.8` | Public DNS resolvers to use. |
| `concurrency` | integer | `10` | Domains checked in parallel (1–50). |
| `extendedChecks` | boolean | `true` | Add MTA-STS, TLS-RPT, BIMI, DNSSEC and CAA checks + hardening score. |

### 📤 Output fields

| Field | Description |
|---|---|
| `domain` | Normalised domain name. |
| `spoofable` | `yes` · `risk` · `no` · `error` |
| `main_fix` | The single most important fix, as a concrete DNS record. |
| `issues` | Every problem found, in plain English. |
| `mx` / `null_mx` | Mail servers / whether the domain declares it receives no mail. |
| `spf_record` · `spf_all` · `spf_lookups` | Raw SPF record · its `all` qualifier · recursive DNS-lookup count. |
| `dmarc_record` · `dmarc_policy` · `dmarc_subdomain_policy` · `dmarc_pct` · `dmarc_rua` | Raw DMARC record and its parsed tags. |
| `dmarc_inherited_from` | Parent domain whose DMARC applies, if the domain has none of its own. |
| `dkim_selectors_found` | DKIM selectors with a published key. |
| `hardening_score` | 0–100: MTA-STS 25 · DNSSEC 25 · CAA 20 · TLS-RPT 15 · BIMI 15 (mail-only checks are skipped for domains without mail). |
| `hardening_missing` / `hardening_next_step` | Missing protections, biggest first, and the record to add for the top one. |
| `mta_sts_record` · `tls_rpt_record` · `bimi_record` · `caa_records` · `dnssec_signed` | Raw hardening data. |
| `error` | Why a domain could not be checked. |
| `checked_at` | UTC timestamp of the check. |

Export as **CSV, Excel, JSON, XML or HTML** from the dataset tab, or connect it to Google Sheets, Zapier, Make or n8n.

***

### 🔌 Use it via API

**Python**

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_API_TOKEN")
run = client.actor("securityzenkai/email-spoof-checker").call(
    run_input={"domains": ["acme-shop.com", "northwind.io"]}
)
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(f'{row["domain"]}: {row["spoofable"]} — {row["main_fix"]}')
```

**JavaScript**

```javascript
import { ApifyClient } from "apify-client";

const client = new ApifyClient({ token: "YOUR_API_TOKEN" });
const run = await client.actor("securityzenkai/email-spoof-checker").call({
  domains: ["acme-shop.com", "northwind.io"],
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.filter((r) => r.spoofable === "yes").forEach((r) => console.log(r.domain, "→", r.main_fix));
```

**Schedule it** (e.g. monthly) in Apify to catch the day someone weakens a DMARC record.

***

### 💰 Pricing

**$5 per 1,000 domains checked** ($0.005 per domain). You only pay for domains that were actually analysed — non-existent, invalid and timed-out domains are free. No subscription, no seats.

***

### 🛡️ Passive & safe by design

This Actor only performs **public DNS lookups** — the same queries every mail server makes when it receives an email. It never connects to, scans, logs into or sends email to the domains you check. That makes it safe to run on any list of domains, including prospects and third parties.

### ⚠️ What it does not do

- **DKIM selectors can't be enumerated.** "No DKIM found" means none of the common selectors — the domain may use a custom one. Add known selectors via `dkimSelectors`.
- **MTA-STS is checked in DNS only.** The `_mta-sts` record is verified; the policy file on `mta-sts.<domain>` is not downloaded, to keep the Actor DNS-only.
- **DNSSEC = DS record published.** It confirms the zone is signed at the registrar, not a full chain validation.
- **No live SMTP tests.** It doesn't connect to mail servers, so it doesn't verify STARTTLS or actual message signing.
- **No look-alike domains.** `acme-sh0p.com` is a different problem (typosquatting); this Actor audits the domains you give it.
- **Parent-domain lookup is simplified.** For DMARC inheritance it strips one label (`mail.acme.com` → `acme.com`).
- **Point-in-time.** DNS changes; results reflect the moment of the check (`checked_at`).

***

### ❓ FAQ

**What does "spoofable" actually mean?**
An attacker can send email with your domain in the visible *From:* address and receiving mail servers will deliver it. Only an enforced DMARC policy (`quarantine` or `reject`) stops that — SPF alone does not, because SPF checks the hidden envelope sender, not the *From:* header people see.

**Why is `p=none` rated `yes`?**
`p=none` is monitoring mode: receivers report spoofed mail but still deliver it. It's the right first step, not the end state.

**Why does a domain that never sends email need SPF and DMARC?**
Parked and unused domains are favourite spoofing targets precisely because nobody watches them. Two TXT records lock them down completely.

**`~all` or `-all`?**
With DMARC enforced, both are fine — DMARC does the blocking. Without DMARC, neither protects the *From:* header.

**Is checking someone else's domain legal?**
Yes — the Actor reads the same public DNS records any mail server or `dig` command reads. Nothing is scanned or probed.

**Do I need API keys or accounts?**
No.

***

### 🔗 Related Actors

- **[Website Security & SSL Expiry Checker](https://apify.com/securityzenkai/website-security-checker)** — grade the same domains A–F on security headers, TLS certificate expiry and HTTPS redirects.

# Actor input Schema

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

Domains to check, one per line. URLs and email addresses are accepted — the domain is extracted.

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

Optional extra DKIM selectors to probe on top of ~35 common ones (Google, Microsoft 365, Mailchimp, SendGrid, ...).

## `nameservers` (type: `array`):

Public DNS resolvers used for the lookups.

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

How many domains are checked in parallel.

## `extendedChecks` (type: `boolean`):

Also check MTA-STS, TLS-RPT, BIMI, DNSSEC and CAA and return a 0–100 hardening score. DNS lookups only; does not change the spoofable verdict.

## Actor input object example

```json
{
  "domains": [
    "google.com",
    "example.com"
  ],
  "dkimSelectors": [],
  "nameservers": [
    "1.1.1.1",
    "8.8.8.8"
  ],
  "concurrency": 10,
  "extendedChecks": true
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

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

No description

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

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "domains": [
        "google.com",
        "example.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("securityzenkai/email-spoof-checker").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "domains": [
        "google.com",
        "example.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("securityzenkai/email-spoof-checker").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "domains": [
    "google.com",
    "example.com"
  ]
}' |
apify call securityzenkai/email-spoof-checker --silent --output-dataset

```

## MCP server setup

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

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/qcnwMhh8VNvCu9qGz/builds/uDG0sD8KSoZD6REGp/openapi.json
