Email Verification
Purpose
Verify and clean email lists using multi-level checks: syntax validation, DNS/MX lookup, disposable domain detection, role-based address flagging, and typo domain correction. Ensures only deliverable addresses reach your campaign tools.
When to Use
- Before any cold outreach campaign push
- Before CRM or list imports
- Before email platform list uploads
- When processing new lead lists from any source
- When cleaning existing contact databases
Quick Start
Single email
python3 email_verifier.py --email test@example.com
CSV batch
python3 email_verifier.py \
--input leads.csv \
--email-column email \
--output leads_verified.csv \
--workers 20
Force DNS-only (skip SMTP probe)
python3 email_verifier.py \
--input leads.csv \
--email-column email \
--output results.csv \
--workers 20 \
--skip-smtp
Parameters
| Parameter | Flag | Description | Default |
|---|---|---|---|
| Input file | --input, -i |
CSV file with email column | (required for batch) |
| Email column | --email-column, -c |
Column name containing emails | email |
| Output file | --output, -o |
Output CSV path | {input}_verified.csv |
| Single email | --email, -e |
Verify one email (no CSV needed) | -- |
| Workers | --workers, -w |
Max concurrent threads | 5 |
| Skip SMTP | --skip-smtp |
Force DNS-only mode | auto-detected |
Status Meanings
| Status | Meaning | Action |
|---|---|---|
| VALID | Domain has MX records, syntax OK, not disposable | Send to campaign |
| INVALID | Dead domain, no MX/A records, disposable provider, bad syntax | Remove from list |
| RISKY | Role-based address (info@, admin@) or A-record-only domain | Separate low-priority sequence or exclude |
| UNKNOWN | DNS timeout or lookup failure | Re-verify later or exclude |
Flags (in output CSV flags column)
| Flag | Meaning |
|---|---|
ROLE_BASED |
Address is a role mailbox (info@, support@, admin@, etc.) |
FREE_PROVIDER |
Gmail, Yahoo, Outlook, etc. (relevant for B2B filtering) |
NO_MX_A_ONLY |
Domain has A record but no MX record -- may not accept mail |
Standard Workflow
- Verify: Run the tool on the lead CSV
- Split by status:
VALID-- push to primary campaign sequenceRISKY-- push to separate low-priority sequence, or excludeINVALID-- remove entirelyUNKNOWN-- re-verify in next batch or exclude
- Push: Upload clean list to your email platform or CRM
Implementation
import dns.resolver
import re
import csv
import concurrent.futures
from datetime import datetime, timezone
# Disposable domain blocklist (subset -- expand as needed)
DISPOSABLE_DOMAINS = {
'mailinator.com', 'guerrillamail.com', 'tempmail.com', 'throwaway.email',
'yopmail.com', 'sharklasers.com', 'grr.la', 'guerrillamailblock.com',
'pokemail.net', 'spam4.me', 'trashmail.com', 'dispostable.com',
# Add 70+ more as needed
}
# Role-based prefixes
ROLE_PREFIXES = {
'info', 'admin', 'support', 'sales', 'contact', 'help', 'billing',
'accounts', 'webmaster', 'postmaster', 'abuse', 'noreply', 'no-reply',
'team', 'office', 'hello', 'careers', 'jobs', 'press', 'media',
'marketing', 'security', 'privacy', 'compliance', 'legal', 'hr',
'feedback', 'inquiries', 'enquiries', 'service', 'orders',
}
# Free email providers
FREE_PROVIDERS = {
'gmail.com', 'yahoo.com', 'outlook.com', 'hotmail.com', 'aol.com',
'icloud.com', 'protonmail.com', 'mail.com', 'zoho.com', 'yandex.com',
}
# Common typo corrections
TYPO_DOMAINS = {
'gmial.com': 'gmail.com', 'gmai.com': 'gmail.com', 'gamil.com': 'gmail.com',
'gmali.com': 'gmail.com', 'gmal.com': 'gmail.com', 'gnail.com': 'gmail.com',
'yahooo.com': 'yahoo.com', 'yaho.com': 'yahoo.com',
'outlok.com': 'outlook.com', 'outloo.com': 'outlook.com',
'hotmal.com': 'hotmail.com', 'hotmial.com': 'hotmail.com',
}
def verify_email(email: str) -> dict:
"""Verify a single email address."""
email = email.strip().lower()
result = {
'email': email,
'status': 'UNKNOWN',
'reason': '',
'mx_host': '',
'flags': [],
'checked_at': datetime.now(timezone.utc).isoformat(),
}
# Syntax check
if not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', email):
result['status'] = 'INVALID'
result['reason'] = 'Bad syntax'
return result
local, domain = email.rsplit('@', 1)
# Typo domain check
if domain in TYPO_DOMAINS:
result['status'] = 'INVALID'
result['reason'] = f'Typo domain -- did you mean {TYPO_DOMAINS[domain]}?'
return result
# Disposable domain check
if domain in DISPOSABLE_DOMAINS:
result['status'] = 'INVALID'
result['reason'] = 'Disposable email provider'
return result
# Role-based check
if local in ROLE_PREFIXES:
result['flags'].append('ROLE_BASED')
# Free provider check
if domain in FREE_PROVIDERS:
result['flags'].append('FREE_PROVIDER')
# DNS/MX lookup
try:
mx_records = dns.resolver.resolve(domain, 'MX')
result['mx_host'] = str(mx_records[0].exchange)
result['status'] = 'VALID'
result['reason'] = 'MX record found'
if result['flags'] and 'ROLE_BASED' in result['flags']:
result['status'] = 'RISKY'
result['reason'] = 'Role-based address'
except dns.resolver.NoAnswer:
# Try A record fallback
try:
dns.resolver.resolve(domain, 'A')
result['status'] = 'RISKY'
result['reason'] = 'A record only, no MX'
result['flags'].append('NO_MX_A_ONLY')
except:
result['status'] = 'INVALID'
result['reason'] = 'No DNS records'
except dns.resolver.NXDOMAIN:
result['status'] = 'INVALID'
result['reason'] = 'Domain does not exist'
except Exception as e:
result['reason'] = f'DNS lookup failed: {str(e)}'
result['flags'] = '|'.join(result['flags'])
return result
Performance
- ~160 emails/sec in DNS-only mode
- 44K leads in ~5 minutes with
--workers 20 - Per-domain concurrency capped at 2 with 0.5s delay (built-in rate limiting)
Limitation
If port 25 is blocked on your server, the tool falls back to DNS-only mode. This means:
- No SMTP mailbox existence check (RCPT TO probe)
- No catch-all domain detection
- Results are domain-level, not mailbox-level
For full SMTP verification, use an external service (e.g., ZeroBounce, NeverBounce) or run from a server with port 25 open.
Output CSV Columns
| Column | Description |
|---|---|
email |
Lowercased, trimmed email |
status |
VALID / INVALID / RISKY / UNKNOWN |
reason |
Human-readable explanation |
mx_host |
MX server hostname (if found) |
flags |
Pipe-separated flags (ROLE_BASED, FREE_PROVIDER, NO_MX_A_ONLY) |
checked_at |
ISO 8601 UTC timestamp |
Built by PureBrain -- AI-powered marketing operations.
Want the full suite of 183+ production-tested skills? Start your trial ->