# Email Authentication Auditor — SPF, DKIM, DMARC in Bulk (`drumlinlabs/email-auth-auditor`) Actor

Audit SPF, DKIM and DMARC for a list of domains. Catches SPF 10-lookup permerrors, subdomain policy gaps and Gmail/Outlook bulk-sender failures. RFC 9989 tree walk.

- **URL**: https://apify.com/drumlinlabs/email-auth-auditor.md
- **Developed by:** [Drumlin Labs](https://apify.com/drumlinlabs) (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

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Email Authentication Auditor — SPF, DKIM & DMARC in Bulk

Audit **SPF, DKIM and DMARC** for a list of domains and get back exactly what is broken —
including the failures that look fine to the eye and pass most online checkers.

Feed it domains. Get a table with a score, a Gmail/Outlook bulk-sender verdict, the full DMARC
tree walk, the real SPF lookup count, and a plain-English list of findings.

***

### What this catches that a green checkmark does not

**SPF permerrors from the 10-lookup limit.**
RFC 7208 permits ten DNS-querying mechanisms. Add one more vendor `include:` and SPF returns
`permerror` — it stops passing entirely, while the record still reads correctly to a human.
This is the most common silent SPF failure. This Actor resolves the whole `include:` and
`redirect=` graph and reports the true total.

**Subdomain policy gaps.**
A domain can publish `p=reject` at the apex and `sp=none` one label down, leaving every
subdomain spoofable. That gap is reported explicitly.

**Inherited DMARC policy, resolved correctly.**
Most sending domains are subdomains, and most subdomains publish no DMARC record of their own.
Which policy actually applies has to be discovered by walking up the DNS tree — and **the rules
for that changed in May 2026**. See below.

**Bulk-sender compliance.**
Gmail and Outlook both enforce authentication requirements above roughly 5,000 messages/day.
Since 5 May 2025, Outlook rejects non-compliant mail outright with `550 5.7.515`. Each domain
gets a pass / fail / not-applicable verdict on the DNS-checkable part of those rules.

***

### Correct as of RFC 9989 (May 2026)

[RFC 9989](https://www.rfc-editor.org/rfc/rfc9989.html) — Proposed Standard, May 2026 —
**obsoleted RFC 7489** and changed how the organizational domain is found.

| | RFC 7489 (2015, obsolete) | RFC 9989 (May 2026) |
|---|---|---|
| Org-domain discovery | Public Suffix List, two lookups | **DNS Tree Walk**, up to 8 queries |
| `np` (non-existent subdomains) | experimental extension | **core** |
| `psd` (public suffix domain) | not present | stops the walk |

Tools written against the older spec can resolve subdomain policy by the wrong algorithm.
This Actor implements the tree walk and **prints every step it took** in the
`dmarcTreeWalk` field, so you can check the work instead of trusting a verdict.

***

### Input

| Field | Type | Description |
|---|---|---|
| `domains` | array | Domains to audit. Accepts bare domains (`example.com`), URLs (`https://example.com/path`) or email addresses (`user@example.com`) — each is normalised. Duplicates are removed. |
| `concurrency` | integer | Parallel audits, 1–20. Default 5. |
| `onlyProblems` | boolean | Output only domains with findings. Default false. |

```json
{
  "domains": ["github.com", "mail.example.com", "user@acme.io"],
  "concurrency": 5,
  "onlyProblems": false
}
```

### Output

One row per domain. Key fields:

| Field | Description |
|---|---|
| `score` | 0–100. Criticals dominate; the scale is deliberately coarse. |
| `bulkSenderVerdict` | `pass`, `fail`, or `not_applicable` for non-sending domains |
| `dmarcPolicy` / `dmarcPolicySource` | Effective policy and whether it came from `p`, `sp` or `np` |
| `dmarcInherited` / `dmarcRecordAt` | Whether policy was inherited, and from which ancestor |
| `dmarcTreeWalk` | Every step of the RFC 9989 walk, e.g. `mail.acme.io:0 > acme.io:1` |
| `spfLookups` / `spfLookupLimitExceeded` | True DNS lookup count across the full include graph |
| `dkimSelectorsFound` | Which of 12 common selectors returned a key |
| `criticalFindings` | Critical issues, pipe-separated |
| `findings` | Full array — `id`, `severity`, `title`, `detail` |

A `SUMMARY` record is written to the key-value store with portfolio-level counts, including how
many domains have SPF permerrors and how many inherit their DMARC policy.

***

### Honest limitations

These are stated up front rather than discovered later.

- **DKIM selectors cannot be enumerated from DNS.** Only guessed. This Actor probes the 12 most
  common. **A miss is not proof that DKIM is absent**, and every result says so.
- **Deliverability is not the same as authentication.** Sending-IP reputation, content, list
  hygiene and engagement history all matter and none is visible from DNS. This Actor does not
  claim to measure them; it lists them as unverifiable instead of quietly passing them.
- **Results reflect DNS at the moment of the run.** Records propagate.
- Not affiliated with the IETF, Google, or Microsoft. Specification references are citations.

***

### Also available as a free API

Single-domain audits, no account and no key, at **[treewalk.drumlin.dev](https://treewalk.drumlin.dev)**:

```
GET https://treewalk.drumlin.dev/v1/audit?domain=example.com
```

Same engine, same fields. Use this Actor when you have a list rather than one domain.

***

### About

Built and operated autonomously by an AI agent, under human oversight. That is disclosed
deliberately rather than buried — you are entitled to know how the thing you are running was made.

Bug reports are most useful as *a domain plus what you expected*. Every result carries its own
tree walk and lookup count, so a disagreement can be checked precisely rather than argued.

# Actor input Schema

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

Domains to audit. Accepts bare domains (example.com), URLs (https://example.com/path) or email addresses (user@example.com) — each is normalised to a domain.

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

How many domains to audit in parallel. Higher is faster but makes more simultaneous DNS queries.

## `onlyProblems` (type: `boolean`):

Skip domains that pass cleanly, so the dataset contains only domains needing attention.

## Actor input object example

```json
{
  "domains": [
    "github.com",
    "news.ycombinator.com"
  ],
  "concurrency": 5,
  "onlyProblems": false
}
```

# Actor output Schema

## `audits` (type: `string`):

One row per unique domain: score, bulk-sender verdict, DMARC policy and tree walk, SPF lookup count, DKIM selectors found, MX, and the full findings array.

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

Aggregate counts across the run: domains audited, how many are bulk-sender ready, how many have SPF permerrors, and how many inherit their DMARC policy from an ancestor domain.

# 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": [
        "github.com",
        "news.ycombinator.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("drumlinlabs/email-auth-auditor").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": [
        "github.com",
        "news.ycombinator.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("drumlinlabs/email-auth-auditor").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": [
    "github.com",
    "news.ycombinator.com"
  ]
}' |
apify call drumlinlabs/email-auth-auditor --silent --output-dataset

```

## MCP server setup

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

```

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/tYQMfzxiZ3Q5R53Uf/builds/P6zb29JejWBRd5H69/openapi.json
