What is MailTidy3?
MailTidy3 is a self-hosted email list verification SaaS you install on your own VPS. You own the server, the data, and the throughput.
Unlike cloud-based verifiers that charge per email and throttle your throughput, MailTidy3 runs entirely on infrastructure you control. Upload a list of 100,000 emails, get accurate classifications back — valid, invalid, catch-all, disposable, role-based — without per-email fees.
The system is built on three components: a Laravel 13 web application handling auth, billing, and the buyer dashboard; a Python FastAPI verification engine doing the actual SMTP probing; and lightweight worker agents you can deploy to additional VPS to multiply throughput.
What you need.
Three non-negotiable requirements. Miss any one of them and the product will not work.
Email verification is making SMTP connections on port 25. If your VPS provider blocks outbound port 25, every verification will time out. Do not use DigitalOcean, AWS EC2, Google Cloud, or Azure — they permanently block port 25 with no path to unblock.
Minimum specifications
| Component | Minimum | Recommended | Notes |
|---|---|---|---|
| CPU | 1 vCPU | 2–4 vCPU | Python engine is async I/O bound, not CPU bound |
| RAM | 1 GB | 4–8 GB | 1 GB is enough to start; 2+ GB for production |
| Disk | 20 GB SSD | 50+ GB NVMe | Uploads + database growth |
| OS | Ubuntu 22.04 LTS | Ubuntu 22.04 LTS | Other Debian-based distros may work; not tested |
| Port 25 | Open outbound | Open outbound | Mandatory — see provider table below |
| Bandwidth | 100 Mbps | 1 Gbps | SMTP probes are tiny (~1 KB each) |
Software stack (auto-installed)
| Layer | Version |
|---|---|
| PHP | 8.4 |
| Laravel | 13.x |
| PostgreSQL | 17 |
| Redis | 6+ |
| Python | 3.10+ |
| Node.js | 24 LTS |
| Nginx | 1.18+ |
Pre-flight check
Run this on your fresh VPS before installing. If port 25 is blocked, stop and switch providers.
$ timeout 10 bash -c 'cat < /dev/tcp/gmail-smtp-in.l.google.com/25' \ && echo "✅ Port 25 OPEN — proceed with install" \ || echo "❌ Port 25 BLOCKED — switch VPS provider" # You should see: 220 mx.google.com ESMTP ... then ✅
Up in under 2 minutes.
One command installs the entire stack — PHP, Python, PostgreSQL, Redis, Nginx, SSL, and the app itself.
yourdomain.com → your VPS IPapp.yourdomain.com → your VPS IPImportant: Use DNS-only (grey cloud) in Cloudflare — do not proxy the app subdomain.
https://app.yourdomain.com/setup. The wizard asks for your admin email/password, Envato purchase code, and basic settings. That's it.Payment gateway keys, Google OAuth credentials, and SMTP settings are all configured after install through the admin panel. The installer never asks for sensitive API keys.
Verifying your first list.
https://app.yourdomain.com. Sign in with the admin credentials you set in the wizard.Three services, one purpose.
MailTidy3 splits concerns cleanly: Laravel handles the user-facing application, Python does the verification, workers scale the throughput.
Browser (React SPA) │ HTTPS / JSON API ▼ Laravel 13 (PHP 8.4) · Auth, billing, dashboard · Job state, file uploads · PostgreSQL 17 + Redis │ HTTP internal (localhost:8000) ▼ Python Engine (FastAPI + asyncio) · Verification logic · Worker registry & dispatch · Per-domain rate limiting · Webhook results → Laravel │ HTTP (public, token-auth) ▼ Worker Agents (one per VPS) · Polls engine for batches · Runs SMTP probes · Returns results
Why this separation?
PHP handles request/response well but can't do high-concurrency async I/O. Python (asyncio + aiosmtplib) can hold hundreds of SMTP connections in flight simultaneously. Laravel would block a PHP-FPM worker for the entire duration of a 100K-email job — Python never blocks at all.
Workers must run on separate IPs because Gmail, Yahoo, and Outlook rate-limit per source IP — not per account. Each additional worker VPS = one more independent rate-limit budget = roughly linear capacity increase.
Data flow for a verification job
| # | Step | Who does it |
|---|---|---|
| 1 | Buyer uploads CSV → Laravel parses, creates job record | Laravel |
| 2 | Job dispatched to Python engine via internal HTTP | Laravel → Python |
| 3 | Engine splits into batches of 200, queues in Redis | Python engine |
| 4 | Workers poll for batches, run SMTP probes concurrently | Worker agents |
| 5 | Results returned to engine, progress webhooks sent to Laravel | Python → Laravel |
| 6 | Laravel writes results to DB, notifies buyer | Laravel |
Nine ways an email can be classified.
Every email gets one primary status. Role-based is a flag that can appear alongside any status.
When RCPT TO returns 250, MailTidy3 immediately probes a random address at the same domain (e.g. ck-a7f3b2c1d9@domain.com). If that also returns 250, the domain is catch-all. This eliminates false "valid" classifications.
Numbers from actual tests.
These benchmarks were run on a single Contabo VPS (2 vCPU, 3.8 GB RAM) before the MailTidy3 rebuild was committed. They are not marketing estimates.
Per-email speed
Sustained throughput (single VPS)
| Concurrency | Emails/sec | Notes |
|---|---|---|
| 10 | 8.9 | Underutilising the network |
| 30 | 14.2 | Sweet spot for a 2 vCPU box |
| 50 | 17.6 | Diminishing returns begin |
| 100 | 19.1 | Memory pressure; not worth it |
Soft-block wall
Gmail and other major providers rate-limit by source IP. During testing, sustained probing from a single IP hit a soft-block at approximately 4,400 emails. Recovery took 30 minutes of zero traffic. MailTidy3 handles this automatically with per-domain semaphores and auto-cooldown.
Realistic daily capacity
| Setup | Emails/day |
|---|---|
| 1 VPS (main only) | 50,000 – 150,000 |
| 2 VPS (main + 1 worker) | 100,000 – 300,000 |
| 5 VPS | 250,000 – 750,000 |
| 10 VPS | 500,000 – 1.5 million |
Any file, any structure.
MailTidy3 accepts CSV, XLSX, XLS, TSV, and TXT files up to 100 MB. Multi-column files are fully supported — the engine scans the first 100 rows to identify which column contains email addresses. If multiple columns look like emails, a column picker appears.
Accepted formats
| Format | Extension | Notes |
|---|---|---|
| CSV | .csv | Comma-separated. Headers optional. |
| Excel | .xlsx, .xls | First sheet used. Multi-column supported. |
| TSV | .tsv | Tab-separated. |
| Plain text | .txt | One email per line. |
Upload options
| Option | Default | Description |
|---|---|---|
| Skip first row | On | Treat the first row as a header and skip it |
| Deduplicate | On | Remove exact duplicate addresses before verifying |
Lists of 200 emails or fewer bypass the background queue and run synchronously — results appear in under 60 seconds. This threshold is configurable in the admin panel.
Verify programmatically, at scale.
MailTidy3 includes a public REST API. Generate an API key in your dashboard and start verifying from any codebase.
Authentication
All API requests require a bearer token in the Authorization header:
Authorization: Bearer mtp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Verify a single email
POST https://app.yourdomain.com/api/v1/verify { "email": "user@example.com" } // Response { "email": "user@example.com", "status": "valid", "smtp_code": 250, "smtp_message": "2.1.5 OK", "mx_host": "aspmx.l.google.com", "flags": [], "duration_ms": 284 }
Bulk verify
POST https://app.yourdomain.com/api/v1/verify/bulk { "emails": [ "user1@example.com", "user2@example.com", "invalid@nonexistent.xyz" ] } // Response { "job": { "id": "job_abc123", "status": "processing" } }
Rate limits & quotas
| Limit | Default | Where to change |
|---|---|---|
| Daily API calls per key | 10,000 | Admin → Settings → API |
| Bulk request size | 10,000 emails | Admin → Settings → Engine |
| Rate limit (requests/min) | 60 | Admin → Settings → API |
Verified. Now what? Push to your ESP.
When a verification job finishes, push the verified addresses straight into your email marketing platform — no CSV export, no manual import. Nine integrations are bundled, including the two self-hosted ESPs that matter most to this audience.
Supported platforms
| Platform | What you can do | Self-hosted |
|---|---|---|
| Mailwizz | Lists · Tags · Custom fields | ✓ |
| Acelle Mail | Lists · Custom fields | ✓ |
| Mailchimp | Audiences · Tags · Merge fields | — |
| Brevo (formerly Sendinblue) | Contacts · Lists · Tags | — |
| Kit (formerly ConvertKit) | Forms · Sequences · Tags | — |
| GetResponse | Campaigns · Custom fields · Tags | — |
| ActiveCampaign | Lists · Tags · Custom fields | — |
| Campaign Monitor | Lists · Custom fields · Segments | — |
| MailerLite | Groups · Fields · Tags | — |
How a push works
From the results page of any completed job, click Push to autoresponder. Pick a connected ESP, choose the target list/audience/group, decide which classifications to include (valid, catch-all, greylisted), map fields, add tags, and hit push. The job runs in the background — close the dialog and check Push History later for the per-status counts (added, updated, skipped, failed).
You set up each ESP once under Settings → Integrations. After that, every results page shows a Push to autoresponder button that uses the saved credentials. Credentials are encrypted at rest and never logged.
Mailwizz (self-hosted)
If you already run Mailwizz on your own server, this is the most direct integration — your verified contacts go straight into the same database your campaigns send from.
Step 1 — Find your Mailwizz API URL
It's your Mailwizz install plus /api, e.g. https://mail.yourdomain.com/api.
Step 2 — Generate a public API key
In Mailwizz Customer Area, go to API Keys → Create new. Copy the public key — Mailwizz uses public/private key signing, so the public key is what MailTidy3 needs.
Step 3 — Get the list UID
Go to Lists, open the target list, and look at the URL. The UID is the alphanumeric segment after /lists/. You'll need it when pushing.
Step 4 — Connect in MailTidy3
Go to Settings → Integrations → Mailwizz → Connect. Paste the API URL and public key. Click Test connection. On success, the list selector populates automatically — no need to copy the UID manually.
Acelle Mail (self-hosted)
The other major self-hosted ESP. Same direct database push, same workflow.
Step 1 — Find your Acelle API URL
Your install plus /api/v1, e.g. https://acelle.yourdomain.com/api/v1.
Step 2 — Generate an API token
In Acelle, go to Profile → API Token → Generate. Copy the token.
Step 3 — Connect in MailTidy3
Settings → Integrations → Acelle → Connect. Paste API URL and token. Test connection — your lists appear in the dropdown.
Mailchimp
Step 1 — Generate an API key
In Mailchimp, click your profile (top right) → Profile → Extras → API keys → Create A Key. The key looks like a1b2c3d4e5f6g7h8-us21. The suffix after the dash (us21) is your data centre — Mailchimp embeds it in the key itself.
Step 2 — Find your Audience ID
Go to Audience → All contacts → Settings → Audience name and defaults. The Audience ID is shown near the bottom — it's a 10-character alphanumeric string.
Step 3 — Connect
Settings → Integrations → Mailchimp → Connect. Paste the API key. Audiences populate automatically. Map fields like FNAME, LNAME, or any merge field you've added. Tags can be auto-applied (e.g. mailtidy-verified).
Brevo (formerly Sendinblue)
Step 1 — Generate an API key
In Brevo, click your account (top right) → SMTP & API → API Keys → Generate a new API key. Give it a name like "MailTidy 3" and copy the key (starts with xkeysib-).
Step 2 — Connect
Settings → Integrations → Brevo → Connect. Paste the API key. Lists load from your account. Optionally set default tags, then save.
Kit (formerly ConvertKit)
Step 1 — Get your API credentials
In Kit, go to Settings → Advanced → API. You need both the API Key (for general calls) and API Secret (for adding subscribers).
Step 2 — Connect
Settings → Integrations → Kit → Connect. Paste both keys. Kit organises subscribers around Forms and Sequences — pick which to push to. Tags can be applied to all pushed subscribers.
GetResponse
Step 1 — Generate an API key
In GetResponse, go to Menu (top left) → Integrations and API → API → Generate API key. Give it a label and copy the key.
Step 2 — Connect
Settings → Integrations → GetResponse → Connect. Paste the API key. Your campaigns (lists) populate automatically. Map custom fields, set tags, save.
ActiveCampaign
Step 1 — Get API URL and Key
In ActiveCampaign, go to Settings → Developer. You'll see your API URL (looks like https://youraccount.api-us1.com) and Key. Copy both.
Step 2 — Connect
Settings → Integrations → ActiveCampaign → Connect. Paste both values. Lists load. Map ActiveCampaign custom fields to MailTidy3 data, set tags, save.
Campaign Monitor
Step 1 — Get your API key
In Campaign Monitor, click your account name (top right) → Account Settings → API keys → Show API key. Copy it.
Step 2 — Find your Client ID
From the same Account Settings page, your Client ID is shown — it's a long alphanumeric string. You'll need it to identify the right account when multiple clients exist under one login.
Step 3 — Connect
Settings → Integrations → Campaign Monitor → Connect. Paste API key and Client ID. Lists load.
MailerLite
Step 1 — Generate an API token
In MailerLite, go to Integrations → API → Generate new token. Give it a name and copy the token. Note: MailerLite has both Classic and the new platform — make sure you're generating the token from the version you actually use.
Step 2 — Connect
Settings → Integrations → MailerLite → Connect. Paste the token. Groups populate (MailerLite uses Groups instead of Lists). Map custom fields, set tags, save.
Push options reference
| Option | What it does |
|---|---|
| Status filter | Choose which classifications to push. Default: only valid. Common alt: valid + catch_all. |
| Field mapping | Map MailTidy3 data (email, first name, last name, custom CSV columns) to ESP merge fields. |
| Tags | Auto-apply tags to every pushed subscriber, e.g. mailtidy-verified, april-2026. |
| Skip existing | If a subscriber is already on the target list, don't re-add or update them. Default: on. |
| Background processing | Pushes run in the queue. You can close the browser; the push completes and is logged. |
| Push history | Every push is logged with timestamp, target ESP/list, counts (added / updated / skipped / failed), and any error messages. |
It's tempting to push unknown results "just in case." Don't. The whole point of verification is to keep your sender reputation clean — pushing inconclusive results to your ESP defeats it. Stick to valid, optionally catch_all if you understand the trade-off.
For SaaS operators (Extended License)
If you run MailTidy3 as a SaaS for your own paying customers, the SaaS Admin includes a separate flow: SaaS Admin → Customer Lists → Push to autoresponder. This pushes your customer accounts (with consent) to your own marketing autoresponder — useful for product announcements, upgrade nudges, and onboarding sequences targeted at the people paying you.
Customer-list export is gated by an explicit consent setting in SaaS Admin → Settings → Customer Communications. It's off by default. Customers can also opt out individually from their account page.
Add a worker, double the throughput.
Each worker VPS runs a single Python agent that polls the engine for email batches. Workers on different IPs have independent rate-limit budgets with Gmail, Yahoo, and Outlook.
When to add workers
The main VPS handles ~150,000 emails/day comfortably. Add workers if you regularly process lists over 100,000 emails, run multiple client lists simultaneously, or need guaranteed same-day turnaround on large batches.
Adding a worker — 3 steps
lic_a1b2c3d4...$ curl -fsSL https://install.mailtidypro.com/worker | bash -s \
YOUR_TOKEN_HERE \
https://app.yourdomain.com
Running a worker on the same VPS as the main install gains nothing — same IP = same rate-limit budget. Workers only add capacity when they run on different IPs.
Full control, one place.
The admin panel is available to accounts with the admin role. The Extended License additionally unlocks the SaaS Admin section for running MailTidy3 as a multi-tenant service.
Admin capabilities
| Section | What you can do |
|---|---|
| Settings → General | App name, support email, enable/disable registrations |
| Settings → Engine | Engine URL, batch size, max emails per job, file size limits |
| Settings → SMTP | Configure outgoing email for notifications, password reset, verification |
| Settings → Auth | Toggle email verification, free credits on signup, auto-approve |
| Settings → Google OAuth | Enable Google login; shows button on login page when credentials are set |
| Settings → Appearance | Accent color, logo, font |
| Users | View, suspend, activate, delete users; edit credit balances |
| Workers | View active workers, kill ghost workers, toggle small-list bypass |
Extended license (SaaS Admin)
| Section | What you can do |
|---|---|
| Revenue dashboard | MRR, total users, verification volume, recent transactions |
| User management | Full CRUD on all users, plan changes, credit editing, export CSV |
| Pricing | Edit plan names, prices, verification limits; toggle active/inactive |
Sending emails from your install.
MailTidy3 sends emails for: account verification, password reset, and job completion notifications. These go through the SMTP settings you configure in the admin panel.
Recommended providers
| Provider | Free tier | Notes |
|---|---|---|
| Resend | 3,000/month | Best developer experience, easy DNS setup |
| Mailgun | 100/day | Reliable, good for transactional |
| SendGrid | 100/day | Well-known, easy SPF/DKIM |
| Gmail SMTP | 500/day | Works for small installs; requires App Password |
Settings
| Setting | Example |
|---|---|
| SMTP Host | smtp.resend.com |
| SMTP Port | 587 (TLS) or 465 (SSL) |
| Encryption | tls |
| Username | resend (or your email) |
| Password | your API key / app password |
| From Email | noreply@yourdomain.com |
| From Name | MailTidy3 |
If you haven't configured SMTP yet, go to Settings → Auth → Require email verification and turn it off. Users can log in immediately after signup. Turn it back on once SMTP is working.
Regular vs Extended.
| Feature | Regular ($229) | Extended ($1,499) |
|---|---|---|
| Email verification engine | ✅ | ✅ |
| REST API | ✅ | ✅ |
| Multi-worker scaling | ✅ | ✅ |
| Single-user install | ✅ | ✅ |
| SaaS multi-tenant (sell to others) | ❌ | ✅ |
| Revenue dashboard | ❌ | ✅ |
| User management panel | ❌ | ✅ |
| Pricing plan editor | ❌ | ✅ |
How to activate
Go to Settings → Envato License. Select your license type, paste your Envato purchase code, and click Activate. The purchase code is in your Envato account under Downloads → License certificate & purchase code.
Where to host.
DigitalOcean permanently blocks outbound port 25 at the hypervisor level. There is no ticket, no business plan, and no workaround. If you install MailTidy3 on DigitalOcean, every single verification will return a timeout error. Same applies to AWS EC2, Google Cloud, and Azure.
| Provider | Port 25 | Recommended? | Notes |
|---|---|---|---|
| Contabo | Open by default | ✅ Primary (EU) | ~€6/mo · 4 vCPU / 8 GB · best value · tested April 2026 |
| RackNerd | Open by default | ✅ Primary (US) | ~$22/yr · US-based · affordable · tested April 2026 |
| Vultr | Unlock on request | ✅ Good backup | Email support; say "email list verification" |
| Hetzner | Blocked 30 days | ⚠️ Slow start | EU-only; unlocks after 30-day clean account |
| DigitalOcean | Permanently blocked | ❌ Never | No path to unblock. Do not use. |
| AWS EC2 | Blocked by default | ❌ Avoid | Unblock form exists but usually denied |
| Google Cloud | Permanently blocked | ❌ Never | No unblock path |
| Azure | Permanently blocked | ❌ Never | No unblock path |
Contabo (~€6/mo) — port 25 open by default, proven reliable, best price/performance. Used in the MailTidy 3 production infrastructure powering this demo.
Maximum deliverability.
Best results, stay unblocked.
MailTidy3 is already built with anti-block mechanisms baked in — per-domain semaphores, automatic soft-block detection, 30-minute IP cooldowns, and multi-worker routing. But there are five additional steps you should complete on every VPS you install, including workers. They take under an hour total and the difference in accuracy is significant.
Skipping these steps won't stop the install from working — but you'll see more unknown results, faster soft-blocks, and lower accuracy on Gmail and Yahoo specifically. None of these require a developer. Each step has a verification command so you know it worked.
Step 1 — Set reverse DNS (PTR record) Most important
When your VPS connects to Gmail's mail server, Gmail does a reverse DNS lookup on your IP. If the IP doesn't resolve back to a proper hostname, Gmail silently downgrades or rejects the conversation. This is the single most common cause of unknown results on a fresh install.
You need one PTR record per VPS — set from the Contabo control panel, not from DNS. DNS A records alone are not enough.
What to set
| VPS | IP address | PTR value to set | Matching A record needed |
|---|---|---|---|
| Main install | Your main VPS IP | mail.yourdomain.com | mail.yourdomain.com → your IP |
| Worker 1 | Your worker VPS IP | mail2.yourdomain.com | mail2.yourdomain.com → worker IP |
| Worker 2 | Your second worker IP | mail3.yourdomain.com | mail3.yourdomain.com → worker 2 IP |
How to set it on Contabo
Go to my.contabo.com → Your Services → VPS. Click the VPS you want to configure.
In the VPS detail page, look for the Reverse DNS or rDNS section. It shows your current PTR record — by default it's something like vmi123456.contaboserver.net.
Enter mail.yourdomain.com (or mail2. for workers). Click Save. Important: the A record for mail.yourdomain.com must already exist in your DNS pointing to this VPS IP — Contabo validates this before accepting the PTR.
PTR records propagate slowly. After 20 minutes, run the verification command below.
# Verify PTR (reverse DNS) — replace with your actual IP $ dig -x YOUR_VPS_IP +short mail.yourdomain.com. # ✅ correct vmi123456.contaboserver.net. # ❌ not set yet — wait longer or re-check Contabo panel # Verify the forward lookup matches (FCrDNS check) $ dig mail.yourdomain.com +short YOUR_VPS_IP # ✅ must match the IP above
Gmail performs a Forward-Confirmed Reverse DNS check: IP → PTR → hostname → A record → must resolve back to the same IP. All three must be consistent. If mail.yourdomain.com resolves to a different IP than the one connecting, the check fails even if the PTR is set.
Step 2 — Add your VPS IPs to SPF
SPF (Sender Policy Framework) tells receiving mail servers which IPs are authorised to send mail from your domain. The engine sends MAIL FROM: verify@yourdomain.com during every probe. Without your VPS IPs in the SPF record, the probe gets a softfail (~all) which some providers treat as suspicious.
Go to your DNS provider (Cloudflare, Namecheap, etc.) and find the TXT record for your domain. Edit the existing SPF record — do not add a second one, two SPF records on the same domain breaks SPF entirely.
# If you have Google Workspace email (support@yourdomain.com): v=spf1 include:_spf.google.com ip4:MAIN_VPS_IP ip4:WORKER_1_IP ip4:WORKER_2_IP ~all # If you don't use Google Workspace: v=spf1 ip4:MAIN_VPS_IP ip4:WORKER_1_IP ~all # Add each worker IP as a new ip4: entry when you add workers
# Verify SPF propagated (takes 1–5 minutes on Cloudflare) $ dig TXT yourdomain.com +short | grep spf "v=spf1 include:_spf.google.com ip4:84.46.245.142 ~all" # ✅
Step 3 — Set the correct HELO hostname per worker
During every SMTP probe, the engine introduces itself with an EHLO command: "Hello, I am mail.yourdomain.com." The receiving server cross-checks this hostname against the connecting IP. A mismatch — or a generic Contabo hostname like vmi123456.contaboserver.net — causes Gmail to treat the connection as suspicious.
Each worker should EHLO with its own dedicated hostname that has a matching A record and PTR.
| VPS | HELO hostname to use | Where to set it |
|---|---|---|
| Main install | mail.yourdomain.com | /opt/mailtidy-engine/.env → SMTP_HELO_DOMAIN |
| Worker VPS | mail2.yourdomain.com | /opt/mailtidy-worker/.env → MAILTIDY_HOSTNAME |
# Main VPS — edit /opt/mailtidy-engine/.env SMTP_HELO_DOMAIN=mail.yourdomain.com SMTP_MAIL_FROM=verify@yourdomain.com # Worker VPS — edit /opt/mailtidy-worker/.env MAILTIDY_HOSTNAME=mail2.yourdomain.com
# After editing, restart the relevant service # On main VPS: $ sudo systemctl restart mailtidy-engine # On worker VPS: $ sudo systemctl restart mailtidy-worker
Step 4 — Check your IP is not already blacklisted
If your VPS IP is listed on Spamhaus SBL, CBL, or similar RBLs, every single probe will be rejected before it even gets a real response. This is the first thing to check when you see 100% unknown results on a fresh install.
Run this on each VPS immediately after provisioning, before installing anything:
# Get your public IP $ curl -s ifconfig.me 84.46.245.142 # Check Spamhaus — reverse the IP octets before the lookup # For IP 84.46.245.142 → check 142.245.46.84.zen.spamhaus.org $ dig +short 142.245.46.84.zen.spamhaus.org # No output = not listed ✅ # 127.0.0.2 or similar = listed ❌ # Easy web alternative: https://mxtoolbox.com/blacklists.aspx
Contact Contabo support and request a replacement IP for the VPS. Mention you're running email verification software and the current IP has a poor reputation from a previous tenant. Contabo can usually replace it within a few hours. If the replacement is also listed, provision a fresh VPS — listed IPs come from recycled tenants who sent spam.
Step 5 — Understand the rate limits and work with them
MailTidy3's engine already enforces per-domain concurrency limits and automatic cooldowns. Understanding how the limits work helps you get the most out of each IP without triggering blocks.
| Provider | Practical daily limit per IP | What happens if exceeded | Recovery time |
|---|---|---|---|
| Gmail / Google | ~4,000–5,000 probes | Connections time out silently | ~30 minutes |
| Yahoo / AOL | ~3,000–4,000 probes | 421 temporary rejection | ~20–30 minutes |
| Outlook / Hotmail | ~2,000–3,000 probes | Connection refused or 421 | ~60 minutes |
| Custom domains | Varies widely | Usually 421 greylisting | ~5–15 minutes |
The engine detects soft-blocks automatically — when error rate exceeds 80% for a provider on a given IP, that IP is put into a 30-minute cooldown for that provider only, and remaining emails are routed to other healthy worker IPs. You can monitor this in real time on the Workers → IP Health panel in your admin dashboard.
Each worker VPS is one IP and one independent rate-limit budget. If you regularly process lists with more than 50% Gmail addresses and need 50,000+ per day, you need at least 2 workers. The Adding workers guide shows how to add one in under 5 minutes.
| Setup | Realistic daily capacity |
|---|---|
| 1 VPS (main only) | 50,000–150,000 emails/day |
| 2 VPS (main + 1 worker) | 100,000–300,000 emails/day |
| 5 VPS | 250,000–750,000 emails/day |
| 10 VPS | 500,000–1,500,000 emails/day |
Quick checklist — do this for every VPS
Run through this for your main VPS and every worker you add. The whole process takes about 20 minutes per VPS (mostly waiting for PTR propagation).
End-to-end test after setup
After completing all five steps, test with a known-valid email address using your API key. A correct result looks like this:
$ curl -s \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email":"known-valid@gmail.com"}' \ https://app.yourdomain.com/api/v1/verify | python3 -m json.tool
✅ Correct result — everything is configured properly: { "email": "known-valid@gmail.com", "status": "valid", "smtp_code": 250, "smtp_message": "2.1.5 OK ...", "mx_host": "aspmx.l.google.com", "duration_ms": 312.4 ← under 1 second = healthy connection } ❌ Problem result — PTR/HELO misconfigured or IP blocked: { "status": "unknown", "smtp_code": null, "error": "timeout", "duration_ms": 10014.6 ← 10 seconds = connection never completed }
The Workers page in your admin dashboard includes a live IP Health panel. It shows per-provider status for every registered worker IP — whether it's healthy or in a cooldown, the recent error rate, and how many minutes remain in any active cooldown. Cooldowns are automatic and self-healing. You can also manually force or clear a cooldown from the panel without touching the server.
When things go wrong.
All verifications return UNKNOWN
Cause: Port 25 is blocked on your VPS, or aiosmtplib is attempting STARTTLS and failing SSL validation.
Test: timeout 10 bash -c 'cat < /dev/tcp/gmail-smtp-in.l.google.com/25' — should print a 220 banner.
Fix: Switch to Contabo or Vultr (with port 25 unlocked). Ensure the worker service runs as root.
Results table shows "No results match this filter"
Cause: Hard-cached browser assets. Fix: Ctrl+Shift+R (hard refresh).
Job stuck at 0% / never starts
Check queue workers: sudo supervisorctl status — both mailtidy-queue processes should be RUNNING.
Check engine: curl http://127.0.0.1:8000/health/live — should return {"status":"ok"}.
Restart everything: sudo systemctl restart mailtidy-engine mailtidy-worker && sudo supervisorctl restart mailtidy-queue:*
Ghost workers in the workers panel
Cause: A worker registered and then the engine restarted, losing its memory.
Fix: Click Kill ghosts in the Workers panel, or restart the engine: sudo systemctl restart mailtidy-engine
Log locations
# Engine logs $ sudo tail -f /var/log/mailtidy-engine/engine.log # Worker logs $ sudo tail -f /var/log/mailtidy-engine/worker.log # Laravel logs $ sudo tail -f /var/www/mailtidy/storage/logs/laravel.log # Nginx logs $ sudo tail -f /var/log/nginx/error.log
Why Gmail says unknown.
You install MailTidy3, run your first verification, and Yahoo addresses come back in 1.5 seconds — clean, accurate, perfect. But every Gmail address times out at 10 seconds and returns unknown. Nothing is broken. This is Gmail deliberately ignoring you, and there is a precise fix.
Gmail's mail servers answer every knock — but only from visitors who look legitimate. Your VPS has a generic datacenter name, so Gmail holds the door open in silence until you give up and leave.
What actually happens under the hood
When MailTidy3 verifies an email address, it connects to the recipient's mail server on port 25 and has a short SMTP conversation — no actual email is ever sent. Gmail's MX servers (gmail-smtp-in.l.google.com) accept the TCP connection from your IP, but before responding they do a reverse DNS lookup (PTR record) on your IP address.
What they find on a fresh Contabo VPS is something like vmi1478013.contaboserver.net — a generic hostname that millions of spam servers also have. Gmail's response: silently hold the connection open for 10 seconds, then drop it. Your verification returns unknown. This is not a bug. It is Gmail protecting its users from probes it cannot verify.
contaboserver.net
.com ✓
How to diagnose it
First, confirm the problem is rDNS and not a soft-block. Run these two tests back-to-back on your VPS:
# Test 1 — Does Yahoo work fast? (it should) $ curl -s -w "\nTime: %{time_total}s\n" https://app.yourdomain.com/api/v1/verify \ -X POST \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email":"test@yahoo.com"}' # Expect: status "valid" or "catch_all" in ~1-2 seconds # Test 2 — Does Gmail time out? $ curl -s -w "\nTime: %{time_total}s\n" https://app.yourdomain.com/api/v1/verify \ -X POST \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email":"someone@gmail.com"}' # If this takes 10 seconds and returns "unknown" — it's rDNS.
# Check your current PTR record $ dig -x YOUR_VPS_IP +short # Bad: vmi1478013.contaboserver.net. # Good: mail.yourdomain.com.
The fix — 2 steps, 5 minutes
Log in to contabo.com → left sidebar → Reverse DNS Management. Find your VPS IP and set the PTR value to mail.yourdomain.com (replace with your actual domain). Save.
The PTR record says "this IP belongs to mail.yourdomain.com". Gmail then checks the other direction — it looks up mail.yourdomain.com and expects to get your IP back. Both must match or Gmail still rejects you.
In your DNS control panel (Cloudflare, GoDaddy, Namecheap, or wherever your domain is managed), add an A record:
Verify it worked
PTR propagation typically takes 30–60 minutes. Run this to check:
# Both commands should reference each other $ dig -x 84.46.245.142 +short mail.mailtidypro.com. # ← should return your domain, not contaboserver.net $ dig mail.mailtidypro.com A +short 84.46.245.142 # ← should return your IP
Once both match, test Gmail verification directly:
$ curl -s -w "\nTime: %{time_total}s\n" https://app.yourdomain.com/api/v1/verify \ -X POST \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email":"someone@gmail.com"}' # After fix: status "valid" or "invalid" in under 2 seconds. # Before fix: status "unknown" after exactly 10 seconds.
Gmail response: silent timeout
Duration: 10,000 ms
Status: unknown
Gmail response: SMTP 250 / 550
Duration: < 1,500 ms
Status: valid / invalid
Every VPS you add as a verification worker has its own IP — and its own rDNS problem. When you add a worker, set the PTR for that IP to something like worker1.yourdomain.com and add the matching A record in Cloudflare. Same two steps, different hostname.
Microsoft's servers behave similarly but for a different reason — they whitelist known senders. rDNS helps, but Outlook and Hotmail addresses may still return unknown from fresh IPs. This is a Microsoft policy limitation, not a MailTidy3 bug. The classification is honest: if Microsoft won't say, neither will we.
Six tools, built in.
Email deliverability lives or dies on DNS. MailTidy3 ships six diagnostic tools that let you check the records that matter — without leaving the dashboard or pasting your domain into someone else's site.
Open Diagnostics → Tools in the sidebar. Every user (including demo accounts) can run them; results are cached for six hours per (domain or IP, tool) pair so repeated checks are instant. All six are also exposed as API endpoints for Extended License holders.
The six tools
| Tool | What it answers | Input |
|---|---|---|
| DMARC Checker | Is the domain's DMARC policy published, and what does it say (none / quarantine / reject)? | Domain |
| SPF Checker | Does the SPF record exist, parse cleanly, and have a valid all mechanism? | Domain |
| DKIM Checker | Are DKIM keys published? Auto-scans 14 common selectors (default, google, k1, selector1…) in parallel. | Domain (+ optional selector) |
| rDNS / PTR Checker | Does the IP resolve back to a hostname? Is FCrDNS chain valid (PTR → A → original IP)? | IP address |
| Blacklist Checker | Is the IP listed on any of 8 major RBLs (Spamhaus, Barracuda, SORBS, etc.)? Cached 6 hours. | IP address |
| SMTP Tester | Connect to an SMTP server, run AUTH, and report the full step-by-step conversation. Credentials never stored. | Host, port, credentials |
Input persistence between tools
Type a domain into the SPF Checker, then jump to the DMARC or DKIM Checker via the switcher strip — the domain is already filled in. Same for IP-based tools (rDNS and Blacklist share the IP field). It's stored in sessionStorage, so it clears when you close the tab.
Tool API (Extended License)
Each tool is also a REST endpoint. Useful if you want to surface the results inside another product or run scheduled checks on your own infrastructure.
Authentication — the tool API uses the same bearer-token scheme as /api/v1/verify. Generate a key in Settings → API Keys; the key must be issued under an Extended License (or by an admin) to access tool routes.
POST https://app.yourdomain.com/api/v1/tools/spf Authorization: Bearer mtp_live_xxxxxxxxxxxxxxxx Content-Type: application/json { "domain": "example.com" } // Response { "domain": "example.com", "record": "v=spf1 include:_spf.google.com ~all", "valid": true, "all_mechanism": "~all", "includes": ["_spf.google.com"], "warnings": [], "checked_at": "2026-05-05T12:34:56Z" }
All tool endpoints
| Endpoint | Body |
|---|---|
POST /api/v1/tools/dmarc | { "domain": "..." } |
POST /api/v1/tools/spf | { "domain": "..." } |
POST /api/v1/tools/dkim | { "domain": "...", "selector": "default" } (selector optional) |
POST /api/v1/tools/rdns | { "ip": "..." } |
POST /api/v1/tools/blacklist | { "ip": "..." } |
POST /api/v1/tools/smtp | { "host": "...", "port": 587, "username": "...", "password": "..." } |
Rate limits
| Route | Limit | Notes |
|---|---|---|
/api/tools/* (session) | 50 / day per user | Available to all users including demo accounts. |
/api/v1/tools/* (API key) | Per-key daily quota | Extended License only. Counts against the same quota as /api/v1/verify. |
The SMTP Tester opens a one-shot connection, reports the result, and forgets. Credentials aren't logged, persisted, or cached. The same applies to the API endpoint — request bodies pass through the controller and are immediately discarded after the SMTP transaction completes.
When you provision a new worker, the four-check sequence is: rDNS → SPF → Blacklist → SMTP Tester. If those four pass cleanly, your IP is ready to verify against Gmail/Yahoo/Outlook without surprises. Maximum deliverability covers the full setup.
Accept payments. Your gateway, your money.
MailTidy3 supports five payment gateways. Each is off by default — you enable only the one(s) you want. All configuration is done through the admin panel; no `.env` editing is needed after install.
Go to Admin → Settings → Payment Gateways to configure any gateway. Changes take effect immediately — no server restart required.
Use Stripe if you're based in the US, UK, EU, or Australia — it has the best developer experience and widest currency support. Use Razorpay for India. Use Paystack or Flutterwave for Africa. Use Paddle if you want them to handle VAT/tax compliance automatically (they act as merchant of record).
Stripe
Stripe is the recommended gateway for most installs. It supports subscriptions, one-time payments, and automatic invoicing in 135+ currencies.
Step 1 — Create a Stripe account
Go to stripe.com and sign up. Complete identity verification before going live.
Step 2 — Get your API keys
In the Stripe Dashboard go to Developers → API keys. Copy your Publishable key (pk_live_...) and Secret key (sk_live_...). Use test keys (pk_test_... / sk_test_...) while testing.
Step 3 — Create your products
In Stripe Dashboard go to Product catalogue → Add product. Create one product per plan that matches what you've set up in Admin → Pricing. For each product, set a recurring price (monthly or yearly). Copy the Price ID (price_xxx) — you'll need it in the next step.
Step 4 — Enter credentials in MailTidy3
Go to Admin → Settings → Payment Gateways → Stripe. Enter your Publishable key, Secret key, and the Price IDs for each plan. Toggle Enable Stripe on. Click Save.
Step 5 — Set up the webhook
In Stripe Dashboard go to Developers → Webhooks → Add endpoint. Set the endpoint URL to:
https://app.yourdomain.com/api/billing/stripe/webhookSelect these events: customer.subscription.created, customer.subscription.updated, customer.subscription.deleted, invoice.payment_succeeded, invoice.payment_failed.
Copy the Webhook signing secret (whsec_...) and paste it into Admin → Settings → Payment Gateways → Stripe → Webhook Secret.
Step 6 — Test it
Use Stripe's test card 4242 4242 4242 4242 (any future expiry, any CVC) to complete a test purchase. Confirm the subscription appears in Admin → Billing.
The Secret key (sk_live_...) must only be entered in the admin panel. Never paste it in a browser console, email, or chat.
Paddle
Paddle acts as the merchant of record — they handle VAT, GST, and sales tax on your behalf. Ideal if you sell globally and don't want to deal with tax compliance yourself.
Step 1 — Create a Paddle account
Go to paddle.com. Paddle requires business verification before you can go live — allow 1–3 business days.
Step 2 — Get your API credentials
In the Paddle Dashboard go to Developer Tools → Authentication. Copy your Vendor ID and generate an API key. Also note your Client-side token for the frontend.
Step 3 — Create your plans in Paddle
Go to Catalog → Products. Create a product for each plan. Under each product, create a Price (recurring). Copy the Price ID for each.
Step 4 — Enter credentials in MailTidy3
Go to Admin → Settings → Payment Gateways → Paddle. Enter your Vendor ID, API key, Client-side token, and Price IDs. Toggle Enable Paddle on. Click Save.
Step 5 — Set up the webhook
In Paddle Dashboard go to Developer Tools → Notifications → New destination. Set URL to:
https://app.yourdomain.com/api/billing/paddle/webhookSelect all subscription and payment events. Copy the secret key Paddle generates and enter it in Admin → Settings → Payment Gateways → Paddle → Webhook Secret.
Razorpay (India)
Razorpay is the most popular payment gateway for India, supporting UPI, NetBanking, cards, and wallets in INR.
Step 1 — Create a Razorpay account
Go to razorpay.com. Complete KYC verification. You can use test mode immediately without KYC.
Step 2 — Get API keys
In the Razorpay Dashboard go to Settings → API Keys → Generate Test Key (or Live Key). Copy the Key ID (rzp_live_...) and Key Secret.
Step 3 — Create plans in Razorpay
Go to Subscriptions → Plans → Create plan. Create one plan per pricing tier. Copy the Plan ID (plan_...) for each.
Step 4 — Enter credentials in MailTidy3
Go to Admin → Settings → Payment Gateways → Razorpay. Enter your Key ID, Key Secret, and Plan IDs. Toggle Enable Razorpay on. Click Save.
Step 5 — Set up the webhook
In Razorpay Dashboard go to Settings → Webhooks → Add new webhook. Set URL to:
https://app.yourdomain.com/api/billing/razorpay/webhookSelect events: subscription.activated, subscription.charged, subscription.cancelled, payment.captured, payment.failed.
Enter a Webhook Secret of your choice and paste the same value in Admin → Settings → Payment Gateways → Razorpay → Webhook Secret.
Paystack (Africa)
Paystack is the leading gateway for Nigeria, Ghana, Kenya, and South Africa. Supports cards, bank transfers, and mobile money.
Step 1 — Create a Paystack account
Go to paystack.com. Business registration required for live mode.
Step 2 — Get API keys
In the Paystack Dashboard go to Settings → API Keys & Webhooks. Copy your Public Key (pk_live_...) and Secret Key (sk_live_...).
Step 3 — Create plans
Go to Products → Plans → Create Plan. Create one plan per pricing tier. Copy the Plan Code for each.
Step 4 — Enter credentials in MailTidy3
Go to Admin → Settings → Payment Gateways → Paystack. Enter your Public Key, Secret Key, and Plan Codes. Toggle Enable Paystack on. Click Save.
Step 5 — Set up the webhook
In Paystack Dashboard go to Settings → API Keys & Webhooks → Webhook URL. Set it to:
https://app.yourdomain.com/api/billing/paystack/webhookPaystack signs all webhooks with your Secret Key — no separate webhook secret needed.
Flutterwave (Africa)
Flutterwave operates in 30+ African countries and supports cards, bank transfers, mobile money, and USSD.
Step 1 — Create a Flutterwave account
Go to flutterwave.com. Complete business verification for live mode.
Step 2 — Get API keys
In the Flutterwave Dashboard go to Settings → API → API Keys. Copy your Public Key, Secret Key, and Encryption Key.
Step 3 — Create payment plans
Go to Payment Plans → Create a plan. Create one plan per pricing tier. Copy the Plan ID for each.
Step 4 — Enter credentials in MailTidy3
Go to Admin → Settings → Payment Gateways → Flutterwave. Enter your Public Key, Secret Key, Encryption Key, and Plan IDs. Toggle Enable Flutterwave on. Click Save.
Step 5 — Set up the webhook
In Flutterwave Dashboard go to Settings → Webhooks. Set URL to:
https://app.yourdomain.com/api/billing/flutterwave/webhookGenerate a Secret Hash in the Flutterwave webhook settings and enter the same value in Admin → Settings → Payment Gateways → Flutterwave → Webhook Secret.
Every gateway has a test/sandbox mode. Always complete a test transaction — including a simulated webhook — before switching to live keys. Your customers' first payment must work.
Common questions.
Does MailTidy3 send actual emails?
No. The verification engine connects to recipient mail servers, introduces itself (EHLO), and asks whether a mailbox exists (RCPT TO) — then disconnects before sending anything. No email is ever delivered.
Will my IP get blacklisted?
MailTidy3 uses per-domain rate limiting (max 5 concurrent probes per recipient domain) and automatic cooldown when error rates spike. During testing, 4,400+ emails through a single IP triggered a temporary Gmail soft-block — which cleared in 30 minutes. The system detects this automatically.
Can I white-label this for my customers?
Yes, with the Extended License. Install MailTidy3 on your own VPS, customize branding through the admin panel, point your domain at it, and sell verification credits to your customers.
What's the difference between Regular and Extended license?
Regular is for using MailTidy3 for your own email verification needs. Extended allows you to run it as a SaaS — charging your own customers for verification. See the license comparison table above.
How accurate is it?
In pre-launch testing across ~500 hand-verified results, classification was correct in every case. Catch-all detection was a major improvement over the original MailTidy — the new probe sends a random address before classifying as "valid".
Does it work with Outlook/Hotmail?
Microsoft's mail servers are conservative and don't always return definitive accept/reject responses for unrecognised source IPs. Results for @outlook.com and @hotmail.com will often come back as "unknown". This is a limitation of Microsoft's SMTP behaviour, not a bug.
Can I run it on 1 GB RAM?
Yes — 1 GB is enough to get started and test. For production workloads, 2–4 GB is recommended. The Python engine and Redis together use roughly 400–600 MB at rest.
Is there a hosted version?
Not currently. MailTidy3 is self-hosted only — you own the server and the data. A hosted cloud edition is on the roadmap.