# Bulk Email DNS Readiness Auditor (`afkdigital/bulk-email-dns-readiness-auditor`) Actor

Bulk MX, SPF, direct DMARC and explicit DKIM selector DNS audits. IDN support, evidence, JSON/CSV exports. No email login or SMTP. $6 per 1,000 completed unique domains.

- **URL**: https://apify.com/afkdigital/bulk-email-dns-readiness-auditor.md
- **Developed by:** [AFK Digital](https://apify.com/afkdigital) (community)
- **Categories:** Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$6.00 / 1,000 successfully checked unique domains

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 DNS Readiness Auditor

Audit MX, SPF, direct-domain DMARC and **only the DKIM selectors you provide**, in bulk. Export evidence as JSON or CSV for domain inventories, agency onboarding, DNS configuration reviews and migration checks. No third-party paid data account, email login or API key is needed.

**DNS readiness is not a guarantee of inbox delivery, sender reputation, message authentication or permission to send email.** This Actor never sends email, opens SMTP sessions, probes mailboxes or guesses selectors.

### Quick start

```json
{
  "domains": ["gmail.com", "example.com", "newapi73-audit-missing.invalid"],
  "dkimSelectors": [],
  "includeMx": true,
  "maxDomains": 100
}
```

The default three-domain sample is designed to finish within five minutes. Public DNS changes over time; results reflect the recursive resolver view at the recorded UTC timestamp. The reserved .invalid example intentionally demonstrates NXDOMAIN; it is not a technical failure.

To inspect your own DKIM publication, set e.g. `"dkimSelectors": ["your-selector"]`. Supply only selectors you know. Every supplied selector is queried for every supplied domain. At most **five selector entries**, including duplicates, are accepted. There is no selector surcharge.

### Input

| Field | Meaning |
| --- | --- |
| domains | Array of fully qualified domain strings. No URLs, IP addresses or email addresses. |
| csv | Optional inline CSV text, a single domain column and optional domain/domains header. Maximum 1 MB. Combined with domains. No remote CSV download. |
| dkimSelectors | Explicit selector labels, maximum 5; empty by default. Dotted selectors supported. |
| includeMx | Boolean, true by default. |
| maxDomains | First N unique normalized domains, 1–1,000, default 100. Maximum 10,000 input rows. |

IDNs use IDNA/UTS46 normalization; case and a trailing root dot are normalized before deduplication. A domain present in both sources is processed and charged at most once per run. Invalid input fails before any DNS work or event charge. CSV example: `domain\nexample.com\ngmail.com\n`.

### What the audit reports

**MX:** priorities and exchanges, explicit null MX (RFC 7505), absent records and query status. Missing MX does not by itself prove inability to receive mail: SMTP implicit A/AAAA fallback is outside this audit.

**SPF:** TXT record selection, multiple policies, mechanism grammar, IPv4/IPv6 network and prefix syntax, duplicate redirect/exp, unknown mechanisms, literal include/redirect graph, cycles, missing include policies and DNS query evidence. The syntax status describes this bounded static parser, not full RFC conformance certification.

DNS-causing terms are include, redirect, a, mx, exists and ptr. The graph resolves literal a (A and AAAA), mx (up to ten hosts plus their addresses) and exists (A), and follows literal include/redirect policies. IP literals and all do not consume SPF DNS-term allowance; exp is not queried. Terms after all, including redirect, are unreachable. Sender-specific macro expansion and ptr require context; the audit records them as unresolved without inventing values. Simple %{d} is supported. Unknown modifiers are ignored as SPF specifies.

`dns_terms_upper_bound` is a **conservative aggregate of explored branches**, not the DNS-term count of an actual sender evaluation. RFC 7208 limits DNS-causing terms to ten per actual evaluation, including nested policies; cache hits do not remove this protocol count. The reported aggregate can exceed ten even when a particular sender path would not. Observed void A/AAAA/TXT/MX queries are also graph observations, not a claim that an actual sender exceeds the recommended two-void-lookup limit. Limits and context gaps set analysis_complete=false. Include cycles are static risks, not unconditional SPF verdicts. **spf_result is always NOT_EVALUATED:** no sending IP, MAIL FROM or HELO is provided.

**DMARC:** inspect only TXT at `_dmarc.<supplied-domain>`. Report the raw record, direct p value, duplicates, malformed tags, recognized policy/alignment/test-mode values and historic tags. The current core standard is **RFC 9989 (May 2026)**, replacing RFC 7489/9091. Under RFC 9989 an absent p defaults to none; pct/rf/ri are historic. Malformed tags are findings; receiver recovery behavior may differ from this syntax diagnosis. This Actor does **not** perform Organizational Domain/PSD tree-walk discovery, inherited policy selection, message identifier alignment, report URI validation or external reporting authorization. Direct-record absence must not be read as “no effective DMARC policy.”

**DKIM:** query only selector.\_domainkey.domain TXT; normal DNS alias resolution applies. Report absence, errors, multiple records, revoked empty p, tag/base64/key encoding, RSA size and Ed25519 key length. RSA below 1,024 bits is flagged (RFC 8301); 2,048 bits is recommended. No email signature is verified and private-key possession is not proven. Without selectors: NOT_CHECKED, reason “No user-supplied selectors.”

### Output and exports

One Dataset item per normalized unique domain, including technical failures:

`domain, mx_records, spf_record, spf_syntax_status, spf_lookup_findings, dmarc_record, dmarc_policy, dkim_results, findings, dns_query_status, checked_at`.

Additional evidence: all SPF/DMARC candidate records, dmarc_syntax_status, audit_status and limitations. dns_query_status contains each name/type, status, records and remaining TTL. Findings have code, severity and detail. DKIM findings are nested within each selector result.

- **OK:** DNS answered with records.
- **NODATA:** name answered, requested RR type absent.
- **NXDOMAIN:** queried name does not exist.
- **TIMEOUT, SERVFAIL, REFUSED, DNS_ERROR:** distinct technical failures.
- **LIMIT_EXCEEDED:** bounded audit could not finish; technical failure.
- **NOT_CHECKED:** optional check skipped.

Missing SPF, DMARC, DKIM or MX, malformed configuration and NXDOMAIN are completed observations and may be charged. **Any technical DNS failure anywhere in a domain's audit makes audit_status=TECHNICAL_ERROR and that domain event is not charged.** Records found before the error remain visible.

Use Dataset Export for JSON/CSV, or the run output links. OUTPUT is also stored as JSON, OUTPUT.csv contains nested fields as JSON strings inside CSV cells, and SUMMARY contains counts.

### Pricing

**$0.006 per successfully checked unique domain ($6 per 1,000)**, including all requested selectors. The sole paid event is domain-checked. No startup or per-selector event fee, no per-Dataset-item fee, and platform usage is included in event pricing. Technical-query failures are exported without a domain event charge. New runs are independent audits and can charge the same domain again. Owner/test runs can be exempt from charges on Apify.

A domain is persisted before its event is charged. On a resumed run, already exported domains are skipped; interruption between storage and charging can undercharge rather than charging again. Apify run budgets can stop the run early; increase the maximum charge or reduce maxDomains to process more domains.

### Limits and DNS behavior

Five concurrent domain jobs, globally throttled to at most 40 logical resolver calls/second. The configured system recursive resolver is used with search suffixes disabled; dnspython handles standard UDP DNS and TCP fallback on truncated answers. Per-call timeout 1.5 seconds, total resolver lifetime 3 seconds. Resolver retries fit within that lifetime. Each domain has a maximum 100 logical queries and a 45-second audit budget checked before queries; an in-flight query can add up to 3 seconds. SPF recursion depth 10 and static graph DNS terms 40.

The in-memory shared LRU cache holds up to 20,000 positive/negative answers, respects TTL expiration, and is cleared between runs. Negative DNS caching follows SOA TTL semantics (RFC 2308); transient errors are not cached as absence. No arbitrary nameserver or network target is accepted. DNSSEC validation, propagation checks from multiple geographies and direct authoritative-server delegation checks are outside scope. DNS answers can change between checks.

### Standards and dependencies

Verified on 2026-11-06 against primary sources:

- [SPF RFC 7208](https://www.rfc-editor.org/rfc/rfc7208), especially sections 4.6.4 and 7.
- [DMARC RFC 9989](https://www.rfc-editor.org/rfc/rfc9989), direct-record scope only.
- [DKIM RFC 6376](https://www.rfc-editor.org/rfc/rfc6376), [RFC 8301](https://www.rfc-editor.org/rfc/rfc8301), [Ed25519 RFC 8463](https://www.rfc-editor.org/rfc/rfc8463).
- DNS RFC 1035/2308 and null MX RFC 7505.
- [dnspython 2.9.2](https://dnspython.readthedocs.io/en/stable/): ISC; [Apify SDK 4.2.1](https://docs.apify.com/sdk/python/): Apache-2.0; [idna 3.20](https://github.com/kjd/idna): BSD-3-Clause; [cryptography 50.0.2](https://cryptography.io/): Apache-2.0 OR BSD-3-Clause.
- [Apify pay-per-event documentation](https://docs.apify.com/actors/monetize/pay-per-event).

Version 1.0.0 · Product NEWAPI73 · AFK Digital

# Actor input Schema

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

Fully qualified domains, without URLs or email addresses. IDNs are normalized and duplicates removed.

## `csv` (type: `string`):

Inline single-column CSV; optional domain header. Combined with domains, deduplicated. Maximum 1 MB.

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

Only these selectors will be queried for every domain. Maximum five. Empty means NOT_CHECKED; no guessing.

## `includeMx` (type: `boolean`):

Retrieve MX and detect null MX. No SMTP connection.

## `maxDomains` (type: `integer`):

Process the first N normalized unique domains.

## Actor input object example

```json
{
  "domains": [
    "gmail.com",
    "example.com",
    "newapi73-audit-missing.invalid"
  ],
  "dkimSelectors": [],
  "includeMx": true,
  "maxDomains": 100
}
```

# Actor output Schema

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

No description

## `csv` (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": [
        "gmail.com",
        "example.com",
        "newapi73-audit-missing.invalid"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("afkdigital/bulk-email-dns-readiness-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": [
        "gmail.com",
        "example.com",
        "newapi73-audit-missing.invalid",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("afkdigital/bulk-email-dns-readiness-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": [
    "gmail.com",
    "example.com",
    "newapi73-audit-missing.invalid"
  ]
}' |
apify call afkdigital/bulk-email-dns-readiness-auditor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,afkdigital/bulk-email-dns-readiness-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/4zeDL12EPxzVMCHzs/builds/ga6SaAEmpQrR9jcLC/openapi.json
