Ever had fake signups flood your app with addresses like user@mailinator.com or nobody@guerrillamail.com?
Disposable email services let anyone create a throwaway inbox in seconds. Great for privacy — terrible for your conversion metrics, trial abuse, and email deliverability.
Here's how to detect them programmatically using DNS lookups — no third-party blocklist subscription required.
The Strategy
Instead of maintaining a static list of known disposable domains (which goes stale fast), we'll use a multi-signal approach:
- MX record analysis — disposable services share mail servers
- Domain age heuristics — newly registered domains are suspicious
- Known provider fingerprinting — match MX records against known disposable mail infrastructure
Step 1: Look Up MX Records via API
We need DNS data for the email domain. Instead of running dig commands, we'll use a free DNS API:
async function getDomainDNS(domain) {
const API = 'https://agent-gateway-kappa.vercel.app';
const res = await fetch(`${API}/api/resolve/${domain}`, {
headers: { 'X-Api-Key': 'YOUR_API_KEY' }
});
return res.json();
}
Get a free API key (200 credits) at frostbyte.dev/gateway.
Step 2: Build the Detector
Here's the full disposable email detector:
// Known disposable email MX server patterns
const DISPOSABLE_MX_PATTERNS = [
'mailinator.com',
'guerrillamail',
'sharklasers.com',
'grr.la',
'guerrillamailblock.com',
'tempmail',
'temp-mail',
'throwaway',
'yopmail',
'maildrop.cc',
'dispostable',
'mailnesia',
'trashmail',
'fakeinbox',
'mailcatch',
'getairmail',
'mohmal',
'minutemail',
'tempinbox',
'discard.email',
'emailondeck',
'crazymailing',
];
// Known disposable domains (top 50 most common)
const DISPOSABLE_DOMAINS = new Set([
'mailinator.com', 'guerrillamail.com', 'guerrillamail.de',
'sharklasers.com', 'grr.la', 'guerrillamailblock.com',
'tempmail.com', 'temp-mail.org', 'throwaway.email',
'yopmail.com', 'yopmail.fr', 'maildrop.cc',
'dispostable.com', 'mailnesia.com', 'trashmail.com',
'fakeinbox.com', 'mailcatch.com', 'getairmail.com',
'mohmal.com', 'minutemail.com', 'tempinbox.com',
'discard.email', 'emailondeck.com', 'crazymailing.com',
'getnada.com', 'mailsac.com', 'harakirimail.com',
'33mail.com', 'maildrop.cc', 'inboxbear.com',
'spamgourmet.com', 'mytemp.email', 'burnermail.io',
'temp-mail.io', '10minutemail.com', 'tempail.com',
'tempr.email', 'binkmail.com', 'mailnull.com',
'trashmail.net', 'trashmail.me', 'trashmail.io',
'disposable.email', 'tempmailaddress.com', 'tmpmail.net',
'tmpmail.org', 'emailfake.com', 'cuvox.de',
'armyspy.com', 'dayrep.com',
]);
async function checkEmail(email) {
const domain = email.split('@')[1]?.toLowerCase();
if (!domain) return { disposable: false, reason: 'invalid_email' };
const result = {
email,
domain,
disposable: false,
confidence: 0,
signals: [],
};
// Signal 1: Direct domain match
if (DISPOSABLE_DOMAINS.has(domain)) {
result.disposable = true;
result.confidence = 0.99;
result.signals.push('known_disposable_domain');
return result;
}
// Signal 2: DNS lookup
try {
const API = 'https://agent-gateway-kappa.vercel.app';
const dns = await fetch(`${API}/api/resolve/${domain}`, {
headers: { 'X-Api-Key': 'YOUR_API_KEY' }
}).then(r => r.json());
// Check MX records against known disposable patterns
const mxRecords = dns.mx || [];
for (const mx of mxRecords) {
const mxHost = (mx.exchange || mx.value || '').toLowerCase();
for (const pattern of DISPOSABLE_MX_PATTERNS) {
if (mxHost.includes(pattern)) {
result.disposable = true;
result.confidence = 0.95;
result.signals.push(`mx_match: ${mxHost} contains ${pattern}`);
}
}
}
// Signal 3: No MX records at all (suspicious)
if (mxRecords.length === 0) {
result.confidence += 0.3;
result.signals.push('no_mx_records');
}
// Signal 4: No SPF record (legitimate services usually have SPF)
const txtRecords = dns.txt || [];
const hasSPF = txtRecords.some(t =>
(t.value || t || '').includes('v=spf1')
);
if (!hasSPF) {
result.confidence += 0.1;
result.signals.push('no_spf_record');
}
// Signal 5: No DMARC (legitimate services usually have DMARC)
// Check _dmarc subdomain
try {
const dmarc = await fetch(
`${API}/api/resolve/_dmarc.${domain}`,
{ headers: { 'X-Api-Key': 'YOUR_API_KEY' } }
).then(r => r.json());
const hasDMARC = (dmarc.txt || []).some(t =>
(t.value || t || '').includes('v=DMARC1')
);
if (!hasDMARC) {
result.confidence += 0.1;
result.signals.push('no_dmarc_record');
}
} catch (e) {
result.signals.push('dmarc_lookup_failed');
}
// Threshold: if confidence > 0.5, flag as likely disposable
if (result.confidence >= 0.5) {
result.disposable = true;
}
} catch (err) {
result.signals.push('dns_lookup_failed');
}
return result;
}
Step 3: Use It
// Test against known disposable addresses
const tests = [
'user@mailinator.com', // Known disposable
'test@guerrillamail.com', // Known disposable
'john@gmail.com', // Legitimate
'jane@company.com', // Legitimate
'fake@tempmail.com', // Known disposable
];
for (const email of tests) {
const result = await checkEmail(email);
console.log(
`${result.disposable ? '🚫' : '✅'} ${email}`,
`(confidence: ${(result.confidence * 100).toFixed(0)}%)`,
result.signals.length ? `[${result.signals.join(', ')}]` : ''
);
}
Output:
🚫 user@mailinator.com (confidence: 99%) [known_disposable_domain]
🚫 test@guerrillamail.com (confidence: 99%) [known_disposable_domain]
✅ john@gmail.com (confidence: 0%)
✅ jane@company.com (confidence: 0%)
🚫 fake@tempmail.com (confidence: 99%) [known_disposable_domain]
Step 4: Express Middleware
Drop this into your signup route:
import express from 'express';
const app = express();
app.use(express.json());
async function rejectDisposableEmails(req, res, next) {
const email = req.body?.email;
if (!email) return next();
const result = await checkEmail(email);
if (result.disposable) {
return res.status(422).json({
error: 'Disposable email addresses are not allowed',
reason: result.signals[0],
});
}
next();
}
app.post('/api/signup', rejectDisposableEmails, (req, res) => {
// Email passed validation - proceed with signup
res.json({ success: true, message: 'Account created' });
});
app.listen(3000);
Why DNS > Static Lists
| Approach | Pros | Cons |
|---|---|---|
| Static blocklist | Fast, no API calls | Goes stale, misses new providers |
| DNS MX analysis | Catches new providers using known infrastructure | Requires API call per check |
| Combined (this approach) | Fast for known domains + catches unknown ones | Best of both worlds |
The DNS approach catches disposable services even when they use custom domains, because they often route through the same mail servers (like mailinator.com MX records).
Going Further
You can enhance this with additional signals:
- Domain WHOIS age — domains registered in the last 30 days are higher risk
- IP geolocation of mail server — check if MX servers are in unusual locations
- TXT record analysis — legitimate domains usually have SPF, DKIM, and DMARC
All of these are available through the Frostbyte API — DNS lookups, IP geolocation, and domain analysis in one place.
Get started free: Create an API key (200 free credits, no credit card required).
The full DNS API docs are at frostbyte.dev/gateway.
Top comments (0)