# Email List Cleaner & Validator (Syntax, MX, Disposable) (`divinxxii/email-list-cleaner`) Actor

Clean and validate email lists in bulk: syntax, MX records, disposable domains, role addresses, typo fixes and duplicates. No SMTP probing, privacy-friendly.

- **URL**: https://apify.com/divinxxii/email-list-cleaner.md
- **Developed by:** [James Brown](https://apify.com/divinxxii) (community)
- **Categories:** Lead generation, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.56 / 1,000 email checkeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Email Validator & List Cleaner (MX, Disposable, Typos)

This email validator and list cleaner checks an email list before you send to it. For every address you get **syntax validation, a DNS MX check (with the RFC 5321 A-record fallback), disposable-domain detection, role-address detection (info@, sales@, admin@, enquiries@, kontakt@…), domain typo suggestions (gmial.com → gmail.com), duplicate flags** and a plain **verdict: `deliverable-likely`, `risky` or `invalid`** with the reasons. A ready-made **clean CSV** of the addresses worth keeping is saved next to the results.

**No SMTP probing, no mailbox verification.** The actor never connects to a mail server and never sends a message, so it cannot get your IP blocklisted, trigger greylisting or tip anyone off that an address is being tested. The only network traffic about your addresses is DNS lookups of their *domains* (the part after the `@`). The part before the `@` never leaves the run. That makes it fast, cheap and privacy-friendly: **$0.80 per 1,000 addresses**.

### What the email validator can and cannot tell you

| | This actor |
|---|---|
| Is the address well-formed? | Yes |
| Does the domain exist and can it receive mail (MX, or A/AAAA fallback, null-MX)? | Yes |
| Is the domain a known disposable / throwaway service? | Yes (bundled list, CC0) |
| Is it a shared role mailbox (info@, sales@…)? | Yes |
| Did the user mistype a popular domain (gmial.com)? | Yes, with a suggested fix |
| Does *this particular mailbox* exist? | **No** (that needs SMTP probing, which this actor deliberately does not do) |
| Is the domain a catch-all? | No |

A `deliverable-likely` verdict means "well-formed, the domain accepts mail, and nothing suspicious was found". It is not a guarantee the mailbox exists. Use it to remove the obviously dead, mistyped and throwaway addresses before a send, not as proof of delivery.

### What the email list cleaner returns per address

| Field | Meaning |
|---|---|
| `row` | 1-based position among non-blank inputs |
| `original` | The input, trimmed |
| `normalized` | Trimmed, domain lowercased (IDN domains in punycode), `Name <addr>` and `mailto:` unwrapped. `null` if the syntax is invalid |
| `syntaxValid` | RFC 5321/5322 dot-atom syntax check (quoted local parts and IP-literal domains are rejected) |
| `domain` | Lowercase domain |
| `hasMx` | `true` if the domain has MX records, **or** none but an A/AAAA record (implicit MX per RFC 5321 §5.1; see `mxSource`); `false` if it cannot receive mail; `null` if the lookup failed |
| `mxSource` | `mx`, `a-fallback` or `null` |
| `dnsStatus` | `ok`, `nxdomain`, `null-mx` (RFC 7505: domain refuses mail), `no-records`, or `error` (timeout / SERVFAIL after one retry) |
| `isDisposable` | Domain, or a parent domain, is on the disposable list |
| `isRoleAddress` | Local part is a role mailbox (`info`, `sales`, `admin`, `no-reply`, `enquiries`, `bookings`, `accounts`, `reception`, `kontakt`, `ventas`, `accueil`…; dots, hyphens, underscores and `+tags` ignored) |
| `typoSuggestion` | Likelier domain, e.g. `gmail.com` for `gmial.com` |
| `suggestedEmail` | The address with the domain fixed |
| `duplicateOf`, `duplicateOfRow` | Set on repeats (compared case-insensitively on the normalized address; inputs that are not valid addresses are compared by their trimmed text); `duplicateOfRow` is the first occurrence |
| `recoveredAfterRestart` | Only present (`true`) on a row pushed again after a restart; it was not charged a second time |
| `verdict` | `deliverable-likely`, `risky`, `invalid` (`null` on duplicates, whose first occurrence carries the verdict) |
| `reasons` | Why: see below |

**Verdict rules.** `invalid`: bad syntax (`syntax:…`), `domain-not-found`, `null-mx`, `no-mail-records`. Otherwise `risky` if any of `disposable-domain`, `role-address`, `possible-typo`, `no-mx-a-record-fallback` (the domain has a web server record but no MX), `dns-unavailable` (the lookup failed, so the domain is unknown), `internationalised-local-part` (valid, but many systems cannot handle it). Otherwise `deliverable-likely`.

### Input

| Field | Description |
|---|---|
| `emails` | List of addresses |
| `text` | Blob: one per line, or separated by commas / semicolons / tabs. `Name <a@b.com>` and `mailto:` forms are understood |
| `csvUrl` + `csvColumn` | Public CSV URL, read as a stream (up to 200 MB). Column by header name or 1-based index; auto-detects headers containing "mail" |
| `csvHeader` | `auto` / `yes` / `no`: whether the first CSV row is a header |
| `includeRiskyInCleanCsv` | Also put `risky` addresses in the clean CSV. Default off |

Run it with no input and you get an 11-row sample.

#### Example input

```json
{
    "emails": ["jane.doe@gmail.com", "Jane.Doe@GMAIL.com", "sam@hotmial.co.uk", "info@gmail.com", "promo@mailinator.com", "user@@broken"]
}
```

#### Example output (one record shown)

```json
{
    "row": 3,
    "original": "sam@hotmial.co.uk",
    "normalized": "sam@hotmial.co.uk",
    "syntaxValid": true,
    "domain": "hotmial.co.uk",
    "hasMx": true,
    "mxSource": "mx",
    "dnsStatus": "ok",
    "isDisposable": false,
    "isRoleAddress": false,
    "typoSuggestion": "hotmail.co.uk",
    "suggestedEmail": "sam@hotmail.co.uk",
    "duplicateOf": null,
    "duplicateOfRow": null,
    "verdict": "risky",
    "reasons": ["possible-typo"]
}
```

#### Clean-CSV export

When the run ends, two files are saved in the run's key-value store:

- `clean-emails.csv`: one `email` column with the first occurrence of every `deliverable-likely` address (plus `risky` ones if you switched that on). Invalid addresses are never in it. Addresses starting with `=`, `+`, `-` or `@` are left out (spreadsheet formula risk) and listed in the review file instead.
- `review-emails.csv`: every `risky` and `invalid` address with its reasons and `suggestedEmail`, for fixing by hand. Text beginning with `= + - @` is prefixed with `'` so spreadsheets do not execute it.

Both are built from the run's dataset when the run finishes, so they are complete even after a restart.

### Pricing (pay per event)

| Event | Price | When it is charged |
|---|---|---|
| Actor start | $0.005 | Once per run |
| Email checked | $0.0008 | Per unique address checked (duplicates, blank rows and failed DNS lookups are free) |

| Addresses | Approximate cost |
|---|---|
| 1,000 | $0.805 |
| 10,000 | $8.005 |
| 100,000 | $80.005 |

- **Actor start:** $0.005 per run (Apify's `apify-actor-start` event). Maximum run memory is capped at 1,024 MB so the start event is never multiplied.
- **Email checked:** $0.0008 per address (`email-checked` event), charged **after** the result is saved to the dataset. Syntax-invalid addresses are charged too (first occurrence only): they are a completed check with an `invalid` verdict.
- **Free:** duplicates (including repeated invalid inputs such as `N/A`), blank rows, and addresses whose DNS lookup failed with a timeout/SERVFAIL (`dnsStatus: "error"`; those are returned as `risky` / `dns-unavailable`; re-run them later).

Example: 50,000 unique addresses = $40.00 + $0.005. If you set a maximum cost per run, the actor stops cleanly when it is reached.

### Performance

Addresses are processed in batches of 500, with up to 50 DNS lookups in flight and one lookup per distinct domain per run (cached). Measured locally (Windows dev machine, Python 3.11, real DNS from a home connection, local file storage, no pricing configured): 403 addresses over 10 domains took about 7 seconds; 5,000 addresses over about 300 distinct domains took about 70 seconds with a peak of about 84 MB resident memory (much of that time is local per-item dataset writes, as with the other actors in this portfolio). Lists with many distinct domains are limited by DNS latency, not CPU; memory is dominated by the interpreter and the bundled disposable list (about 10,000 domains), and the duplicate index holds one short entry per distinct address. These figures were measured locally, not on the Apify platform.

#### Restarts

Progress is saved to the key-value store (`PROGRESS`, counters only, never addresses), so a restart or migration continues without re-pushing or re-charging addresses already delivered, and without repeating their DNS lookups. Results are never lost and never double-charged: the few addresses that were in flight when the run was interrupted are pushed again, **without a charge**, and carry `recoveredAfterRestart: true`. In rare cases that means a result row appears twice in the dataset (the first copy has no flag); the two CSV exports are built with each row number once, so they contain no repeats.

### Data sources, licences and compliance

- **Disposable domains:** [disposable-email-domains](https://github.com/disposable-email-domains/disposable-email-domains) `disposable_email_blocklist.conf`, **CC0 1.0 Universal** (public-domain dedication; verified 2026-10-21 against the repository's `LICENSE.txt`). Bundled unmodified in `data/disposable_domains.txt` (upstream commit and licence copy in `data/SOURCE.md` and `data/LICENSE-disposable-email-domains.txt`). The list is as of the actor's build and is refreshed by releasing a new version. It also contains some typo-squatting domains (e.g. `gmial.com`), which therefore get both `disposable-domain` and `possible-typo`.
- **Role-address and typo lists:** written for this actor (no third-party data).
- **Dependencies:** [dnspython](https://github.com/rthalley/dnspython) (ISC licence), [httpx](https://www.python-httpx.org/) (BSD-3-Clause), the Apify SDK (Apache-2.0). No GPL/AGPL code.
- **ToS / robots.txt:** nothing is scraped. DNS is queried through the platform's resolver. The only HTTP access is when *you* supply a `csvUrl`: the actor then fetches that file, identifies itself with a User-Agent, honours the host's `robots.txt` (RFC 9309; it refuses if disallowed, and also if `robots.txt` cannot be fetched or returns 5xx/429), re-checks every redirect target, blocks private/internal addresses, and spaces requests about 1 second apart. This cannot be switched off.
- Credentials in URLs (`user:pass@host`) are not sent: the actor never makes authenticated requests, so use a URL that needs no login.
- **What is stored:** the results go to the run's default dataset and the two CSV files plus `PROGRESS` to the run's default key-value store, with standard Apify storage retention for your account/plan. You control and can delete them. The actor keeps nothing outside your run. Addresses are never written to the run log; error messages contain neither addresses nor full URLs. Apart from the domain DNS lookups described above, the addresses are not sent anywhere.
- **Personal data:** email addresses are personal data. You are responsible for having a lawful basis (e.g. UK GDPR / PECR) for processing and mailing the addresses you submit.

### Limitations

- **No mailbox-level verification.** Cannot detect non-existent mailboxes on a working domain, full inboxes or catch-all servers.
- Disposable detection is list-based and only as current as the bundled list; new throwaway domains will be missed. The list can also contain domains you consider legitimate.
- Typo suggestions are advisory: exact for a curated set of common mistakes, plus a conservative one-edit fuzzy match against about 50 popular providers. A real but unusual domain one letter away from a big provider can be flagged.
- Role detection is a word list; unusual role names are not recognised, and a few personal addresses may share a role word.
- Duplicates compare the full normalized address case-insensitively; Gmail dots and `+tags` are **not** collapsed.
- DNS answers reflect the moment of the run, as seen from the platform's resolver. A `dns-unavailable` row means "unknown": re-run it.
- Quoted local parts (`"a b"@x.com`), comments and IP-literal domains are treated as invalid.
- CSV input: quoted multi-line fields are parsed as one record. `csvColumn` takes a header name or a 1-based number. If the file changes between a restart and the original run, row numbers may shift.
- The platform's dataset retention applies; download your results in time.

### FAQ

**How do I clean an email list?**
Add the addresses to `emails`, paste them into `text`, or give a public `csvUrl`. Download `clean-emails.csv` from the run's key-value store for the addresses worth keeping, and `review-emails.csv` for the ones to fix.

**Does it verify that a mailbox exists?**
No. It checks syntax, whether the domain can receive mail (MX), disposable domains, role addresses and likely typos. It never connects to a mail server, so it cannot confirm a specific mailbox.

**Can it find disposable email addresses?**
Yes, against a bundled list of about 10,000 disposable domains. New throwaway domains may be missed until the list is refreshed.

**Can it fix typos such as gmial.com?**
It suggests the likelier domain in `typoSuggestion` and `suggestedEmail`; it does not change your data.

### Related Actors

- [Phone Number Validator & Formatter](https://apify.com/divinxxii/phone-validator): validate and format phone numbers in bulk, offline.
- [PDF to Markdown for RAG](https://apify.com/divinxxii/doc-to-markdown): turn PDF, DOCX, PPTX and HTML into Markdown for LLMs.
- [Domain Health Checker](https://apify.com/divinxxii/domain-health-audit): SPF, DMARC, DKIM and SSL expiry checks for any domain, handy before a mailing.

# Actor input Schema

## `emails` (type: `array`):

List of email addresses. Leave empty (with no text or CSV URL) to run a small built-in sample.

## `text` (type: `string`):

Paste addresses separated by new lines, commas or semicolons (e.g. a column copied from a spreadsheet). 'Name <addr@example.com>' and 'mailto:' forms are understood.

## `csvUrl` (type: `string`):

Public URL of a CSV file. The actor reads one column, streaming, and honours the host's robots.txt.

## `csvColumn` (type: `string`):

Header name, or 1-based column number (1 is the first column; 0 is rejected), holding the email addresses. If empty, the first header containing 'email' or 'mail' is used; with no such header, column 1 is read and the first row is treated as data.

## `csvHeader` (type: `string`):

Whether the first CSV row is a header (skipped). 'auto': it is a header if any of its cells contains 'mail'. 'yes': always skip it. 'no': always treat it as data (not allowed with a header-name column).

## `includeRiskyInCleanCsv` (type: `boolean`):

The clean-emails.csv export in the key-value store lists deliverable-likely addresses only. Switch on to add 'risky' ones (role, disposable, typo-suspect, A-record-only domains). Invalid addresses are never included.

## Actor input object example

```json
{
  "emails": [
    "jane.doe@gmail.com",
    "sam@gmial.com",
    "info@example.org",
    "promo@mailinator.com",
    "not-an-email"
  ],
  "csvHeader": "auto",
  "includeRiskyInCleanCsv": false
}
```

# Actor output Schema

## `results` (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 = {
    "emails": [
        "jane.doe@gmail.com",
        "sam@gmial.com",
        "info@example.org",
        "promo@mailinator.com",
        "not-an-email"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("divinxxii/email-list-cleaner").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 = { "emails": [
        "jane.doe@gmail.com",
        "sam@gmial.com",
        "info@example.org",
        "promo@mailinator.com",
        "not-an-email",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("divinxxii/email-list-cleaner").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 '{
  "emails": [
    "jane.doe@gmail.com",
    "sam@gmial.com",
    "info@example.org",
    "promo@mailinator.com",
    "not-an-email"
  ]
}' |
apify call divinxxii/email-list-cleaner --silent --output-dataset

```

## MCP server setup

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

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/NvwWa7iJm2PMwfiJS/builds/MdKfk57ciZ7hss3bi/openapi.json
