https://github.com/centminmod/validate-emails
Self-hosted email verification script to clean up bad invalid email address lists. Supports various commercial email verification provider APIs all in one script
https://github.com/centminmod/validate-emails
email-cleaner email-cleaning email-validate email-validation email-validation-api email-validator email-verification email-verification-api email-verifier emaillistverify millionverifer proofy reoon verify-email verifyemail zerobounce
Last synced: 4 months ago
JSON representation
Self-hosted email verification script to clean up bad invalid email address lists. Supports various commercial email verification provider APIs all in one script
- Host: GitHub
- URL: https://github.com/centminmod/validate-emails
- Owner: centminmod
- Created: 2024-05-05T09:52:37.000Z (about 2 years ago)
- Default Branch: master
- Last Pushed: 2025-11-20T15:52:28.000Z (7 months ago)
- Last Synced: 2025-11-20T17:26:29.859Z (7 months ago)
- Topics: email-cleaner, email-cleaning, email-validate, email-validation, email-validation-api, email-validator, email-verification, email-verification-api, email-verifier, emaillistverify, millionverifer, proofy, reoon, verify-email, verifyemail, zerobounce
- Homepage:
- Size: 855 KB
- Stars: 68
- Watchers: 1
- Forks: 7
- Open Issues: 3
-
Metadata Files:
- Readme: readme.md
Awesome Lists containing this project
- awesome-opensource-email - validate-email - Self-hosted email verification script to clean up bad invalid email address lists. Supports various commercial email verification provider APIs all in one script - `PHP` (Deliverability / Email Verification)
README
2025: Black Friday discount coupons for some email verification providers:
* [Zerobounce](https://centminmod.com/zerobounce) Promo code: **SAVE40** Get 40% bonus PAYG credits now!
* [Bouncify](https://centminmod.com/bouncify) Promo code: **BF2025** Get 20% discount
* [EmailListVerify](https://centminmod.com/emaillistverify) Promo code: **5BF25** Get 5.5% discount
* [CaptainVerify](https://centminmod.com/captainverify) Promo code: **BF25** 25% discount on verification packs
---
# Email Validation Script
- [Overview](#overview)
- [Features](#features)
- [Requirements](#requirements)
- [Usage](#usage)
- [Output](#output)
- [Logging](#logging)
- [Configuration](#configuration)
- [`validate_emails.ini` config file](#validate_emailsini-config-file)
- [S3 Storage Support](#s3-storage-support)
- [-smtp ses](#-smtp-ses)
- [-smtp rotate](#-smtp-rotate)
- [-smtp generic](#-smtp-generic)
- [Customization](#customization)
- [Troubleshooting](#troubleshooting)
- [Example Usage](#example-usage)
- [Xenforo](#xenforo)
- [Xenforo Email Bounce Log](#xenforo-email-bounce-log)
- [API Support](#api-support)
- [API Usage Commands](#api-usage-commands)
- [Personal Experience](#personal-experience)
- [Email Verification Provider Comparison Costs](#email-verification-provider-comparison-costs)
- [Email Verification Provider API Speed & Rate Limits](#email-verification-provider-api-speed--rate-limits)
- [Email Verification Provider API Speed Benchmarks](#email-verification-provider-api-speed-benchmarks)
- [Email Verification Results Table Compare](#email-verification-results-table-compare)
- [EmailListVerify](#emaillistverify)
- [EmailListVerify Bulk File API](#emaillistverify-bulk-file-api)
- [MillionVerifier](#millionverifier)
- [MillionVerifier Bulk File API](#millionverifier-bulk-file-api)
- [MillionVerifier Bulk API Differences](#millionverifier-bulk-api-differences)
- [CaptainVerify API](#captainverify-api)
- [Proofy API](#proofy-api)
- [MyEmailVerifier API](#myemailverifier-api)
- [Zerobounce API](#zerobounce-api),
- [Reoon API](#reoon-api)
- [Bouncify API](#bouncify-api)
- [Bounceless API](#bounceless-api)
- [API Merge](#api-merge)
- [API Merge Filters](#api-merge-filters)
- [Cloudflare HTTP Forward Proxy Cache With KV Storage](#cloudflare-http-forward-proxy-cache-with-kv-storage)
- [MillionVerifier Cloudflare Cache Support](#millionverifier-cloudflare-cache-support)
- [ZeroBounce Cloudflare Cache Support](#zerobounce-cloudflare-cache-support)
- [Cloudflare Cache Purge Support](#cloudflare-cache-purge-support)
- [EmailListVeirfy Bulk File API Cloudflare Cache Support](#emaillistveirfy-bulk-file-api-cloudflare-cache-support)
- [EmailListVerify API Check Times: Regular vs Cached](#emaillistverify-api-check-times-regular-vs-cached)
- [PHP Wrapper](#php-wrapper)
- [PHP Wrapper With Cloudflare Cache And S3 Store Support](#php-wrapper-with-cloudflare-cache-and-s3-store-support)
## Overview
The `validate_emails.py` is a Python-based tool for email verification and email validation that allows you to classify your email addresses' status using syntax, DNS, and SMTP (Simple Mail Transfer Protocol) and other checks and 3rd party APIs. Using the script's returned status classifications, you can then clean or scrub your email lists. This can be done self-hosted locally on a server or via the supported [commercial email verification service APIs](#api-support) for [EmailListVerify](https://centminmod.com/emaillistverify), [MillionVerifier](https://centminmod.com/millionverifier), [MyEmailVerifier](https://centminmod.com/myemailverifier), [CaptainVerify](https://centminmod.com/captainverify), [Proofy.io](https://centminmod.com/proofy), [Zerobounce](https://centminmod.com/zerobounce), [Reoon](https://centminmod.com/reoon), [Bouncify](https://centminmod.com/bouncify), [Bounceless](https://centminmod.com/bounceless) ([jump straight to email verification provider API speed and result benchmarks](#email-verification-provider-api-speed-benchmarks)). The `validate_emails.py` script is coded to conditionally support each commercial email verification providers' API rate limits and can be tuned via argument `-wf` work factor to adjust the number of concurrent threads used for single email address API requests. This ensures you do not waste time and credit $$$$ running into API rate limits.
The script's [API Merge support](#api-merge) also allows you to combine 2 API email verification providers results into one JSON formated output for double verification checks. The script provides a convenient way to verify the existence and deliverability of email addresses, helping you maintain a clean and accurate email list.
The script offers specific support for [Xenforo](#xenforo) forum member email list verification through dedicated Xenforo argument flags. These flags enable you to mark invalid Xenforo forum member emails and move them to a `bounce_email` status, effectively disabling Xenforo email sending to those members without actually deleting the Xenforo member account. You can then setup a Xenforo forum wide notice targetting `bounce_email` status users - prompting them to update their email addresses.
To reduce potential 3rd party email verification API costs, this script also supports [Cloudflare HTTP Forward Proxy Cache With KV Storage](#cloudflare-http-forward-proxy-cache-with-kv-storage) for both per email check and [bulk file](#emaillistveirfy-bulk-file-api-cloudflare-cache-support) API check results to be temporarily cached and return the email verification status codes at the Cloudflare CDN and Cloudflare Worker KV storage level.
Email verification results can also be optionally saved to [Amazon AWS S3 and Cloudflare R2 object storage](#s3-storage-support) for long term storage and retrieval.
You can check out [Email Verification Provider Comparison Costs](#email-verification-provider-comparison-costs) table to visually compare their costs or [jump straight to email verification provider API speed and result benchmarks](#email-verification-provider-api-speed-benchmarks).
The `validate_emails.py` email validation script was written by George Liu (eva2000) for his paid consulting clients usage. The below is public documentation for the script.
## Features
- Validates email addresses using syntax, DNS and SMTP checks
- Validates `-f` from email address's SPF, DKIM, DMARC records and logs them for troubleshooting mail deliverability
- Optionally save your email verification results to [S3 object storage providers - Cloudflare R2 or Amazon S3](#s3-storage-support)
- Support local self-hosted email verification + [API support](#api-support) for:
- [EmailListVerify](https://centminmod.com/emaillistverify) [[example](#emaillistverify-1), [bulk API](#emaillistverify-bulk-file-api)]
- [MillionVerifier](https://centminmod.com/millionverifier) [[example](#millionverifier), [bulk API](#millionverifier-bulk-file-api)]
- [MyEmailVerifier](https://centminmod.com/myemailverifier) [[example](#myemailverifier-api)]
- [CaptainVerify](https://centminmod.com/captainverify) [[example](#captainverify-api)]
- [Proofy.io](https://centminmod.com/proofy) [[example](#proofy-api)]
- [Zerobounce](https://centminmod.com/zerobounce) [[example](#zerobounce-api)]
- [Reoon](https://centminmod.com/reoon) [[example](#reoon-api)]
- [Bouncify](https://centminmod.com/bouncify) [[example](#bouncify-api)]
- [Bounceless](https://centminmod.com/bounceless) [[example](#bounceless-api)]
- [API Merge support](#api-merge) via `-apimerge` argument to merge [EmailListVerify](https://centminmod.com/emaillistverify) + [MillionVerifier](https://centminmod.com/millionverifier) API results together for more accurate email verification results.
- Supports [Cloudflare HTTP Forward Proxy Cache With KV Storage](#cloudflare-http-forward-proxy-cache-with-kv-storage) for [EmailListVerify](https://centminmod.com/emaillistverify) per email check API
- Classifies email addresses into various categories based on the syntax, DNS, and SMTP response
- Supports concurrent processing for faster validation of multiple email addresses
- Provides detailed logging for tracking the validation process
- Allows customization of delay between requests to respect email server limitations
- Supports input of email addresses via command-line arguments or a file
- Identifies disposable email addresses and free domain name provider addresses
- Checks email addresses against custom blacklists and whitelists
- Supports different test modes for syntax, DNS, SMTP, and disposable email checks
- Configurable SMTP port and TLS/SSL support
- Supports SMTP profiles. However, using SMTP relay profiles won't get accurate SMTP checks. Locally ran server SMTP checks are more accurate.
- Supports different DNS lookup methods: asyncio, concurrent, and sequential
- Supports different processing modes: thread and asyncio
- [Xenforo](#xenforo) support. Generates SQL queries for updating user status `user_state` in XenForo forum based email validation results. Allowing you to clean up your Xenforo user database's email addresses by disabling email sending to those specific bad email addresses.
## Requirements
- Python 3.6 minimum. Script tested on AlmaLinux 8 Python 3.6 and AlmaLinux 9 Python 3.9.
## Usage
1. Open a terminal or command prompt and navigate to the directory where the script is located.
2. Run the script with the desired command-line arguments.
```
python validate_emails.py
usage: validate_emails.py [-h] -f FROM_EMAIL [-e EMAILS] [-l LIST_FILE] [-b BATCH_SIZE] [-d] [-v] [-delay DELAY] [--cache-timeout CACHE_TIMEOUT]
[-t TIMEOUT] [-r RETRIES] [-tm {syntax,dns,smtp,all,disposable}] [-dns {asyncio,concurrent,sequential}]
[-p {thread,asyncio}] [-bl BLACKLIST_FILE] [-wl WHITELIST_FILE] [-smtp {default,ses,generic,rotate}] [-xf]
[-xfdb XF_DATABASE] [-xfprefix XF_PREFIX] [-profile] [-wf WORKER_FACTOR]
[-api {emaillistverify,millionverifier,captainverify,proofy,myemailverifier,zerobounce,reoon,bouncify,bounceless}]
[-apikey EMAILLISTVERIFY_API_KEY] [-apikey_mv MILLIONVERIFIER_API_KEY]
[-apibulk {emaillistverify,millionverifier,proofy,bounceless}] [-apikey_cv CAPTAINVERIFY_API_KEY]
[-apikey_pf PROOFY_API_KEY] [-apiuser_pf PROOFY_USER_ID] [-pf_max_connections PROOFY_MAX_CONNECTIONS]
[-pf_batchsize PROOFY_BATCH_SIZE] [-apikey_mev MYEMAILVERIFIER_API_KEY] [-apikey_zb ZEROBOUNCE_API_KEY]
[-apikey_rn REOON_API_KEY] [-reoon_mode {quick,power}] [-reoon_max_connections REOON_MAX_CONNECTIONS]
[-apikey_bf BOUNCIFY_API_KEY] [-apikey_bl BOUNCELESS_API_KEY] [-mev_max_connections MEV_MAX_CONNECTIONS] [-apimerge]
[-apicache {emaillistverify,zerobounce,millionverifier}] [-apicachettl APICACHETTL]
[-apicachecheck {count,list,purge}] [-apicache-purge] [-store {r2,s3}] [-store-list]
validate_emails.py: error: the following arguments are required: -f/--from_email
```
The available arguments are:
- `-f`, `--from_email` (required):
- Description: The email address to use in the MAIL FROM command.
- `-e`, `--emails` (optional):
- Description: A single email or comma-separated list of emails to check.
- `-l`, `--list_file` (optional):
- Description: The path to a file containing emails, one per line.
- `-b`, `--batch_size` (optional):
- Description: The number of concurrent processes to use (default is 1).
- `-d`, `--debug` (optional):
- Description: Enable debug logging for more verbose output.
- `-v`, `--verbose` (optional):
- Description: Enable verbose output.
- `-delay`, `--delay` (optional):
- Description: The delay between requests in seconds (default is 1).
- `--cache-timeout` (optional):
- Description: Set the caching resolver timeout value (default is 14400).
- `-t`, `--timeout` (optional):
- Description: The timeout in seconds for SMTP connection and commands (default is 10).
- `-r`, `--retries` (optional):
- Description: The number of retries for temporary failures and timeouts (default is 3).
- `-tm`, `--test-mode` (optional):
- Description: The test mode to use for email validation. Available options are:
- `syntax`: Check email format syntax only.
- `dns`: Perform DNS validation only (default).
- `smtp`: Perform SMTP validation only.
- `all`: Perform all validations.
- `disposable`: Check if the email is from a disposable domain.
- `-dns`, `--dns-method` (optional):
- Description: The DNS lookup method to use. Available options are:
- `asyncio`: Use asynchronous DNS lookups using the asyncio library (default).
- `concurrent`: Use concurrent DNS lookups using the concurrent.futures library.
- `sequential`: Use basic sequential DNS lookups.
- `-p`, `--process_mode` (optional):
- Description: The processing mode to use for the `process_emails` function. Available options are:
- `thread`: Use thread-based processing (default).
- `asyncio`: Use asyncio-based processing.
- `-bl`, `--blacklist_file` (optional):
- Description: The path to a file containing blacklisted domains, one per line.
- `-wl`, `--whitelist_file` (optional):
- Description: The path to a file containing whitelisted domains, one per line.
- `-smtp`, `--smtp_profile` (optional):
- Description: The SMTP profile to use for email validation. Available options are:
- `default`: Use the default SMTP settings (default).
- `ses`: Use Amazon SES SMTP settings via `ses.ini` config file.
- `generic`: Use generic SMTP settings via `smtp.ini` config file.
- `rotate`: Use multiple SMTP profile settings via `rotate.ini` config file which you can rotate through.
- `-xf`, `--xf_sql` (optional):
- Description: Generate SQL queries for updating `user_state` in XenForo for emails with specific statuses.
- `-xfdb`, `--xf_database` (optional):
- Description: The XenForo database name (default is 'DATABNAME').
- `-xfprefix`, `--xf_prefix` (optional):
- Description: The XenForo table prefix (default is 'xf_').
- `-profile`, `--profile` (optional):
- Description: Enable profiling of the script.
- `-wf`, `--worker-factor` (optional):
- Description: The worker factor for calculating the maximum number of worker threads (default is 16).
- `-api`, `--api` (optional):
- Description: Specify the API to use for email verification. Available options are:
- `emaillistverify`: Use the EmailListVerify API.
- `millionverifier`: Use the MillionVerifier API.
- `myemailverifier`: Use the MyEmailVerifier API.
- `captainverify`: Use the CaptainVerify API.
- `proofy`: Use the Proofy API.
- `zerobounce`: Use the Zerobounce.net API.
- `reoon`: Use the Zerobounce.net API.
- `bouncify`: Use the Bouncify API.
- `bounceless`: Use the Bounceless API.
- `-apimerge`, `--api_merge` (optional):
- Description: Merge and combine `emaillistverify` or `millionverifier` API results into one result
- `-apibulk`, `--api_bulk` (optional):
- Description: Use `emaillistverify` or `millionverifier` values for Bulk file API method.
- `-apikey`, `--emaillistverify_api_key` (optional):
- Description: The API key for the EmailListVerify service.
- `-apikey_mv`, `--millionverifier_api_key` (optional):
- Description: The API key for the MillionVerifier service.
- `-apikey_mev`, `--myemailverifier-api-key` (optional):
- Description: The API key for the MyEmailVerifier service.
- `-apikey_cv`, `--captainverify_api_key` (optional):
- Description: The API key for the CaptainVerify service.
- `-apikey_zb`, `--zerobounce_api_key` (optional):
- Description: The API key for the Zerobounce.net service.
- `-apikey_rn`, `--reoon_api_key` (optional):
- Description: The API key for the Reoon service.
- `-reoon_mode`, `--reoon_mode`
- Description: Reoon verification mode `quick` or `power`
- `-apikey_bf`, `--bouncify_api_key`
- Description: The API key for the Bouncify service.
- `-apikey_bl`, `--bounceless_api_key`
- Description: The API key for the Bounceless service.
- `-apikey_pf`, `--proofy_api_key` (optional):
- Description: The API key for the Proofy service.
- `-apiuser_pf`, `--proofy_user_id` (optional):
- Description: The Proofy userid.
- `-pf_max_connections` (optional):
- Description: Maximum number of concurrent connections for the Proofy.io API (default: 1)
- `-mev_max_connections` (optional):
- Description: Maximum number of concurrent connections for the MyEmailVerifier API (default: 1)
- `-reoon_max_connections` (optional):
- Description: Maximum number of concurrent connections for the Reoon API (default: 5)
- `-apicache`, `--api_cache` (optional):
- Description: Set the appropriate API's Cloudflare Worker KV cacheKey prefix. Available options are:
- `emaillistverify`: Use with the EmailListVerify API.
- `zerobounce`: Use with the ZeroBounce API.
- `millionverifier`: Use with the MillionVerifier API.
- `-apicachettl` (optional):
- Description: this sets the cache TTL duration in seconds for how long Cloudflare CDN/KV stores in cache (default: 300 seconds)
- `-apicachecheck` (optional):
- Description: operates when `-apicachettl` is set and takes `count` or `list` or `purge` options to query the Cloudflare KV storage cache to count number of cached entries or list the entries themselves
- `-apicache-purge` (optional):
- Description: purges Cloudflare CDN/KV cache when `-apicachecheck` set to `purge` option
Validates `-f` from email address's SPF, DKIM, DMARC records when argument is passed and logs them
```
python validate_emails.py -f user@domain1.com
```
```
cat email_verification_log_2024-05-05_01-54-51.log
2024-05-05 01:54:51,105 - INFO - SPF record found for user@domain1.com
2024-05-05 01:54:51,105 - INFO - SPF record: "v=spf1 include:_spf.google.com +a +mx ~all"
2024-05-05 01:54:51,142 - ERROR - Error checking DKIM for user@domain1.com with selector default: The DNS response does not contain an answer to the question: default._domainkey.domain1.com. IN TXT
2024-05-05 01:54:51,174 - INFO - DKIM record found for user@domain1.com with selector google
2024-05-05 01:54:51,174 - INFO - DKIM record: "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCMF3nm2Za6EN0udFQLb35Jcy3u63iCzdaojAkVsCISJsHKe2ThgSsriU1MRm32abcd/u2aNEAxJ3jhN7TbkuV8j7xppV5PW+abcd/84lxa2xTlngXOymlJWleoTZUQQPkmkB66IO/XqZVj7RrF/Iru1qpAvJ9aW+6vCZEJFjCZowIDAQAB"
2024-05-05 01:54:51,237 - WARNING - Error checking DMARC for user@domain1.com with record name _dmarc: The DMARC record must be located at _dmarc._dmarc.domain1.com, not _dmarc.domain1.com
2024-05-05 01:54:51,562 - INFO - DMARC record found for user@domain1.com with record name _dmarc.mail
2024-05-05 01:54:51,562 - INFO - DMARC record: v=DMARC1; p=quarantine; sp=none; rua=mailto:re+xxx@inbound.dmarcdigests.com,mailto:re+yyy@dmarc.postmarkapp.com; aspf=r; pct=100
2024-05-05 01:54:51,562 - INFO - DMARC policy check passed for user@domain1.com
```
Example usage for DNS only checks (skipping SMTP checks):
```
python validate_email.py -f sender@example.com -e user1@example.com,user2@example.com -tm dns
```
Example usage for DNS + SMTP checks:
```
python validate_email.py -f sender@example.com -e user1@example.com,user2@example.com -tm all
```
**Notes:**
- If the `-tm` flag is not passed, it defaults to DNS test mode only.
- Tuning `-wf` worker factor value for calculating the maximum number of worker threads can speed up the processing of emails when in `-p thread` process mode or when no `-p` flag is set. Example benchmark of 10,000 email addresses for `-tm dns -p thread` for DNS only tests (using default `-dns asyncio`) using process method = thread took 4mins 40s to complete with work factor `-wf 4`. However, with `-wf 80` time to completion took ~40s on a Intel Core i7 4790K 4C/8T or ~33s on a Intel Xeon E-2276G 6C/12T based dedicated server.
## Output
The script outputs the validation results in JSON format. Each email address is represented by an object with the following fields:
- `email`: The email address.
- `status`: The validation status of the email address. Possible values are:
- `valid_format`: The email address has a valid format.
- `invalid`: The email address has an invalid format.
- `valid_dns`: The email address has valid DNS records.
- `invalid_dns`: The email address has invalid DNS records.
- `ok`: The email address passed SMTP validation.
- `unknown_email`: The email address is unknown or doesn't exist.
- `temporary_failure`: A temporary failure occurred during SMTP validation.
- `syntax_or_command_error`: An SMTP syntax or command error occurred.
- `transaction_failed`: The SMTP transaction failed.
- `timeout`: A timeout occurred during SMTP validation.
- `skipped_smtp_check`: SMTP validation was skipped based on the test mode.
- `status_code`: SMTP validation check's logged SMTP response code.
- `free_email`: Indicates whether the email address is from a free email provider. Possible values are `yes`, `no`, or `unknown`.
- `disposable_email`: Indicates whether the email address is from a disposable domain. Possible values are `yes`, `no`, or `notchecked`.
- `xf_sql` (optional): The SQL query for updating the user status in XenForo based on the validation result.
## Logging
The script generates a log file named `email_verification_log_.log` in the same directory as the script. The log file contains detailed information about the validation process, including any errors or warnings encountered.
If `-v` verbose mode is used with `-tm all` for SMTP domain MX record checks, an additional debug log file named `email_verification_log_debug_.log` is generated in the same directory as the script. The debug log has extended logging of the SMTP responses.
## Configuration
### `validate_emails.ini` config file
Add `validate_emails.ini` config file support so Cloudflare Worker KV caching endpoint url can be defined outside of the script. If `validate_emails.ini` doesn't exist, you'll get this message
```
./validate_emails.py
Error: 'api_url' setting not found in 'validate_emails.ini' file.
Please create the 'validate_emails.ini' file in the same directory as the script and add the 'api_url' setting.
```
`validate_emails.ini` contents setting `api_url`
```
[settings]
api_url=http=https://your_cf_worker.com
```
### S3 Storage Support
Commercial email verification providers usually only store your file based uploaded or bulk file API uploaded files for a defined duration i.e. 15 to 30 days before they are deleted. And per email check API results are usually not stored at all. So if you need to store your per email check or bulk file API email verification results for longer, the `validate_emails.py` script now supports saving your results to S3 object storage providers - Cloudflare R2 or Amazon AWS S3. Saving such email verification result logs might be usual for historic comparisons and checks. According to [ZeroBounce](https://www.zerobounce.net/email-list-decay/), on average an email list decays by an average of 25.74% yearly with the leading causes of bounced emails being invalid email addresses and catch-all email addresses.
Add optional Cloudflare R2 S3 object storage or Amazon AWS S3 object storage which will allow you to save your `validate_emails.py` ran JSON output in externel Cloudflare R2 or Amazon AWS S3 object storage buckets via `validate_emails.ini` defined:
For Cloudflare R2
```
[r2]
endpoint_url = https://your-account-id.r2.cloudflarestorage.com
aws_access_key_id = your-r2-access-key-id
aws_secret_access_key = your-r2-secret-access-key
bucket_name = your-r2-bucket-name
```
For Amazon AWS S3
```
[s3]
endpoint_url = https://your-s3-endpoint-url
aws_access_key_id = your-s3-access-key-id
aws_secret_access_key = your-s3-secret-access-key
bucket_name = your-s3-bucket-name
```
Below example to send `validate_emails.py` script results to Cloudflare R2 S3 object storage via `-store r2` argument. Using [EmailListVerify](https://centminmod.com/emaillistverify) per email check API `-api emaillistverify -apikey $elvkey` + Cloudflare cached for 120 seconds `-apicache emaillistverify -apicachettl 120`
```
time python validate_emails.py -f user@domain1.com -e hnyfmw@canadlan-drugs.com,hnyfmw2@canadlan-drugs.com,hnyfmw3@canadlan-drugs.com -api emaillistverify -apikey $elvkey -apicache emaillistverify -apicachettl 120 -tm all -store r2
Output stored successfully in R2: emailapi-emaillistverify-cached/output_20240511051940.json
[
{
"email": "hnyfmw@canadlan-drugs.com",
"status": "unknown",
"status_code": null,
"free_email": "no",
"disposable_email": "no"
},
{
"email": "hnyfmw2@canadlan-drugs.com",
"status": "unknown",
"status_code": null,
"free_email": "no",
"disposable_email": "no"
},
{
"email": "hnyfmw3@canadlan-drugs.com",
"status": "unknown",
"status_code": null,
"free_email": "no",
"disposable_email": "no"
}
]
real 0m1.663s
user 0m0.391s
sys 0m0.039s
```
`validate_emails.py` script's Cloudflare R2 saved `emailapi-emaillistverify-cached/output_20240511051940.json` log contents
```
cat emailapi-emaillistverify-cached/output_20240511051940.json
[{"email": "hnyfmw@canadlan-drugs.com", "status": "unknown", "status_code": null, "free_email": "no", "disposable_email": "no"}, {"email": "hnyfmw2@canadlan-drugs.com", "status": "unknown", "status_code": null, "free_email": "no", "disposable_email": "no"}, {"email": "hnyfmw3@canadlan-drugs.com", "status": "unknown", "status_code": null, "free_email": "no", "disposable_email": "no"}]
```
`validate_emails.py` run log inspection
```
cat $(ls -Art | tail -3 | grep 'email_verification')
2024-05-11 05:14:23,074 - INFO - Checking cache for email: hnyfmw@canadlan-drugs.com
2024-05-11 05:14:23,075 - INFO - Checking cache for email: hnyfmw2@canadlan-drugs.com
2024-05-11 05:14:23,075 - INFO - Checking cache for email: hnyfmw3@canadlan-drugs.com
2024-05-11 05:14:25,206 - INFO - Cache result: unknown
2024-05-11 05:14:25,966 - INFO - Cache result: unknown
2024-05-11 05:14:26,092 - INFO - Cache result: unknown
```
Non-cached [EmailListVerify](https://centminmod.com/emaillistverify) per email check API `-api emaillistverify -apikey $elvkey` run
```
time python validate_emails.py -f user@domain1.com -e hnyfmw@canadlan-drugs.com,hnyfmw2@canadlan-drugs.com,hnyfmw3@canadlan-drugs.com -api emaillistverify -apikey $elvkey -tm all -store r2
Output stored successfully in R2: emailapi-emaillistverify/output_20240511055822.json
[
{
"email": "hnyfmw@canadlan-drugs.com",
"status": "unknown",
"status_code": null,
"free_email": "no",
"disposable_email": "no"
},
{
"email": "hnyfmw2@canadlan-drugs.com",
"status": "unknown",
"status_code": null,
"free_email": "no",
"disposable_email": "no"
},
{
"email": "hnyfmw3@canadlan-drugs.com",
"status": "unknown",
"status_code": null,
"free_email": "no",
"disposable_email": "no"
}
]
real 0m10.612s
user 0m0.541s
sys 0m0.033s
```
`validate_emails.py` script's Cloudflare R2 saved `emailapi-emaillistverify/output_20240511055822.json` log contents
```
cat emailapi-emaillistverify/output_20240511055822.json
[{"email": "hnyfmw@canadlan-drugs.com", "status": "unknown", "status_code": null, "free_email": "no", "disposable_email": "no"}, {"email": "hnyfmw2@canadlan-drugs.com", "status": "unknown", "status_code": null, "free_email": "no", "disposable_email": "no"}, {"email": "hnyfmw3@canadlan-drugs.com", "status": "unknown", "status_code": null, "free_email": "no", "disposable_email": "no"}]
```
### -smtp ses
The script supports loading SMTP settings from a configuration file named `ses.ini`. The file should have the following format:
```
[ses]
server = your_ses_smtp_server
port = your_ses_smtp_port
use_tls = True
username = your_ses_username
password = your_ses_password
```
If the `ses` SMTP profile is specified using the `-smtp ses` argument, the script will load the SMTP settings from the `ses.ini` file.
However, using SMTP relay profiles won't get accurate SMTP checks. Locally ran server SMTP checks are more accurate.
### -smtp rotate
The `rotate.ini` file is used to store multiple SMTP profiles that can be rotated during the email verification process with `-smtp rotate` argument is passed on command line. Each profile represents a different SMTP server configuration.
Here's an example of how the `rotate.ini` file can be populated:
```ini
[profile1]
server = smtp1.example.com
port = 587
use_tls = yes
username = user1@example.com
password = password1
[profile2]
server = smtp2.example.com
port = 465
use_tls = yes
username = user2@example.com
password = password2
[profile3]
server = smtp3.example.com
port = 25
use_tls = no
username = user3@example.com
password = password3
```
In this example, the `rotate.ini` file contains three SMTP profiles: `profile1`, `profile2`, and `profile3`. Each profile is defined as a separate section in the INI file.
The properties for each profile are:
- `server`: The hostname or IP address of the SMTP server.
- `port`: The port number to use for the SMTP connection (e.g., 25, 465, 587).
- `use_tls`: Indicates whether to use TLS encryption for the SMTP connection. Set to `yes` or `no`.
- `username`: The username for SMTP authentication.
- `password`: The password for SMTP authentication.
You can add as many profiles as needed to the `rotate.ini` file, each with its own unique section name and SMTP server settings.
When the `rotate` profile is selected using the `-smtp rotate` option, the script will randomly choose one of the profiles defined in `rotate.ini` for each email verification request. This allows for distributing the email verification load across multiple SMTP servers.
Make sure to populate the `rotate.ini` file with the appropriate SMTP server settings for each profile before using the `rotate` profile option.
However, using SMTP relay profiles won't get accurate SMTP checks. Locally ran server SMTP checks are more accurate.
### -smtp generic
The `smtp.ini` file is used to store the configuration for a single generic SMTP server that can be used for email verification with `-smtp generic` argument is passed on command line.
Here's an example of how the `smtp.ini` file can be populated:
```ini
[generic]
server = smtp.example.com
port = 587
use_tls = yes
username = user@example.com
password = password
```
In this example, the `smtp.ini` file contains a single section named `[generic]`, which represents the generic SMTP profile.
The properties for the generic profile are:
- `server`: The hostname or IP address of the SMTP server.
- `port`: The port number to use for the SMTP connection (e.g., 25, 465, 587).
- `use_tls`: Indicates whether to use TLS encryption for the SMTP connection. Set to `yes` or `no`.
- `username`: The username for SMTP authentication.
- `password`: The password for SMTP authentication.
When the `generic` profile is selected using the `-smtp generic` option, the script will use the SMTP server settings defined in the `smtp.ini` file for all email verification requests.
Make sure to populate the `smtp.ini` file with the appropriate SMTP server settings before using the `generic` profile option.
However, using SMTP relay profiles won't get accurate SMTP checks. Locally ran server SMTP checks are more accurate.
## Customization
You can customize the behavior of the script by modifying the following variables in the code:
- `DEFAULT_DELAY`: The default delay between requests in seconds (default is 1).
- `LARGE_PROVIDER_DELAY`: The delay for large email providers in seconds (default is 2).
- `LARGE_PROVIDERS`: A list of large email providers that require a longer delay between requests.
## Troubleshooting
If you encounter any issues or errors while running the script, consider the following:
- Ensure that you have installed all the required Python packages.
- Check the log file for detailed error messages and traceback information.
- Verify that the email addresses provided are valid and properly formatted.
- Make sure you have a stable internet connection for accurate DNS and SMTP validations.
- If you are using custom blacklist or whitelist files, ensure that they exist and contain valid domain entries.
## `domain_responses` function
1. The `smtp_check` function now accepts a `domain_responses` parameter, which is a dictionary to store the SMTP responses for each domain. It records the SMTP response code and message for each domain.
2. The `process_emails` function creates a `domain_responses` dictionary to store the SMTP responses per domain. It passes this dictionary to the `validate_and_classify` function.
3. After processing the emails, the `process_emails` function analyzes the SMTP responses for each domain. It calculates the failure rate based on the SMTP response codes. If the failure rate exceeds a predefined threshold (e.g., 5% in this example), it logs a warning message indicating that the verification strategy needs to be adjusted for that domain.
4. The `validate_and_classify` function now accepts the `domain_responses` parameter and passes it to the `smtp_check` function.
With these changes, the script will proactively monitor SMTP responses and adjust the verification strategy based on the feedback from the servers.
Example in log
```
grep failure email_verification_log_2024-05-03_08-45-05.log
2024-05-03 08:45:07,910 - INFO - Acceptable failure rate (0.00%) for domain gmail.com.
```
# Example Usage
AWS SES SMTP support and Xenforo support to display MySQL query for bad emails only to set their status to `email_bounce` passing flags `-xf` and `-xfdb xenforo` and `-xfprefix xf_`
```
python validate_emails.py -f user@domain1.com -l emaillist.txt -smtp ses -xf -xfdb xenforo -xfprefix xf_
```
`ses.ini`
```
[ses]
server = email-smtp.us-west-2.amazonaws.com
port = 587
use_tls = true
username = YOUR_AWS_SES_SMTP_USERNAME
password = YOUR_AWS_SES_SMTP_PASSWORD
```
### Xenforo
Xenforo support to display MySQL query for bad emails only to set their status to `email_bounce` passing flags `-xf` and `-xfdb xenforo` and `-xfprefix xf_` with `disposable_email` status field
Xenforo arguments mode for `-xf -xfdb xenforo -xfprefix xf_` has ben updated to output
- `xf_sql` for MySQL query you can run on SSH command line. The command's double quotes are escaped `\"` so you remove the backslash manually, or use below outlined `jq` tool to automatically remove it
- `xf_sql_batch` for MySQL query run in MySQL client, phpmyadmin etc
- `xf_sql_user` for MySQL query you can run on SSH command line to look up Xenforo user details for that email address. The command's double quotes are escaped `\"` so you remove the backslash manually, or use below outlined `jq` tool to automatically remove it
```
python validate_emails.py -f user@domain1.com -l emaillist.txt -tm all -xf -xfdb xenforo -xfprefix xf_
[
{
"email": "user@mailsac.com",
"status": "ok",
"status_code": 250,
"free_email": "yes",
"disposable_email": "yes",
"xf_sql": "mysql -e \"UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'user@mailsac.com';\" xenforo",
"xf_sql_batch": "UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'user@mailsac.com';",
"xf_sql_user": "mysql -e \"SELECT user_id, username, email, user_group_id, secondary_group_ids, message_count, register_date, last_activity, user_state, is_moderator, is_admin, is_banned FROM xf_user WHERE email = 'user@mailsac.com'\\G\" xenforo"
},
{
"email": "xyz@centmil1.com",
"status": "invalid",
"status_code": null,
"free_email": "unknown",
"disposable_email": "no"
},
{
"email": "user+to@domain1.com",
"status": "ok",
"status_code": 250,
"free_email": "no",
"disposable_email": "no"
},
{
"email": "xyz@domain1.com",
"status": "unknown_email",
"status_code": 550,
"free_email": "no",
"disposable_email": "no",
"xf_sql": "mysql -e \"UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'xyz@domain1.com';\" xenforo",
"xf_sql_batch": "UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'xyz@domain1.com';",
"xf_sql_user": "mysql -e \"SELECT user_id, username, email, user_group_id, secondary_group_ids, message_count, register_date, last_activity, user_state, is_moderator, is_admin, is_banned FROM xf_user WHERE email = 'xyz@domain1.com'\\G\" xenforo"
},
{
"email": "abc@domain1.com",
"status": "unknown_email",
"status_code": 550,
"free_email": "no",
"disposable_email": "no",
"xf_sql": "mysql -e \"UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'abc@domain1.com';\" xenforo",
"xf_sql_batch": "UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'abc@domain1.com';",
"xf_sql_user": "mysql -e \"SELECT user_id, username, email, user_group_id, secondary_group_ids, message_count, register_date, last_activity, user_state, is_moderator, is_admin, is_banned FROM xf_user WHERE email = 'abc@domain1.com'\\G\" xenforo"
},
{
"email": "123@domain1.com",
"status": "unknown_email",
"status_code": 550,
"free_email": "no",
"disposable_email": "no",
"xf_sql": "mysql -e \"UPDATE xf_user SET user_state = 'email_bounce' WHERE email = '123@domain1.com';\" xenforo",
"xf_sql_batch": "UPDATE xf_user SET user_state = 'email_bounce' WHERE email = '123@domain1.com';",
"xf_sql_user": "mysql -e \"SELECT user_id, username, email, user_group_id, secondary_group_ids, message_count, register_date, last_activity, user_state, is_moderator, is_admin, is_banned FROM xf_user WHERE email = '123@domain1.com'\\G\" xenforo"
},
{
"email": "pop@domain1.com",
"status": "unknown_email",
"status_code": 550,
"free_email": "no",
"disposable_email": "no",
"xf_sql": "mysql -e \"UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'pop@domain1.com';\" xenforo",
"xf_sql_batch": "UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'pop@domain1.com';",
"xf_sql_user": "mysql -e \"SELECT user_id, username, email, user_group_id, secondary_group_ids, message_count, register_date, last_activity, user_state, is_moderator, is_admin, is_banned FROM xf_user WHERE email = 'pop@domain1.com'\\G\" xenforo"
},
{
"email": "pip@domain1.com",
"status": "unknown_email",
"status_code": 550,
"free_email": "no",
"disposable_email": "no",
"xf_sql": "mysql -e \"UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'pip@domain1.com';\" xenforo",
"xf_sql_batch": "UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'pip@domain1.com';",
"xf_sql_user": "mysql -e \"SELECT user_id, username, email, user_group_id, secondary_group_ids, message_count, register_date, last_activity, user_state, is_moderator, is_admin, is_banned FROM xf_user WHERE email = 'pip@domain1.com'\\G\" xenforo"
},
{
"email": "user@tempr.email",
"status": "ok",
"status_code": 250,
"free_email": "no",
"disposable_email": "yes",
"xf_sql": "mysql -e \"UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'user@tempr.email';\" xenforo",
"xf_sql_batch": "UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'user@tempr.email';",
"xf_sql_user": "mysql -e \"SELECT user_id, username, email, user_group_id, secondary_group_ids, message_count, register_date, last_activity, user_state, is_moderator, is_admin, is_banned FROM xf_user WHERE email = 'user@tempr.email'\\G\" xenforo"
},
{
"email": "info@domain2.com",
"status": "ok",
"status_code": 250,
"free_email": "no",
"disposable_email": "no"
},
{
"email": "user@gmail.com",
"status": "ok",
"status_code": 250,
"free_email": "yes",
"disposable_email": "no"
},
{
"email": "op999@gmail.com",
"status": "unknown_email",
"status_code": 550,
"free_email": "yes",
"disposable_email": "no",
"xf_sql": "mysql -e \"UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'op999@gmail.com';\" xenforo",
"xf_sql_batch": "UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'op999@gmail.com';",
"xf_sql_user": "mysql -e \"SELECT user_id, username, email, user_group_id, secondary_group_ids, message_count, register_date, last_activity, user_state, is_moderator, is_admin, is_banned FROM xf_user WHERE email = 'op999@gmail.com'\\G\" xenforo"
},
{
"email": "user@yahoo.com",
"status": "ok",
"status_code": 250,
"free_email": "yes",
"disposable_email": "no"
},
{
"email": "user1@outlook.com",
"status": "ok",
"status_code": 250,
"free_email": "yes",
"disposable_email": "no"
},
{
"email": "user2@hotmail.com",
"status": "ok",
"status_code": 250,
"free_email": "yes",
"disposable_email": "no"
}
]
```
Use `jq` tool to filter for `xf_sql` only
```
python validate_emails.py -f user@domain1.com -l emaillist.txt -tm all -xf -xfdb xenforo -xfprefix xf_ | jq -r '.[] | select(.xf_sql) | .xf_sql'
mysql -e "UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'user@mailsac.com';" xenforo
mysql -e "UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'xyz@domain1.com';" xenforo
mysql -e "UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'abc@domain1.com';" xenforo
mysql -e "UPDATE xf_user SET user_state = 'email_bounce' WHERE email = '123@domain1.com';" xenforo
mysql -e "UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'pop@domain1.com';" xenforo
mysql -e "UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'pip@domain1.com';" xenforo
mysql -e "UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'user@tempr.email';" xenforo
mysql -e "UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'op999@gmail.com';" xenforo
```
Use `jq` tool to filter for `xf_sql_batch` only. You can pipe or place this output into a `update.sql` file and import into your Xenforo MySQL database to batch update the user's `user_state`
```
python validate_emails.py -f user@domain1.com -l emaillist.txt -tm all -xf -xfdb xenforo -xfprefix xf_ | jq -r '.[] | select(.xf_sql_batch) | .xf_sql_batch'
UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'user@mailsac.com';
UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'xyz@domain1.com';
UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'abc@domain1.com';
UPDATE xf_user SET user_state = 'email_bounce' WHERE email = '123@domain1.com';
UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'pop@domain1.com';
UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'pip@domain1.com';
UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'user@tempr.email';
UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'op999@gmail.com';
```
Use `jq` tool to filter for `xf_sql_user` only. This allows you to run on SSH command line the Xenforo database lookup for the Xenforo user details for the specific email address
```
python validate_emails.py -f user@domain1.com -l emaillist.txt -tm all -xf -xfdb xenforo -xfprefix xf_ | jq -r '.[] | select(.xf_sql_user) | .xf_sql_user'
mysql -e "SELECT user_id, username, email, user_group_id, secondary_group_ids, message_count, register_date, last_activity, user_state, is_moderator, is_admin, is_banned FROM xf_user WHERE email = 'user@mailsac.com'\G" xenforo
mysql -e "SELECT user_id, username, email, user_group_id, secondary_group_ids, message_count, register_date, last_activity, user_state, is_moderator, is_admin, is_banned FROM xf_user WHERE email = 'xyz@domain1.com'\G" xenforo
mysql -e "SELECT user_id, username, email, user_group_id, secondary_group_ids, message_count, register_date, last_activity, user_state, is_moderator, is_admin, is_banned FROM xf_user WHERE email = 'abc@domain1.com'\G" xenforo
mysql -e "SELECT user_id, username, email, user_group_id, secondary_group_ids, message_count, register_date, last_activity, user_state, is_moderator, is_admin, is_banned FROM xf_user WHERE email = '123@domain1.com'\G" xenforo
mysql -e "SELECT user_id, username, email, user_group_id, secondary_group_ids, message_count, register_date, last_activity, user_state, is_moderator, is_admin, is_banned FROM xf_user WHERE email = 'pop@domain1.com'\G" xenforo
mysql -e "SELECT user_id, username, email, user_group_id, secondary_group_ids, message_count, register_date, last_activity, user_state, is_moderator, is_admin, is_banned FROM xf_user WHERE email = 'pip@domain1.com'\G" xenforo
mysql -e "SELECT user_id, username, email, user_group_id, secondary_group_ids, message_count, register_date, last_activity, user_state, is_moderator, is_admin, is_banned FROM xf_user WHERE email = 'user@tempr.email'\G" xenforo
mysql -e "SELECT user_id, username, email, user_group_id, secondary_group_ids, message_count, register_date, last_activity, user_state, is_moderator, is_admin, is_banned FROM xf_user WHERE email = 'op999@gmail.com'\G" xenforo
```
Example running one of these commands for `pop@domain1.com` on server where Xenforo is installed. The `user_state` would either be `valid` prior to running above `UPDATE` command or `email_bounce` after running `UPDATE` command.
```
mysql -e "SELECT user_id, username, email, user_group_id, secondary_group_ids, message_count, register_date, last_activity, user_state, is_moderator, is_admin, is_banned FROM xf_user WHERE email = 'pop@domain1.com'\G" xenforo
*************************** 1. row ***************************
user_id: 11
username: pop
email: pop@domain1.com
user_group_id: 2
secondary_group_ids: 3,4,6,8
message_count: 191817
register_date: 1400868747
last_activity: 1715011284
user_state: email_bounce
is_moderator: 1
is_admin: 1
is_banned: 0
```
`register_date` date
```
date -d @1400868747
Fri May 23 18:12:27 UTC 2014
```
`last_activity` date
```
date -d @1715011284
Mon May 6 16:01:24 UTC 2024
```
```
python validate_emails.py -f user@domain1.com -l emaillist.txt -tm all -xf -xfdb xenforo -xfprefix xf_ | jq 'map({ status: .status }) | group_by(.status) | map({ status: .[0].status, count: length })'
[
{
"status": "invalid",
"count": 1
},
{
"status": "ok",
"count": 8
},
{
"status": "unknown_email",
"count": 6
}
]
```
### Xenforo Email Bounce Log
Example lookup for Xenforo forum's email bounce log via my custom `xf_bounce_log.py` for email address `hnyfmw@canadlan-drugs.com` that is bouncing emails. And using `validate_emails.py` script's local and API to lookup email address status.
```
./xf_bounce_log.py -d $xfdb -n 10 -s desc | jq '.[] | select(.recipient == "hnyfmw@canadlan-drugs.com") | {bounce_id, message_type, action_taken, user_id, recipient, status_code, diagnostic_info, "Delivered-To": .raw_message["Delivered-To:"], "Delivery-date": .raw_message["Delivery-date:"], "Delivery-date": .raw_message["Delivery-date:"], "Subject": .raw_message["Subject:"]}'
{
"bounce_id": 203,
"message_type": "bounce",
"action_taken": "soft",
"user_id": 122136,
"recipient": "hnyfmw@canadlan-drugs.com",
"status_code": "4.4.7",
"diagnostic_info": " 550 4.4.7 Message expired: unable to deliver in 840 minutes.<421 4.4.0 Unable to lookup DNS for canadlan-drugs.com>",
"Delivered-To": "bouncer@domain1.com",
"Delivery-date": "Fri, 26 Apr 2024 15:44:06 +0000",
"Subject": "Delivery Status Notification (Failure)"
}
```
`validate_emails.py` self-hosted local email verification check for syntax, DNS and SMTP checks for `hnyfmw@canadlan-drugs.com`
```
time python validate_emails.py -f user@domain1.com -e hnyfmw@canadlan-drugs.com -tm all
[
{
"email": "hnyfmw@canadlan-drugs.com",
"status": "invalid",
"status_code": null,
"free_email": "unknown",
"disposable_email": "no"
}
]
real 0m0.932s
user 0m0.428s
sys 0m0.025s
```
`validate_emails.py` using external [EmailListVerify](https://centminmod.com/emaillistverify) API email verification check
```
time python validate_emails.py -f user@domain1.com -e hnyfmw@canadlan-drugs.com -tm all -api emaillistverify -apikey $elvkey
[
{
"email": "hnyfmw@canadlan-drugs.com",
"status": "unknown",
"status_code": null,
"free_email": "no",
"disposable_email": "no"
}
]
real 0m2.626s
user 0m0.461s
sys 0m0.023s
```
`validate_emails.py` using external [MillionVerifier](https://centminmod.com/millionverifier) API email verification check
```
time python validate_emails.py -f user@domain1.com -e hnyfmw@canadlan-drugs.com -tm all -api millionverifier -apikey_mv $mvkey
[
{
"email": "hnyfmw@canadlan-drugs.com",
"status": "invalid",
"status_code": null,
"free_email": "no",
"disposable_email": "no",
"free_email_api": false,
"role_api": false
}
]
real 0m1.142s
user 0m0.455s
sys 0m0.024s
```
`validate_emails.py` using external [MyEmailVerifier](https://centminmod.com/myemailverifier) API email verification check
```
time python validate_emails.py -f user@domain1.com -e hnyfmw@canadlan-drugs.com -tm all -api myemailverifier -apikey_mev $mevkey
[
{
"email": "hnyfmw@canadlan-drugs.com",
"status": "invalid",
"status_code": null,
"free_email": "no",
"disposable_email": "no"
}
]
real 0m1.823s
user 0m0.463s
sys 0m0.019s
```
`validate_emails.py` using external [CaptainVerify](https://centminmod.com/captainverify) API email verification check
```
time python validate_emails.py -f user@domain1.com -e hnyfmw@canadlan-drugs.com -tm all -api captainverify -apikey_cv $cvkey
[
{
"email": "hnyfmw@canadlan-drugs.com",
"status": "unknown",
"status_code": null,
"free_email": "no",
"disposable_email": "no"
}
]
real 0m25.264s
user 0m0.457s
sys 0m0.022s
```
Unfortunately, I ran out of credits to test with [Proofy.io](https://centminmod.com/proofy).
`validate_emails.py` using external [Zerobounce](https://centminmod.com/zerobounce) API enabled run `-api zerobounce -apikey_zb $zbkey -tm all` with specified email address `-e hnyfmw@canadlan-drugs.com`. The `status`, `sub_status` and `free_email_api` JSON fields are from API and `free_email` and `disposable_email` JSON fields are from local script database checks.
```
python validate_emails.py -f user@domain1.com -e hnyfmw@canadlan-drugs.com -tm all -api zerobounce -apikey_zb $zbkey -tm all
[
{
"email": "hnyfmw@canadlan-drugs.com",
"status": "invalid",
"sub_status": "no_dns_entries",
"status_code": null,
"free_email": "no",
"disposable_email": "no",
"free_email_api": "no"
}
]
```
`validate_emails.py` using external [Reoon](https://centminmod.com/reoon) API enabled run `-api reoon -apikey_rn $reokey -tm all` with specified email address `-e hnyfmw@canadlan-drugs.com`. The `status`, `role_account`, `mx_accepts_mail`, `spamtrap`, `mx_records`, `overall_score`, `safe_to_send`, `can_connect_smtp`, `inbox_full`, `catch_all`, `deliverable`, `disabled` JSON field is from API and `free_email` and `disposable_email` JSON fields are from local script database checks.
Reoon has 2 modes for single email verification API which can be set via `-reoon_mode` to a value of either `quick` or `power`. The default mode without `-reoon_mode` being set is `quick`.
```
time python validate_emails.py -f user@domain1.com -e hnyfmw@canadlan-drugs.com -tm all -api reoon -apikey_rn $reokey
[
{
"email": "hnyfmw@canadlan-drugs.com",
"status": "invalid",
"status_code": null,
"free_email": "no",
"disposable_email": "yes",
"role_account": "no",
"mx_accepts_mail": "no",
"spamtrap": "no",
"mx_records": null,
"verification_mode": "quick",
"overall_score": null,
"safe_to_send": null,
"can_connect_smtp": null,
"inbox_full": null,
"catch_all": null,
"deliverable": null,
"disabled": null
}
]
real 0m3.748s
user 0m0.358s
sys 0m0.028s
```
`validate_emails.py` using external [Bouncify](https://centminmod.com/bouncify) API enabled run `-api bouncify -apikey_bf $bfkey -tm all` with specified email address `-e hnyfmw@canadlan-drugs.com`. The `status`, `free_email_api`, `disposable_email_api`, `role_api`, and `spamtrap_api` JSON field are from API and `free_email` and `disposable_email` JSON fields are from local script database checks.
```
time python validate_emails.py -f user@domain1.com -e hnyfmw@canadlan-drugs.com -tm all -api bouncify -apikey_bf $bfkey
[
{
"email": "hnyfmw@canadlan-drugs.com",
"status": "undeliverable",
"status_code": null,
"free_email": "no",
"disposable_email": "no",
"free_email_api": "no",
"disposable_email_api": "no",
"role_api": "no",
"spamtrap_api": "no"
}
]
real 0m7.900s
user 0m0.357s
sys 0m0.030s
```
## EmailListVerify
Here's a comparison using a commercial paid service [EmailListVerify](https://centminmod.com/emaillistverify) for the same `emaillist.txt` tested above. You can sign up using my affiliate link for [EmailListVerify](https://centminmod.com/emaillistverify) and free accounts get 100 free email verifications for starters.
```csv
disposable,"user@mailsac.com"
dead_server,"xyz@centmil1.com"
ok,"user+to@domain1.com"
disposable,"user@tempr.email"
ok,"info@domain2.com"
email_disabled,"xyz@domain1.com"
email_disabled,"abc@domain1.com"
email_disabled,"123@domain1.com"
email_disabled,"pop@domain1.com"
email_disabled,"pip@domain1.com"
ok,"user@gmail.com"
email_disabled,"op999@gmail.com"
ok,"user@yahoo.com"
ok,"user1@outlook.com"
ok,"user2@hotmail.com"
```
Where [EmailListVerify](https://centminmod.com/emaillistverify) status codes are as follows:
- `ok` All is OK. The server is saying that it is ready to receive a letter to,this address, and no tricks have been detected
- `error` The server is saying that delivery failed, but no information about,the email exists
- `smtp_error` The SMTP answer from the server is invalid or the destination server,reported an internal error to us
- `smtp_protocol` The destination server allowed us to connect but the SMTP,session was closed before the email was verified
- `unknown_email` The server said that the delivery failed and that the email address does,not exist
- `attempt_rejected` The delivery failed; the reason is similar to “rejected”
- `relay_error` The delivery failed because a relaying problem took place
- `antispam_system` Some anti-spam technology is blocking the,verification progress
- `email_disabled` The email account is suspended, disabled, or limited and can not,receive emails
- `domain_error` The email server for the whole domain is not installed or is,incorrect, so no emails are deliverable
- `ok_for_all` The email server is saying that it is ready to accept letters,to any email address
- `dead_server` The email server is dead, and no connection to it could be established
- `syntax_error` There is a syntax error in the email address
- `unknown` The email delivery failed, but no reason was given
- `accept_all` The server is set to accept all emails at a specific domain.,These domains accept any email you send to them
- `disposable` The email is a temporary address to receive letters and expires,after certain time period
- `spamtrap` The email address is maintained by an ISP or a third party,which neither clicks nor opens emails
- `invalid_mx` An undocumentated status value that isn't in their documentation. As the name implies, invalid MX DNS records
Filter for disposable_email = yes
```
python validate_emails.py -f user@domain1.com -l emaillist.txt -xf -xfdb xenforo -xfprefix xf_ -tm all | jq '.[] | select(.disposable_email == "yes")'
{
"email": "user@mailsac.com",
"status": "ok",
"status_code": 250,
"free_email": "yes",
"disposable_email": "yes"
}
{
"email": "user@tempr.email",
"status": "ok",
"status_code": 250,
"free_email": "no",
"disposable_email": "yes"
}
```
Using `jq` tool to only list MySQL queries for bad emails only
```
python validate_emails.py -f user@domain1.com -l emaillist.txt -tm all -xf -xfdb xenforo -xfprefix xf_ | jq -r '.[] | select(.xf_sql) | .xf_sql'
mysql -e "UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'xyz@domain1.com';" xenforo
mysql -e "UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'abc@domain1.com';" xenforo
mysql -e "UPDATE xf_user SET user_state = 'email_bounce' WHERE email = '123@domain1.com';" xenforo
mysql -e "UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'pop@domain1.com';" xenforo
mysql -e "UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'pip@domain1.com';" xenforo
mysql -e "UPDATE xf_user SET user_state = 'email_bounce' WHERE email = 'op999@gmail.com';" xenforo
```
Filter using `jq` tool for `free_email = yes` emails only
```
python validate_emails.py -f user@domain1.com -l emaillist.txt -tm all | jq '.[] | select(.free_email == "yes")'
{
"email": "user@mailsac.com",
"status": "ok",
"status_code": 250,
"free_email": "yes",
"disposable_email": "yes"
}
{
"email": "user@gmail.com",
"status": "ok",
"status_code": 250,
"free_email": "yes",
"disposable_email": "no"
}
{
"email": "op999@gmail.com",
"status": "unknown_email",
"status_code": 550,
"free_email": "yes",
"disposable_email": "no"
}
{
"email": "user@yahoo.com",
"status": "ok",
"status_code": 250,
"free_email": "yes",
"disposable_email": "no"
}
{
"email": "user1@outlook.com",
"status": "ok",
"status_code": 250,
"free_email": "yes",
"disposable_email": "no"
}
{
"email": "user2@hotmail.com",
"status": "ok",
"status_code": 250,
"free_email": "yes",
"disposable_email": "no"
}
```
`-tm disposable` mode only skipping SMTP server checks with email addresses in file `emaillist.txt`
```
python validate_emails.py -f user@domain1.com -l emaillist.txt -tm disposable -v
[
{
"email": "user@mailsac.com",
"status": "not_disposable",
"status_code": null,
"free_email": "yes",
"disposable_email": "yes"
},
{
"email": "xyz@centmil1.com",
"status": "not_disposable",
"status_code": null,
"free_email": "no",
"disposable_email": "no"
},
{
"email": "user+to@domain1.com",
"status": "not_disposable",
"status_code": null,
"free_email": "no",
"disposable_email": "no"
},
{
"email": "xyz@domain1.com",
"status": "not_disposable",
"status_code": null,
"free_email": "no",
"disposable_email": "no"
},
{
"email": "abc@domain1.com",
"status": "not_disposable",
"status_code": null,
"free_email": "no",
"disposable_email": "no"
},
{
"email": "123@domain1.com",
"status": "not_disposable",
"status_code": null,
"free_email": "no",
"disposable_email": "no"
},
{
"email": "pop@domain1.com",
"status": "not_disposable",
"status_code": null,
"free_email": "no",
"disposable_email": "no"
},
{
"email": "pip@domain1.com",
"status": "not_disposable",
"status_code": null,
"free_email": "no",
"disposable_email": "no"
},
{
"email": "user@tempr.email",
"status": "not_disposable",
"status_code": null,
"free_email": "no",
"disposable_email": "yes"
},
{
"email": "info@domain2.com",
"status": "not_disposable",
"status_code": null,
"free_email": "no",
"disposable_email": "no"
},
{
"email": "user@gmail.com",
"status": "not_disposable",
"status_code": null,
"free_email": "yes",
"disposable_email": "no"
},
{
"email": "op999@gmail.com",
"status": "not_disposable",
"status_code": null,
"free_email": "yes",
"disposable_email": "no"
},
{
"email": "user@yahoo.com",
"status": "not_disposable",
"status_code": null,
"free_email": "yes",
"disposable_email": "no"
},
{
"email": "user1@outlook.com",
"status": "not_disposable",
"status_code": null,
"free_email": "yes",
"disposable_email": "no"
},
{
"email": "user2@hotmail.com",
"status": "not_disposable",
"status_code": null,
"free_email": "yes",
"disposable_email": "no"
}
]
```
`-tm dns` mode only skipping SMTP server checks with email addresses in file `emaillist.txt`
```
python validate_emails.py -f user@domain1.com -l emaillist.txt -tm dns -v
[
{
"email": "user@mailsac.com",
"status": "valid_dns",
"status_code": null,
"free_email": "yes",
"disposable_email": "notchecked"
},
{
"email": "xyz@centmil1.com",
"status": "invalid",
"status_code": null,
"free_email": "unknown",
"disposable_email": "notchecked"
},
{
"email": "user+to@domain1.com",
"status": "valid_dns",
"status_code": null,
"free_email": "no",
"disposable_email": "notchecked"
},
{
"email": "xyz@domain1.com",
"status": "valid_dns",
"status_code": null,
"free_email": "no",
"disposable_email": "notchecked"
},
{
"email": "abc@domain1.com",
"status": "valid_dns",
"status_code": null,
"free_email": "no",
"disposable_email": "notchecked"
},
{
"email": "123@domain1.com",
"status": "valid_dns",
"status_code": null,
"free_email": "no",
"disposable_email": "notchecked"
},
{
"email": "pop@domain1.com",
"status": "valid_dns",
"status_code": null,
"free_email": "no",
"disposable_email": "notchecked"
},
{
"email": "pip@domain1.com",
"status": "valid_dns",
"status_code": null,
"free_email": "no",
"disposable_email": "notchecked"
},
{
"email": "user@tempr.email",
"status": "valid_dns",
"status_code": null,
"free_email": "no",
"disposable_email": "notchecked"
},
{
"email": "info@domain2.com",
"status": "valid_dns",
"status_code": null,
"free_email": "no",
"disposable_email": "notchecked"
},
{
"email": "user@gmail.com",
"status": "valid_dns",
"status_code": null,
"free_email": "yes",
"disposable_email": "notchecked"
},
{
"email": "op999@gmail.com",
"status": "valid_dns",
"status_code": null,
"free_email": "yes",
"disposable_email": "notchecked"
},
{
"email": "user@yahoo.com",
"status": "valid_dns",
"status_code": null,
"free_email": "yes",
"disposable_email": "notchecked"
},
{
"email": "user1@outlook.com",
"status": "valid_dns",
"status_code": null,
"free_email": "yes",
"disposable_email": "notchecked"
},
{
"email": "user2@hotmail.com",
"status": "valid_dns",
"status_code": null,
"free_email": "yes",
"disposable_email": "notchecked"
}
]
```
Logging SMTP server response displayed in `-v` verbose mode with email addresses in file `emaillist.txt` will create a 2nd debug log `email_verification_log_debug_*.log` with entries.
```
ls -lhArt | tail -2
-rw-r--r-- 1 root root 9.8K May 4 23:35 email_verification_log_debug_2024-05-04_23-35-47.log
-rw-r--r-- 1 root root 6.0K May 4 23:35 email_verification_log_2024-05-04_23-35-47.log
```
```
python validate_emails.py -f user@domain1.com -l emaillist.txt -tm all -v
[
{
"email": "user@mailsac.com",
"status": "ok",
"status_code": 250,
"free_email": "yes",
"disposable_email": "yes"
},
{
"email": "xyz@centmil1.com",
"status": "invalid",
"status_code": null,
"free_email": "unknown",
"disposable_email": "no"
},
{
"email": "user+to@domain1.com",
"status": "ok",
"status_code": 250,
"free_email": "no",
"disposable_email": "no"
},
{
"email": "xyz@domain1.com",
"status": "unknown_email",
"status_code": 550,
"free_email": "no",
"disposable_email": "no"
},
{
"email": "abc@domain1.com",
"status": "unknown_email",
"status_code": 550,
"free_email": "no",
"disposable_email": "no"
},
{
"email": "123@domain1.com",
"status": "unknown_email",
"status_code": 550,
"free_email": "no",
"disposable_email": "no"
},
{
"email": "pop@domain1.com",
"status": "unknown_email",
"status_code": 550,
"free_email": "no",
"disposable_email": "no"
},
{
"email": "pip@domain1.com",
"status": "unknown_email",
"status_code": 550,
"free_email": "no",
"disposable_email": "no"
},
{
"email": "user@tempr.email",
"status": "ok",
"status_code": 250,
"free_email": "no",
"disposable_email": "yes"
},
{
"email": "info@domain2.com",
"status": "ok",
"status_code": 250,
"free_email": "no",
"disposable_email": "no"
},
{
"email": "user@gmail.com",
"status": "ok",
"status_code": 250,
"free_email": "yes",
"disposable_email": "no"
},
{
"email": "op999@gmail.com",
"status": "unknown_email",
"status_code": 550,
"free_email": "yes",
"disposable_email": "no"
},
{
"email": "user@yahoo.com",
"status": "ok",
"status_code": 250,
"free_email": "yes",
"disposable_email": "no"
},
{
"email": "user1@outlook.com",
"status": "ok",
"status_code": 250,
"free_email": "yes",
"disposable_email": "no"
},
{
"email": "user2@hotmail.com",
"status": "ok",
"status_code": 250,
"free_email": "yes",
"disposable_email": "no"
}
]
```
Regular non-verbose log `email_verification_log_2024-05-04_23-35-47.log` will also show the `smtp` profile used to do the SMTP check testing as well as `Acceptable failure rate` metrics per domain.
```
cat email_verification_log_2024-05-04_23-35-47.log
2024-05-04 23:35:48,080 - INFO - Disposable email address: user@mailsac.com
2024-05-04 23:35:48,082 - INFO - Disposable email address: user@tempr.email
2024-05-04 23:35:48,626 - INFO - SMTP response for user@mailsac.com using profile [default]: 250, b'Accepted'
2024-05-04 23:35:49,022 - INFO - SMTP response for user@tempr.email using profile [default]: 250, b'2.1.5 Ok'
2024-05-04 23:35:49,082 - INFO - Validating user+to@domain1.com
2024-05-04 23:35:49,082 - INFO - Email DNS is valid: user+to@domain1.com
2024-05-04 23:35:49,082 - INFO - Validating 123@domain1.com
2024-05-04 23:35:49,082 - INFO - Validating abc@domain1.com
2024-05-04 23:35:49,082 - INFO - Validating xyz@domain1.com
2024-05-04 23:35:49,082 - INFO - Validating pop@domain1.com
2024-05-04 23:35:49,082 - INFO - Email DNS is valid: pop@domain1.com
2024-05-04 23:35:49,082 - INFO - Email DNS is valid: 123@domain1.com
2024-05-04 23:35:49,082 - INFO - Email DNS is valid: abc@domain1.com
2024-05-04 23:35:49,082 - INFO - Email DNS is valid: xyz@domain1.com
2024-05-04 23:35:49,082 - INFO - Validating pip@domain1.com
2024-05-04 23:35:49,083 - INFO - Validating info@domain2.com
2024-05-04 23:35:49,083 - INFO - Email DNS is valid: pip@domain1.com
2024-05-04 23:35:49,083 - INFO - Email DNS is valid: info@domain2.com
2024-05-04 23:35:49,084 - INFO - Validating user2@hotmail.com
2024-05-04 23:35:49,084 - INFO - Email DNS is valid: user2@hotmail.com
2024-05-04 23:35:49,646 - INFO - SMTP response for pop@domain1.com using profile [default]: 550, b"5.1.1 The email account that you tried to reach does not exist. Please try\n5.1.1 double-checking the recipient's email address for typos or\n5.1.1 unnecessary spaces. For more information, go to\n5.1.1 https://support.google.com/mail/?p=NoSuchUser h14-20020a05640250ce00b00572b21fb7d5si3202708edb.683 - gsmtp"
2024-05-04 23:35:49,666 - INFO - SMTP response for 123@domain1.com using profile [default]: 550, b"5.1.1 The email account that you tried to reach does not exist. Please try\n5.1.1 double-checking the recipient's email address for typos or\n5.1.1 unnecessary spaces. For more information, go to\n5.1.1 https://support.google.com/mail/?p=NoSuchUser sh43-20020a1709076eab00b00a597a01b74csi2965444ejc.273 - gsmtp"
2024-05-04 23:35:49,672 - INFO - SMTP response for abc@domain1.com using profile [default]: 550, b"5.1.1 The email account that you tried to reach does not exist. Please try\n5.1.1 double-checking the recipient's email address for typos or\n5.1.1 unnecessary spaces. For more information, go to\n5.1.1 https://support.google.com/mail/?p=NoSuchUser gt34-20020a1709072da200b00a597ff2fc04si2617860ejc.981 - gsmtp"
2024-05-04 23:35:49,675 - INFO - SMTP response for pip@domain1.com using profile [default]: 550, b"5.1.1 The email account that you tried to reach does not exist. Please try\n5.1.1 double-checking the recipient's email address for typos or\n5.1.1 unnecessary spaces. For more information, go to\n5.1.1 https://support.google.com/mail/?p=NoSuchUser p14-20020aa7d30e000000b00572d0cb1e66si1718373edq.661 - gsmtp"
2024-05-04 23:35:49,694 - INFO - SMTP response for xyz@domain1.com using profile [default]: 550, b"5.1.1 The email account that you tried to reach does not exist. Please try\n5.1.1 double-checking the recipient's email address for typos or\n5.1.1 unnecessary spaces. For more information, go to\n5.1.1 https://support.google.com/mail/?p=NoSuchUser wg10-20020a17090705ca00b00a5999e2028dsi2138359ejb.514 - gsmtp"
2024-05-04 23:35:49,752 - INFO - SMTP response for info@domain2.com using profile [default]: 250, b'2.1.5 OK j7-20020a5d5647000000b0034cfd826251si3647095wrw.519 - gsmtp'
2024-05-04 23:35:49,753 - INFO - SMTP response for user+to@domain1.com using profile [default]: 250, b'2.1.5 OK m8-20020a056402430800b005721e7edb0fsi3177955edc.665 - gsmtp'
2024-05-04 23:35:49,761 - INFO - SMTP response for user2@hotmail.com using profile [default]: 250, b'2.1.5 Recipient OK'
2024-05-04 23:35:50,085 - INFO - Validating user@gmail.com
2024-05-04 23:35:50,085 - INFO - Email DNS is valid: user@gmail.com
2024-05-04 23:35:50,085 - INFO - Validating op999@gmail.com
2024-05-04 23:35:50,085 - INFO - Validating user@yahoo.com
2024-05-04 23:35:50,086 - INFO - Email DNS is valid: op999@gmail.com
2024-05-04 23:35:50,086 - INFO - Email DNS is valid: user@yahoo.com
2024-05-04 23:35:50,086 - INFO - Validating user1@outlook.com
2024-05-04 23:35:50,086 - INFO - Email DNS is valid: user1@outlook.com
2024-05-04 23:35:50,545 - INFO - SMTP response for user1@outlook.com using profile [default]: 250, b'2.1.5 Recipient OK'
2024-05-04 23:35:50,659 - INFO - SMTP response for op999@gmail.com using profile [default]: 550, b"5.1.1 The email account that you tried to reach does not exist. Please try\n5.1.1 double-checking the recipient's email address for typos or\n5.1.1 unnecessary spaces. For more information, go to\n5.1.1 https://support.google.com/mail/?p=NoSuchUser w9-20020a5d4049000000b0034ddab36ff8si3678827wrp.499 - gsmtp"
2024-05-04 23:35:50,801 - INFO - SMTP response for user@gmail.com using profile [default]: 250, b'2.1.5 OK d7-20020a05600c34c700b0041d7d5787bfsi3791910wmq.193 - gsmtp'
2024-05-04 23:35:50,802 - INFO - SMTP response for user@yahoo.com using profile [default]: 250, b'recipient ok'
2024-05-04 23:35:50,802 - INFO - Acceptable failure rate (0.00%) for domain mailsac.com.
2024-05-04 23:35:50,802 - INFO - Acceptable failure rate (0.00%) for domain tempr.email.
2024-05-04 23:35:50,802 - INFO - Acceptable failure rate (0.00%) for domain domain1.com.
2024-05-04 23:35:50,802 - INFO - Acceptable failure rate (0.00%) for domain domain2.com.
2024-05-04 23:35:50,802 - INFO - Acceptable failure rate (0.00%) for domain hotmail.com.
2024-05-04 23:35:50,803 - INFO - Acceptable failure rate (0.00%) for domain outlook.com.
2024-05-04 23:35:50,803 - INFO - Acceptable failure rate (0.00%) for domain gmail.com.
2024-05-04 23:35:50,803 - INFO - Acceptable failure rate (0.00%) for domain yahoo.com.
```
Verbose debug log `email_verification_log_debug_2024-05-04_23-35-47.log`
```
cat email_verification_log_debug_2024-05-04_23-35-47.log
2024-05-04 23:35:48,080 - DEBUG - Connecting to SMTP server alt.mailsac.com. on port 25
2024-05-04 23:35:48,083 - DEBUG - Connecting to SMTP server mx.discard.email. on port 25
2024-05-04 23:35:48,328 - DEBUG - SMTP connection established
2024-05-04 23:35:48,403 - DEBUG - EHLO command sent
2024-05-04 23:35:48,403 - DEBUG - Sending MAIL FROM command: user@domain1.com
2024-05-04 23:35:48,477 - DEBUG - MAIL FROM command sent successfully
2024-05-04 23:35:48,477 - DEBUG - Sending RCPT TO command for: user@mailsac.com
2024-05-04 23:35:48,552 - DEBUG - RCPT TO response: 250, b'Accepted'
2024-05-04 23:35:48,626 - DEBUG - SMTP session closed
2024-05-04 23:35:48,642 - DEBUG - SMTP connection established
2024-05-04 23:35:48,737 - DEBUG - EHLO command sent
2024-05-04 23:35:48,737 - DEBUG - Sending MAIL FROM command: user@domain1.com
2024-05-04 23:35:48,832 - DEBUG - MAIL FROM command sent successfully
2024-05-04 23:35:48,832 - DEBUG - Sending RCPT TO command for: user@tempr.email
2024-05-04 23:35:48,927 - DEBUG - RCPT TO response: 250, b'2.1.5 Ok'
2024-05-04 23:35:49,022 - DEBUG - SMTP session closed
2024-05-04 23:35:49,082 - DEBUG - Connecting to SMTP server aspmx5.googlemail.com. on port 25
2024-05-04 23:35:49,083 - DEBUG - Connecting to SMTP server aspmx5.googlemail.com. on port 25
2024-05-04 23:35:49,083 - DEBUG - Connecting to SMTP server aspmx5.googlemail.com. on port 25
2024-05-04 23:35:49,083 - DEBUG - Connecting to SMTP server aspmx5.googlemail.com. on port 25
2024-05-04 23:35:49,083 - DEBUG - Connecting to SMTP server aspmx5.googlemail.com. on port 25
2024-05-04 23:35:49,084 - DEBUG - Connecting to SMTP server aspmx5.googlemail.com. on port 25
2024-05-04 23:35:49,084 - DEBUG - Connecting to SMTP server aspmx2.googlemail.com. on port 25
2024-05-04 23:35:49,084 - DEBUG - Connecting to SMTP server hotmail-com.olc.protection.outlook.com. on port 25
2024-05-04 23:35:49,277 - DEBUG - SMTP connection established
2024-05-04 23:35:49,277 - DEBUG - SMTP connection established
2024-05-04 23:35:49,285 - DEBUG - SMTP connection established
2024-05-04 23:35:49,291 - DEBUG - SMTP connection established
2024-05-04 23:35:49,294 - DEBUG - SMTP connection established
2024-05-04 23:35:49,305 - DEBUG - SMTP connection established
2024-05-04 23:35:49,307 - DEBUG - SMTP connection established
2024-05-04 23:35:49,367 - DEBUG - EHLO command sent
2024-05-04 23:35:49,367 - DEBUG - Sending MAIL FROM command: user@domain1.com
2024-05-04 23:35:49,367 - DEBUG - EHLO command sent
2024-05-04 23:35:49,367 - DEBUG - Sending MAIL FROM command: user@domain1.com
2024-05-04 23:35:49,382 - DEBUG - EHLO command sent
2024-05-04 23:35:49,382 - DEBUG - Sending MAIL FROM command: user@domain1.com
2024-05-04 23:35:49,383 - DEBUG - EHLO command sent
2024-05-04 23:35:49,384 - DEBUG - Sending MAIL FROM command: user@domain1.com
2024-05-04 23:35:49,397 - DEBUG - EHLO command sent
2024-05-04 23:35:49,397 - DEBUG - Sending MAIL FROM command: user@domain1.com
2024-05-04 23:35:49,399 - DEBUG - EHLO command sent
2024-05-04 23:35:49,399 - DEBUG - Sending MAIL FROM command: user@domain1.com
2024-05-04 23:35:49,401 - DEBUG - EHLO command sent
2024-05-04 23:35:49,401 - DEBUG - Sending MAIL FROM command: user@domain1.com
2024-05-04 23:35:49,455 - DEBUG - MAIL FROM command sent successfully
2024-05-04 23:35:49,455 - DEBUG - Sending RCPT TO command for: user+to@domain1.com
2024-05-04 23:35:49,455 - DEBUG - MAIL FROM command sent successfully
2024-05-04 23:35:49,455 - DEBUG - Sending RCPT TO command for: pop@domain1.com
2024-05-04 23:35:49,473 - DEBUG - MAIL FROM command sent successfully
2024-05-04 23:35:49,473 - DEBUG - Sending RCPT TO command for: 123@domain1.com
2024-05-04 23:35:49,474 - DEBUG - MAIL FROM command sent successfully
2024-05-04 23:35:49,474 - DEBUG - Sending RCPT TO command for: abc@domain1.com
2024-05-04 23:35:49,485 - DEBUG - MAIL FROM command sent successfully
2024-05-04 23:35:49,485 - DEBUG - Sending RCPT TO command for: pip@domain1.com
2024-05-04 23:35:49,494 - DEBUG - MAIL FROM command sent successfully
2024-05-04 23:35:49,494 - DEBUG - Sending RCPT TO command for: xyz@domain1.com
2024-05-04 23:35:49,500 - DEBUG - MAIL FROM command sent successfully
2024-05-04 23:35:49,500 - DEBUG - Sending RCPT TO command for: info@domain2.com
2024-05-04 23:35:49,530 - DEBUG - SMTP connection established
2024-05-04 23:35:49,558 - DEBUG - RCPT TO response: 550, b"5.1.1 The email account that you tried to reach does not exist. Please try\n5.1.1 double-checking the recipient's email address for typos or\n5.1.1 unnecessary spaces. For more information, go to\n5.1.1 https://support.google.com/mail/?p=NoSuchUser h14-20020a05640250ce00b00572b21fb7d5si3202708edb.683 - gsmtp"
2024-05-04 23:35:49,576 - DEBUG - RCPT TO response: 550, b"5.1.1 The email account that you tried to reach does not exist. Please try\n5.1.1 double-checking the recipient's email address for typos or\n5.1.1 unnecessary spaces. For more information, go to\n5.1.1 https://support.google.com/mail/?p=NoSuchUser sh43-20020a1709076eab00b00a597a01b74csi2965444ejc.273 - gsmtp"
2024-05-04 23:35:49,580 - DEBUG - RCPT TO response: 550, b"5.1.1 The email account that you tried to reach does not exist. Please try\n5.1.1 double-checking the recipient's email address for typos or\n5.1.1 unnecessary spaces. For more information, go to\n5.1.1 https://support.google.com/mail/?p=NoSuchUser gt34-20020a1709072da200b00a597ff2fc04si2617860ejc.981 - gsmtp"
2024-05-04 23:35:49,586 - DEBUG - EHLO command sent
2024-05-04 23:35:49,586 - DEBUG - Sending MAIL FROM command: user@domain1.com
2024-05-04 23:35:49,587 - DEBUG - RCPT TO response: 550, b"5.1.1 The email account that you tried to reach does not exist. Please try\n5.1.1 double-checking the recipient's email address for typos or\n5.1.1 unnecessary spaces. For more information, go to\n5.1.1 https://support.google.com/mail/?p=NoSuchUser p14-20020aa7d30e000000b00572d0cb1e66si1718373edq.661 - gsmtp"
2024-05-04 23:35:49,600 - DEBUG - RCPT TO response: 550, b"5.1.1 The email account that you tried to reach does not exist. Please try\n5.1.1 double-checking the recipient's email address for typos or\n5.1.1 unnecessary spaces. For more information, go to\n5.1.1 https://support.google.com/mail/?p=NoSuchUser wg10-20020a17090705ca00b00a5999e2028dsi2138359ejb.514 - gsmtp"
2024-05-04 23:35:49,639 - DEBUG - MAIL FROM command sent successfully
2024-05-04 23:35:49,639 - DEBUG - Sending RCPT TO command for: user2@hotmail.com
2024-05-04 23:35:49,646 - DEBUG - SMTP session closed
2024-05-04 23:35:49,651 - DEBUG - RCPT TO response: 250, b'2.1.5 OK j7-20020a5d5647000000b0034cfd826251si3647095wrw.519 - gsmtp'
2024-05-04 23:35:49,665 - DEBUG - RCPT TO response: 250, b'2.1.5 OK m8-20020a056402430800b005721e7edb0fsi3177955edc.665 - gsmtp'
2024-05-04 23:35:49,666 - DEBUG - SMTP session closed
2024-05-04 23:35:49,672 - DEBUG - SMTP session closed
2024-05-04 23:35:49,675 - DEBUG - SMTP session closed
2024-05-04 23:35:49,694 - DEBUG - SMTP session closed
2024-05-04 23:35:49,709 - DEBUG - RCPT TO response: 250, b'2.1.5 Recipient OK'
2024-05-04 23:35:49,752 - DEBUG - SMTP session closed
2024-05-04 23:35:49,753 - DEBUG - SMTP session closed
2024-05-04 23:35:49,761 - DEBUG - SMTP session closed
2024-05-04 23:35:50,085 - DEBUG - Connecting to SMTP server alt2.gmail-smtp-in.l.google.com. on port 25
2024-05-04 23:35:50,086 - DEBUG - Connecting to SMTP server alt2.gmail-smtp-in.l.google.com. on port 25
2024-05-04 23:35:50,086 - DEBUG - Connecting to SMTP server mta6.am0.yahoodns.net. on port 25
2024-05-04 23:35:50,086 - DEBUG - Connecting to SMTP server outlook-com.olc.protection.outlook.com. on port 25
2024-05-04 23:35:50,314 - DEBUG - SMTP connection established
2024-05-04 23:35:50,314 - DEBUG - SMTP connection established
2024-05-04 23:35:50,402 - DEBUG - EHLO command sent
2024-05-04 23:35:50,402 - DEBUG - Sending MAIL FROM command: user@domain1.com
2024-05-04 23:35:50,407 - DEBUG - EHLO command sent
2024-05-04 23:35:50,407 - DEBUG - Sending MAIL FROM command: user@domain1.com
2024-05-04 23:35:50,451 - DEBUG - SMTP connection established
2024-05-04 23:35:50,472 - DEBUG - EHLO command sent
2024-05-04 23:35:50,472 - DEBUG - Sending MAIL FROM command: user@domain1.com
2024-05-04 23:35:50,484 - DEBUG - SMTP connection established
2024-05-04 23:35:50,486 - DEBUG - MAIL FROM command sent successfully
2024-05-04 23:35:50,486 - DEBUG - Sending RCPT TO command for: op999@gmail.com
2024-05-04 23:35:50,492 - DEBUG - MAIL FROM command sent successfully
2024-05-04 23:35:50,493 - DEBUG - Sending RCPT TO command for: user1@outlook.com
2024-05-04 23:35:50,497 - DEBUG - MAIL FROM command sent successfully
2024-05-04 23:35:50,498 - DEBUG - Sending RCPT TO command for: user@gmail.com
2024-05-04 23:35:50,525 - DEBUG - RCPT TO response: 250, b'2.1.5 Recipient OK'
2024-05-04 23:35:50,545 - DEBUG - SMTP session closed
2024-05-04 23:35:50,561 - DEBUG - EHLO command sent
2024-05-04 23:35:50,561 - DEBUG - Sending MAIL FROM command: user@domain1.com
2024-05-04 23:35:50,575 - DEBUG - RCPT TO response: 550, b"5.1.1 The email account that you tried to reach does not exist. Please try\n5.1.1 double-checking the recipient's email address for typos or\n5.1.1 unnecessary spaces. For more information, go to\n5.1.1 https://support.google.com/mail/?p=NoSuchUser w9-20020a5d4049000000b0034ddab36ff8si3678827wrp.499 - gsmtp"
2024-05-04 23:35:50,641 - DEBUG - MAIL FROM command sent successfully
2024-05-04 23:35:50,641 - DEBUG - Sending RCPT TO command for: user@yahoo.com
2024-05-04 23:35:50,659 - DEBUG - SMTP session closed
2024-05-04 23:35:50,710 - DEBUG - RCPT TO response: 250, b'2.1.5 OK d7-20020a05600c34c700b0041d7d5787bfsi3791910wmq.193 - gsmtp'
2024-05-04 23:35:50,720 - DEBUG - RCPT TO response: 250, b'recipient ok'
2024-05-04 23:35:50,801 - DEBUG - SMTP session closed
2024-05-04 23:35:50,802 - DEBUG - SMTP session closed
```
syntax only test without smtp and dns test
```
python validate_emails.py -f user@domain.com -e user+to@domain.com -tm syntax
[
{
"email": "user@mailsac.com",
"status": "valid_format",
"status_code": null,
"free_email": "yes",
"disposable_email": "notchecked"
},
{
"email": "xyz@centmil1.com",
"status": "valid_format",
"status_code": null,
"free_email": "no",
"disposable_email": "notchecked"
},
{
"email": "user+to@domain1.com",
"status": "valid_format",
"status_code": null,
"free_email": "no",
"disposable_email": "notchecked"
},
{
"email": "xyz@domain1.com",
"status": "valid_format",
"status_code": null,
"free_email": "no",
"disposable_email": "notchecked"
},
{
"email": "abc@domain1.com",
"status": "valid_format",
"status_code": null,
"free_email": "no",
"disposable_email": "notchecked"
},
{
"email": "123@domain1.com",
"status": "valid_format",
"status_code": null,
"free_email": "no",
"disposable_email": "notchecked"
},
{
"email": "pop@domain1.com",
"status": "valid_format",
"status_code": null,
"free_email": "no",
"disposable_email": "notchecked"
},
{
"email": "pip@domain1.com",
"status": "valid_format",
"status_code": null,
"free_email": "no",
"disposable_email": "notchecked"
},
{
"email": "user@tempr.email",
"status": "valid_format",
"status_code": null,
"free_email": "no",
"disposable_email": "notchecked"
},
{
"email": "info@domain2.com",
"status": "valid_format",
"status_code": null,
"free_email": "no",
"disposable_email": "notchecked"
},
{
"email": "user@gmail.com",
"status": "valid_format",
"status_code": null,
"free_email": "yes",
"disposable_email": "notchecked"
},
{
"email": "op999@gmail.com",
"status": "valid_format",
"status_code": null,
"free_email": "yes",
"disposable_email": "notchecked"
},
{
"email": "user@yahoo.com",
"status": "valid_format",
"status_code": null,
"free_email": "yes",
"disposable_email": "notchecked"
},
{
"email": "user1@outlook.com",
"status": "valid_format",
"status_code": null,
"free_email": "yes",
"disposable_email": "notchecked"
},
{
"email": "user2@hotmail.com",
"status": "valid_format",
"status_code": null,
"free_email": "yes",
"disposable_email": "notchecked"
}
]
```
# API Support
In additional to local self-hosted email verification, the script now has added support for the following external Email cleaning service APIs - [EmailListVerify](https://centminmod.com/emaillistverify), [MillionVerifier](https://centminmod.com/millionverifier), [MyEmailVerifier](https://centminmod.com/myemailverifier), [CaptainVerify](https://centminmod.com/captainverify), [Proofy.io](https://centminmod.com/proofy), [Zerobounce](https://centminmod.com/zerobounce), [Reoon](https://centminmod.com/reoon), [Bouncify](https://centminmod.com/bouncify), [Bounceless](https://centminmod.com/bounceless). Links to services maybe affiliate links. If you found this information useful ;)
Updated: Added [API Merge support](#api-merge) via `-apimerge` argument to merge [EmailListVerify](https://centminmod.com/emaillistverify) + [MillionVerifier](https://centminmod.com/millionverifier) API results together for more accurate email verification results.
## API Usage Commands
`validate_emails.py` supports passing individual email's comma separated via `-e` flag i.e. `-e user@domain1.com,user@domain2.com` or passing `-l` flag for a text file with list of email addresses one per line via -`l emaillist.txt`. Both methods use respetive provider's per email verification APIs. Only some providers have support in the script for bulk email API - which is currently [EmailListVerify](https://centminmod.com/emaillistverify) and [MillionVerifier](https://centminmod.com/millionverifier) via `-apibulk` flag i.e. `-l emaillist.txt -apibulk emaillistverify` or `-l emaillist.txt -apibuilk millionverifier`.
You can see a full list and explanation of all argument flags supported at [here](#usage).
The `-api` flag determines which provider you use along with their respective `-apikey*` flag.
[EmailListVerify](https://centminmod.com/emaillistverify)
```
python validate_emails.py -f user@domain1.com -l emaillist.txt -tm all -api emaillistverify -apikey $elvkey
```
[MillionVerifier](https://centminmod.com/millionverifier)
```
python validate_emails.py -f user@domain1.com -l emaillist.txt -tm all -api millionverifier -apikey_mv $mvkey
```
[CaptainVerify](https://centminmod.com/captainverify)
```
python validate_emails.py -f user@domain1.com -l emaillist.txt -tm all -api captainverify -apikey_cv $cvkey
```
[Proofy.io](https://centminmod.com/proofy)
```
python validate_emails.py -f user@domain1.com -l emaillist.txt -tm all -api proofy -apikey_pf $pkey -apiuser_pf $puser
```
[MyEmailVerifier](https://centminmod.com/myemailverifier)
```
python validate_emails.py -f user@domain1.com -l emaillist.txt -tm all -api myemailverifier -apikey_mev $mevkey
```
[Zerobounce](https://centminmod.com/zerobounce)
```
python validate_emails.py -f user@domain1.com -l emaillist.txt -tm all -api zerobounce -apikey_zb $zbkey
```
[Reoon](https://centminmod.com/reoon)
```
python validate_emails.py -f user@domain1.com -l emaillist.txt -tm all -api reoon -apikey_rn $reokey
```
[Bouncify](https://centminmod.com/bouncify)
```
python validate_emails.py -f user@domain1.com -l emaillist.txt -tm all -api bouncify -apikey_bf $bfkey
```
[Bounceless](https://centminmod.com/bounceless)
```
python validate_emails.py -f user@domain1.com -l emaillist.txt -tm all -api bounceless -apikey_bf $blkey
```
**Notes:**
- Some providers have enabled optional [Cloudflare HTTP Forward Proxy Cache With KV Storage](#cloudflare-http-forward-proxy-cache-with-kv-storage)
- All providers support optional `-store` flag to save email verification results to remote [S3 storage](#s3-storage-support) i.e. Amazon AWS S3 or Cloudflare R2 object storage buckets
- There's also work on a [PHP Wrapper](#php-wrapper) script.
## Personal Experience
Personal experience with all commercial email verification providers:
- Disclaimer: I've already been using EmailListVerify since 2015 and Proofy.io since 2022. While the rest of the mentioned providers are new experiences for me.
- EmailListVerify and MillionVerifier while being cheaper than the others seem to be better for the following:
- API documentation
- Less restrictive on API connection and rate limits. Meaning if you are doing per email API checks for many email addresses, the speed of completion will be faster. Though if you're doing many email address checks, you'd want to use their respective bulk email API end points to upload a single text file for processing.
- For bulk API speed though, MillionVerifier is much faster than EmailListVerify. Even MillionVerifier's per email check API speed can be as low as 100ms for check and has a rate limit of [400 emails/second](https://help.millionverifier.com/email-verification-api/real-time-api) per unqiue IP address. For the sample 15 email addresses tested below, MillionVerifier bulk API took ~7 seconds, EmailListVerify bulk API took ~45 seconds. Compared to per email address verification checks, both taking between 2.2 to 3.3 seconds. EmailListVerify seems to have much more detailed status classifications (see below) compared to ther others so more processing is done on their end.
- MillionVerifier has email verification speed information [here](https://help.millionverifier.com/bulk-email-verification/email-verification-speed)
- MillionVerifier allow for a [maximum 2 simultaneous bulk file API uploads at a time](https://help.millionverifier.com/bulk-email-verification/verify-multiple-files-at-the-same-time) and max size of files uploaded are [1 million emails per file or 100MB size](https://help.millionverifier.com/bulk-email-verification/upload-emails-for-verification). If each of the files contains more than 1000 emails, they will verify a maximum of 2 files at a time.
- MillionVerifier API logging for billing is the mosted detailed with historical running balances. They also show per API call credit usage balance details and even list in the logs refunded credits for bulk API file uploaded emails classified as 'risky' (`catch_all` or `unknown`) https://help.millionverifier.com/payments-credits/refund-for-risky-emails. AFAIK, the other providers don't refund any credits that I can see. However, on below sample 15 email addresses tested, I always got 1 refunded credit so it applies to one email address which is a known valid email `user@yahoo.com` which is classed as `unknown` in bulk API but classed as `ok` in per email verification API. Seems to be a bug in their bulk API then as the refunds only apply to bulk API and not per email verification checks due to differences in classifications in bulk API vs per email verification API.
### MillionVerifier Yahoo Email Address
I reached out to MillionVerifier chat support which was initially handled via Milly their AI chat bot which later referred me to support. They emailed me back saying:
> We're glad you reached out to us about this issue, and we're here to help.
> The discrepancy you're seeing in the results is likely because we were unable to connect to the server during the verification process, leading to an "Unknown" result. However, for the single API, the connection went through smoothly, allowing us to verify the email without any problems. An "Unknown" result simply means that we couldn't determine the existence of the email at the time of verification.
> If you have any more questions, queries, or issues, we're more than happy to assist.
I tried a few attempts at bulk API for the same list of 15 emails, and `user@yahoo.com` is always marked as status = `unknown` and never anything different though? It would be hard to differentiate status classifications if it's due connection issues if they're lumped into other emails in unknown label. Maybe would be better to have a separate classification for connection issues so we can differentiate as such. For example, EmailListVerify has 18 different status classifications including for connection related issues.
MillionVerifier follow up - support investigated the issue on their end and said:
> We're really glad you reached out to us about this issue, and we've done our best to get to the bottom of it.
> Thank you for your patience while we looked into this. It turns out the issue wasn't about whether you were verifying emails in bulk or one at a time, but rather which server was used for the verification. Also, if you try to verify some emails multiple times, they might eventually return an "Unknown" result.
> I'd also like to point out that we don't deduct credits for emails marked as "Risky" during API calls. You won't see a credit refund for these in your Credit Balance because we only charge for emails identified as "Good" or "Bad." > The credits for "Risky" emails aren't taken away in the first place.
I confirmed this occurs on MillionVerifier single email verification as well if you test it enough times for `user@yahoo.com` email address specifically.
single email API check for `user@yahoo.com` returns `ok`
```
python validate_emails.py -f user@domain1.com -e user@yahoo.com -api millionverifier -apikey_mv $mvkey -tm all
[
{
"email": "user@yahoo.com",
"status": "ok",
"status_code": null,
"free_email": "yes",
"disposable_email": "no",
"free_email_api": true,
"role_api": false
}
]
```
bulk API upload check excerpt for `user@yahoo.com` returns `unknown`
```
python validate_emails.py -f user@domain1.com -l emaillist.txt -tm all -api millionverifier -apikey_mv $mvkey -apibulk millionverifier
[
{
"email": "user@yahoo.com",
"status": "unknown",
"free_email": "yes",
"disposable_email": "no",
"free_email_api": "yes",
"role_api": "no"
},
]
```
As such you can't 100% rely on the status output to do tasks like updating Xenforo user's `user_state` status to stop sending emails to them without further verification for such emails. For now, I've updated my `validate_emails.py` script for MillionVerifier bulk API and per email check API results, to not list [Xenforo SQL queries](#xenforo) for `unknown` status results and only list [Xenforo SQL queries](#xenforo) for `invalid` and `disposable` status emails. Same can be said for other providers, probably need to really double check your results if you're relying on the results for important tasks. You can filter MillionVerifier's `unknown` status emails and feed them into another commercial provider's API to double check i.e. EmailListVerify or use script's self-hosted local email check. Given cheaper MillionVerifier pricing, it might be more economical to do it this way?
MillionVerifier bulk API filter `-api millionverifier -apikey_mv $mvkey -apibulk millionverifier` filter using `jq` for `unknown` status emails piped into text file `results-millionverifier-bulk-api-unknown-only.txt`
```
python validate_emails.py -f user@domain1.com -l emaillist.txt -tm all -api millionverifier -apikey_mv $mvkey -apibulk millionverifier | jq '.[] | select(.status == "unknown")' 2>&1 > results-millionverifier-bulk-api-unknown-only.txt
```
The `results-millionverifier-bulk-api-unknown-only.txt` contents will show all MillionVerifier bulk API returned `unknown` status results.
```
cat results-millionverifier-bulk-api-unknown-only.txt
{
"email": "user@yahoo.com",
"status": "unknown",
"free_email": "yes",
"disposable_email": "no",
"free_email_api": "yes",
"role_api": "no"
}
```
Using same `results-millionverifier-bulk-api-unknown-only.txt` file, and `jq` just filter out the email addresses into a new `results-millionverifier-bulk-api-unknown-only-emails.txt` file
```
cat results-millionverifier-bulk-api-unknown-only.txt | jq -r '.email' | tee results-millionverifier-bulk-api-unknown-only-emails.txt
user@yahoo.com
```
Then use EmailListVerify bulk API to verify the filtered MillionVerifier `unknown` status list filtered file `results-millionverifier-bulk-api-unknown-only-emails.txt` and double check the status which confirms it's actually a `valid` email.
```
python validate_emails.py -f user@domain1.com -l results-millionverifier-bulk-api-unknown-only-emails.txt -tm all -api emaillistverify -apikey $elvkey -apibulk emaillistverify
[
{
"email": "user@yahoo.com",
"status": "valid",
"status_code": "",
"free_email": "yes",
"disposable_email": "no"
}
]
```
Or EmailListVerify per email verification API check
```
./validate_emails.py -f user@domain1.com -e user@yahoo.com -tm all -api emaillistverify -apikey $elvkey -tm all
[
{
"email": "user@yahoo.com",
"status": "valid",
"status_code": null,
"free_email": "yes",
"dispo