On the surface, validating an email address seems trivial. Check for an @ sign, make sure there is a domain, maybe run a regex. In practice, email validation is one of the most deceptively complex problems in web development. The RFC 5321 specification for email addresses allows characters that would surprise most developers: plus signs, dots in unusual positions, quoted strings with spaces, and even IP addresses instead of domain names. A syntactically valid email can still bounce because the mailbox does not exist, the domain has expired, or the server is misconfigured. And a syntactically valid, deliverable email can still be worthless because it belongs to a disposable email service that the user will never check again.
Email Bouncer attacks this problem with a multi-layer validation strategy. Instead of relying on a single check, it combines client-side syntax validation, the Disify API for disposable email detection, and the EVA (Email Validation API) for deeper domain and mailbox verification. Each layer catches problems the others miss, and the combined results feed into a quality scoring algorithm that gives every email address a score from 0 to 100.
This is not just a toy project. Email list quality directly impacts deliverability rates, sender reputation, and ultimately revenue for any business that relies on email communication. A single-digit percentage improvement in list quality can translate to thousands of dollars in recovered deliverability. The patterns we build here are the same ones used by production email validation services that charge per-validation fees.
Disify specializes in a single task: determining whether an email address belongs to a disposable email service. Services like Guerrilla Mail, 10MinuteMail, and Mailinator provide temporary email addresses that users can create instantly without registration. These addresses are popular for bypassing email verification requirements, and emails sent to them are rarely read. Disify maintains a constantly updated database of these providers.
| Property | Detail |
|---|---|
| Base URL | https://disify.com/api/email/{email} |
| Auth | None required |
| Rate Limit | Reasonable use (undocumented) |
| Format | JSON |
| CORS | Enabled |
The response includes:
format — Whether the email format is validdomain — The domain portion of the emaildisposable — Whether the domain is a known disposable email providerdns — Whether the domain has valid DNS recordsEVA provides deeper validation by checking whether the email domain has valid MX (mail exchange) records, whether the mail server responds, and additional metadata. It is complementary to Disify: while Disify focuses on disposable detection, EVA focuses on deliverability signals.
| Property | Detail |
|---|---|
| Base URL | https://api.eva.pingutil.com/email?email={email} |
| Auth | None required |
| Rate Limit | Reasonable use |
| Format | JSON |
| CORS | Enabled |
EVA's response includes:
data.valid_syntax — Syntax validation resultdata.disposable — Independent disposable checkdata.deliverable — Whether the email appears deliverabledata.spam — Whether the email is associated with spamdata.free_email — Whether it is from a free provider (Gmail, Yahoo, etc.)data.domain — Domain informationBy combining both APIs, we get redundant disposable detection (each maintains its own database, so one may catch providers the other misses), plus the deliverability and spam signals that Disify does not provide.
Email Bouncer uses a pipeline architecture where each validation layer runs in sequence, and subsequent layers can short-circuit based on earlier results. If client-side syntax validation fails, there is no point calling the APIs. If Disify flags the email as disposable, we still call EVA for additional context but adjust our scoring accordingly.
Fast, free, runs instantly. Catches obviously invalid inputs before making any network requests. Uses an RFC 5321-aware regex that handles edge cases most simple patterns miss.
Checks the domain against Disify's disposable email database and verifies DNS records. This layer catches temporary email services and domains that do not exist.
Performs deeper checks including MX record validation, SMTP verification hints, and free email provider detection. This layer adds deliverability confidence.
Combines all signals from the previous layers into a single 0-100 score with weighted categories. The score is accompanied by a plain-English explanation of every factor that contributed to it.
// Validation pipeline architecture
//
// Input: "user@example.com"
// |
// v
// [Layer 1: Syntax] ──→ valid? ──→ no ──→ Score: 0, stop
// | yes
// v
// [Layer 2: Disify] ──→ disposable? dns? format?
// |
// v (parallel)
// [Layer 3: EVA] ──→ deliverable? spam? free? mx?
// |
// v
// [Layer 4: Scorer] ──→ Combine all signals ──→ Score: 0-100
// |
// v
// [Renderer] ──→ Score gauge + detail breakdown + recommendations
Most email regexes you find online are either too permissive (allowing clearly invalid addresses) or too restrictive (rejecting valid addresses with unusual characters). Our validator aims for the practical middle ground: strict enough to catch garbage, permissive enough to accept legitimate addresses.
class SyntaxValidator {
// RFC 5321 practical subset
static EMAIL_REGEX =
/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
static validate(email) {
const issues = [];
const warnings = [];
// Basic checks
if (!email || typeof email !== 'string') {
return {
valid: false,
issues: ['Email is empty or not a string'],
warnings: []
};
}
const trimmed = email.trim().toLowerCase();
if (trimmed !== email) {
warnings.push('Email had leading/trailing whitespace');
}
if (trimmed.length > 254) {
issues.push(
'Email exceeds maximum length of 254 characters'
);
}
// Split into local and domain parts
const atIndex = trimmed.lastIndexOf('@');
if (atIndex === -1) {
return {
valid: false,
issues: ['No @ symbol found'],
warnings
};
}
const local = trimmed.substring(0, atIndex);
const domain = trimmed.substring(atIndex + 1);
// Local part validation
if (local.length === 0) {
issues.push('Local part (before @) is empty');
}
if (local.length > 64) {
issues.push('Local part exceeds 64 characters');
}
if (local.startsWith('.') || local.endsWith('.')) {
issues.push('Local part cannot start or end with a dot');
}
if (local.includes('..')) {
issues.push('Local part cannot have consecutive dots');
}
// Domain validation
if (domain.length === 0) {
issues.push('Domain part (after @) is empty');
}
if (!domain.includes('.')) {
issues.push('Domain must have at least one dot');
}
const tld = domain.split('.').pop();
if (tld && tld.length < 2) {
issues.push('Top-level domain must be at least 2 characters');
}
// Regex check
if (!this.EMAIL_REGEX.test(trimmed)) {
issues.push('Email contains invalid characters');
}
// Warnings for suspicious but valid patterns
if (local.includes('+')) {
warnings.push(
'Email uses plus addressing (e.g., user+tag@domain)'
);
}
if (domain.split('.').length > 4) {
warnings.push('Domain has unusually many subdomains');
}
return {
valid: issues.length === 0,
email: trimmed,
local,
domain,
tld,
issues,
warnings
};
}
}
class DisifyClient {
constructor() {
this.baseURL = 'https://disify.com/api/email';
}
async validate(email) {
try {
const response = await fetch(
`${this.baseURL}/${encodeURIComponent(email)}`
);
if (!response.ok) {
throw new Error(`Disify API error: ${response.status}`);
}
const data = await response.json();
return {
success: true,
formatValid: data.format === true,
disposable: data.disposable === true,
dnsValid: data.dns === true,
domain: data.domain || null
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
}
const disify = new DisifyClient();
class EVAClient {
constructor() {
this.baseURL = 'https://api.eva.pingutil.com/email';
}
async validate(email) {
try {
const response = await fetch(
`${this.baseURL}?email=${encodeURIComponent(email)}`
);
if (!response.ok) {
throw new Error(`EVA API error: ${response.status}`);
}
const result = await response.json();
const data = result.data || {};
return {
success: true,
validSyntax: data.valid_syntax === true,
disposable: data.disposable === true,
deliverable: data.deliverable || null,
spam: data.spam === true,
freeEmail: data.free_email === true,
domain: data.domain || null,
mxRecords: data.mx_records === true
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
}
const eva = new EVAClient();
The scoring algorithm is the intellectual core of Email Bouncer. It assigns weights to different validation signals and produces a composite score. The weights reflect the relative importance of each signal for predicting whether an email is worth sending to.
class QualityScorer {
static WEIGHTS = {
syntax: 15, // Basic validity
dnsValid: 20, // Domain exists
notDisposable: 25, // Not a throwaway address
deliverable: 20, // Server accepts mail
notSpam: 10, // Not flagged as spam
domainReputation: 10 // Domain quality signals
};
static score(syntaxResult, disifyResult, evaResult) {
let totalScore = 0;
let maxPossible = 0;
const factors = [];
// Factor 1: Syntax validity (15 points)
maxPossible += this.WEIGHTS.syntax;
if (syntaxResult.valid) {
totalScore += this.WEIGHTS.syntax;
factors.push({
name: 'Syntax',
score: this.WEIGHTS.syntax,
max: this.WEIGHTS.syntax,
status: 'pass',
detail: 'Email format is valid'
});
} else {
factors.push({
name: 'Syntax',
score: 0,
max: this.WEIGHTS.syntax,
status: 'fail',
detail: syntaxResult.issues.join('; ')
});
// If syntax fails, everything else is moot
return this.buildResult(
0, 100, factors,
syntaxResult, disifyResult, evaResult
);
}
// Factor 2: DNS validity (20 points)
maxPossible += this.WEIGHTS.dnsValid;
if (disifyResult.success && disifyResult.dnsValid) {
totalScore += this.WEIGHTS.dnsValid;
factors.push({
name: 'DNS Records',
score: this.WEIGHTS.dnsValid,
max: this.WEIGHTS.dnsValid,
status: 'pass',
detail: 'Domain has valid DNS records'
});
} else if (disifyResult.success && !disifyResult.dnsValid) {
factors.push({
name: 'DNS Records',
score: 0,
max: this.WEIGHTS.dnsValid,
status: 'fail',
detail: 'Domain has no valid DNS records'
});
} else {
// API failed, give partial credit
totalScore += this.WEIGHTS.dnsValid * 0.5;
factors.push({
name: 'DNS Records',
score: this.WEIGHTS.dnsValid * 0.5,
max: this.WEIGHTS.dnsValid,
status: 'unknown',
detail: 'Could not verify DNS (API unavailable)'
});
}
// Factor 3: Not disposable (25 points)
maxPossible += this.WEIGHTS.notDisposable;
const disifyDisposable = disifyResult.success
? disifyResult.disposable : null;
const evaDisposable = evaResult.success
? evaResult.disposable : null;
// Both agree it's not disposable: full points
// Either says disposable: zero points
// One unavailable, other says not: most points
if (disifyDisposable === false
&& evaDisposable === false) {
totalScore += this.WEIGHTS.notDisposable;
factors.push({
name: 'Not Disposable',
score: this.WEIGHTS.notDisposable,
max: this.WEIGHTS.notDisposable,
status: 'pass',
detail: 'Both APIs confirm this is not a disposable email'
});
} else if (disifyDisposable === true
|| evaDisposable === true) {
factors.push({
name: 'Not Disposable',
score: 0,
max: this.WEIGHTS.notDisposable,
status: 'fail',
detail: 'Detected as a disposable/temporary email address'
});
} else if (disifyDisposable === false
|| evaDisposable === false) {
totalScore += this.WEIGHTS.notDisposable * 0.8;
factors.push({
name: 'Not Disposable',
score: this.WEIGHTS.notDisposable * 0.8,
max: this.WEIGHTS.notDisposable,
status: 'pass',
detail: 'One API confirms not disposable (other unavailable)'
});
} else {
totalScore += this.WEIGHTS.notDisposable * 0.5;
factors.push({
name: 'Not Disposable',
score: this.WEIGHTS.notDisposable * 0.5,
max: this.WEIGHTS.notDisposable,
status: 'unknown',
detail: 'Could not verify disposable status'
});
}
// Factor 4: Deliverability (20 points)
maxPossible += this.WEIGHTS.deliverable;
if (evaResult.success) {
if (evaResult.deliverable === true) {
totalScore += this.WEIGHTS.deliverable;
factors.push({
name: 'Deliverable',
score: this.WEIGHTS.deliverable,
max: this.WEIGHTS.deliverable,
status: 'pass',
detail: 'Email appears to be deliverable'
});
} else if (evaResult.deliverable === false) {
factors.push({
name: 'Deliverable',
score: 0,
max: this.WEIGHTS.deliverable,
status: 'fail',
detail: 'Email does not appear to be deliverable'
});
} else {
// Unknown deliverability
totalScore += this.WEIGHTS.deliverable * 0.5;
factors.push({
name: 'Deliverable',
score: this.WEIGHTS.deliverable * 0.5,
max: this.WEIGHTS.deliverable,
status: 'unknown',
detail: 'Deliverability could not be determined'
});
}
} else {
totalScore += this.WEIGHTS.deliverable * 0.4;
factors.push({
name: 'Deliverable',
score: this.WEIGHTS.deliverable * 0.4,
max: this.WEIGHTS.deliverable,
status: 'unknown',
detail: 'EVA API unavailable for deliverability check'
});
}
// Factor 5: Not spam (10 points)
maxPossible += this.WEIGHTS.notSpam;
if (evaResult.success && evaResult.spam === false) {
totalScore += this.WEIGHTS.notSpam;
factors.push({
name: 'Not Spam',
score: this.WEIGHTS.notSpam,
max: this.WEIGHTS.notSpam,
status: 'pass',
detail: 'Not associated with spam activity'
});
} else if (evaResult.success && evaResult.spam === true) {
factors.push({
name: 'Not Spam',
score: 0,
max: this.WEIGHTS.notSpam,
status: 'fail',
detail: 'Email is associated with spam activity'
});
} else {
totalScore += this.WEIGHTS.notSpam * 0.5;
factors.push({
name: 'Not Spam',
score: this.WEIGHTS.notSpam * 0.5,
max: this.WEIGHTS.notSpam,
status: 'unknown',
detail: 'Spam status could not be verified'
});
}
// Factor 6: Domain reputation (10 points)
maxPossible += this.WEIGHTS.domainReputation;
const domainScore = this.assessDomainReputation(
syntaxResult, disifyResult, evaResult
);
totalScore += domainScore.score;
factors.push(domainScore.factor);
return this.buildResult(
totalScore, maxPossible, factors,
syntaxResult, disifyResult, evaResult
);
}
static assessDomainReputation(syntax, disify, eva) {
const domain = syntax.domain;
let score = 0;
const details = [];
// Well-known providers get full marks
const trustedProviders = [
'gmail.com', 'outlook.com', 'hotmail.com',
'yahoo.com', 'icloud.com', 'protonmail.com',
'proton.me', 'hey.com', 'fastmail.com'
];
if (trustedProviders.includes(domain)) {
return {
score: this.WEIGHTS.domainReputation,
factor: {
name: 'Domain Reputation',
score: this.WEIGHTS.domainReputation,
max: this.WEIGHTS.domainReputation,
status: 'pass',
detail: `${domain} is a well-known email provider`
}
};
}
// Free email: slight deduction
if (eva.success && eva.freeEmail) {
score += this.WEIGHTS.domainReputation * 0.7;
details.push('Free email provider');
} else if (eva.success && !eva.freeEmail) {
score += this.WEIGHTS.domainReputation;
details.push('Custom/business domain');
} else {
score += this.WEIGHTS.domainReputation * 0.5;
details.push('Domain type unknown');
}
return {
score,
factor: {
name: 'Domain Reputation',
score,
max: this.WEIGHTS.domainReputation,
status: score >= this.WEIGHTS.domainReputation * 0.7
? 'pass' : 'warn',
detail: details.join('; ')
}
};
}
static buildResult(score, max, factors,
syntax, disify, eva) {
const normalizedScore = Math.round(
(score / max) * 100
);
let grade, recommendation;
if (normalizedScore >= 80) {
grade = 'Excellent';
recommendation = 'This email appears safe to send to.';
} else if (normalizedScore >= 60) {
grade = 'Good';
recommendation = 'This email is likely valid but has minor concerns.';
} else if (normalizedScore >= 40) {
grade = 'Questionable';
recommendation = 'Proceed with caution. Consider double opt-in verification.';
} else if (normalizedScore >= 20) {
grade = 'Poor';
recommendation = 'High risk of bounce or spam trap. Not recommended.';
} else {
grade = 'Invalid';
recommendation = 'Do not send to this email address.';
}
return {
score: normalizedScore,
grade,
recommendation,
factors,
raw: { syntax, disify, eva }
};
}
}
class EmailBouncer {
constructor() {
this.disify = new DisifyClient();
this.eva = new EVAClient();
this.history = [];
}
async validate(email) {
const startTime = performance.now();
// Layer 1: Syntax (instant)
const syntaxResult = SyntaxValidator.validate(email);
if (!syntaxResult.valid) {
const result = QualityScorer.score(
syntaxResult,
{ success: false },
{ success: false }
);
result.duration = performance.now() - startTime;
this.history.unshift({ email, result, timestamp: Date.now() });
return result;
}
// Layers 2 & 3: API calls (parallel)
const [disifyResult, evaResult] = await Promise.all([
this.disify.validate(syntaxResult.email),
this.eva.validate(syntaxResult.email)
]);
// Layer 4: Scoring
const result = QualityScorer.score(
syntaxResult, disifyResult, evaResult
);
result.duration = Math.round(
performance.now() - startTime
);
// Store in history
this.history.unshift({
email: syntaxResult.email,
result,
timestamp: Date.now()
});
// Keep history manageable
if (this.history.length > 50) {
this.history = this.history.slice(0, 50);
}
return result;
}
async validateBatch(emails) {
const results = [];
// Process in batches of 3 to avoid rate limiting
for (let i = 0; i < emails.length; i += 3) {
const batch = emails.slice(i, i + 3);
const batchResults = await Promise.all(
batch.map(email => this.validate(email))
);
results.push(
...batch.map((email, j) => ({
email,
...batchResults[j]
}))
);
// Brief pause between batches
if (i + 3 < emails.length) {
await new Promise(r => setTimeout(r, 500));
}
}
return results;
}
}
const bouncer = new EmailBouncer();
function renderResult(result, email) {
const container = document.getElementById('results');
const scoreColor = result.score >= 80 ? '#4ade80'
: result.score >= 60 ? '#facc15'
: result.score >= 40 ? '#fb923c'
: '#ef4444';
container.innerHTML = `
<div class="result-card">
<div class="score-section">
<div class="score-ring"
style="--score: ${result.score};
--color: ${scoreColor}">
<span class="score-value">${result.score}</span>
</div>
<div class="score-meta">
<span class="grade"
style="color: ${scoreColor}">
${result.grade}
</span>
<span class="email-tested">${email}</span>
<span class="duration">
Checked in ${result.duration}ms
</span>
</div>
</div>
<div class="recommendation">
<p>${result.recommendation}</p>
</div>
<div class="factors-section">
<h3>Validation Breakdown</h3>
${result.factors.map(f => `
<div class="factor-row factor-${f.status}">
<div class="factor-header">
<span class="factor-icon">
${f.status === 'pass' ? '✓'
: f.status === 'fail' ? '✗'
: f.status === 'warn' ? '!'
: '?'}
</span>
<span class="factor-name">${f.name}</span>
<span class="factor-score">
${Math.round(f.score)}/${f.max}
</span>
</div>
<p class="factor-detail">${f.detail}</p>
</div>
`).join('')}
</div>
</div>
`;
}
If you attempt to write a regex that covers the complete RFC 5321 specification for email addresses, you will end up with a pattern that is hundreds of characters long and nearly impossible to maintain. The specification allows quoted local parts (so "john doe"@example.com is technically valid), IP address domains (user@[192.168.1.1]), and various special characters. Our practical approach validates the 99.9% of real-world email addresses while intentionally not supporting exotic edge cases that no legitimate user would actually use.
New disposable email services appear constantly. Services like Disify maintain updated databases, but there is always a gap between a new disposable service launching and it being added to detection lists. This is why we use two independent APIs: Disify and EVA each maintain their own databases, and the overlap provides better coverage than either alone. Even so, sophisticated disposable services that use custom domains can evade detection entirely.
Neither Disify nor EVA performs full SMTP verification (connecting to the mail server and attempting a RCPT TO command). This means they cannot tell you with certainty whether a specific mailbox exists. Some mail servers accept all addresses (catch-all configuration), while others reject all verification attempts regardless of whether the mailbox exists. True SMTP verification requires a server-side implementation and carries its own risks (being flagged as suspicious by mail servers).
Both APIs describe their rate limits as "reasonable use" without specific numbers. In testing, Disify handles about 60 requests per minute before returning errors, and EVA handles about 30. For a single-user tool, these limits are generous. For batch validation, you need throttling. Our batch implementation processes 3 emails at a time with 500ms pauses between batches, which stays well within both APIs' tolerances.
EVA flags Gmail, Yahoo, and similar providers as "free email." This is useful information, but it is not inherently negative. A Gmail address is perfectly valid for personal communication and even for small business use. The quality score gives a slight deduction for free providers (compared to custom business domains) but does not treat it as a failure. Context matters: for B2B lead generation, free email addresses are less valuable, but for B2C signups, they are perfectly fine.
Sending email addresses to third-party APIs for validation raises privacy concerns. Both Disify and EVA state that they do not store or log the emails submitted. However, for applications handling sensitive data (medical, financial, legal), consider running validation entirely client-side using syntax checks and a locally maintained disposable domain list. The open-source list at disposable-email-domains on GitHub provides a regularly updated list of known disposable domains.
The most common use case is enhancing user registration forms. Instead of accepting any string that looks like an email and discovering later that it bounces, validate in real time as the user types (debounced, of course). Show the quality score and, for low-scoring addresses, suggest the user check their input or use a different address. This dramatically reduces bounce rates and invalid registrations.
Marketing teams regularly need to clean their email lists before campaigns. The batch validation feature can process a CSV of email addresses, score each one, and generate a report showing the distribution of quality scores. Addresses scoring below 40 should be removed or flagged for re-verification. This preemptive cleaning protects sender reputation and improves deliverability for the entire list.
For sales teams, the quality score provides an additional signal for lead prioritization. An email scoring 95 from a custom business domain signals a more serious lead than one scoring 45 from a disposable provider. Integrate the validation into your CRM's lead intake pipeline to automatically tag and prioritize leads.
Disposable emails are commonly used in fraud attempts: creating multiple accounts for promotional abuse, generating fake reviews, or circumventing bans. The disposable detection layer can flag accounts created with temporary email addresses for additional scrutiny. Combined with other signals (IP reputation, device fingerprinting, behavioral analysis), email quality scoring becomes a valuable fraud prevention signal.
Use the quality score as a threshold for onboarding flow variations. High-quality emails (80+) go through a streamlined onboarding. Medium-quality emails (40-79) go through double opt-in verification. Low-quality emails (below 40) receive a gentle message suggesting they use a permanent email address for the best experience. This adaptive approach maximizes conversion while minimizing bad registrations.
If you validate as the user types, debounce the API calls. A 600ms debounce is appropriate for email validation — shorter debounces trigger too many requests during typing, and longer debounces feel unresponsive.
function debounce(fn, ms) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), ms);
};
}
const validateOnInput = debounce(async (email) => {
if (email.length < 5) return;
// Quick client-side check first
const syntax = SyntaxValidator.validate(email);
renderSyntaxFeedback(syntax);
// Only hit APIs if syntax is valid
if (syntax.valid) {
const result = await bouncer.validate(email);
renderResult(result, email);
}
}, 600);
If the user corrects a typo and re-submits the same email, do not call the APIs again. Cache results by email address with a short TTL (5 minutes). This is especially important for the batch validation use case where duplicate emails in a list would otherwise generate redundant API calls.
class ValidationCache {
constructor(ttl = 300000) {
this.cache = new Map();
this.ttl = ttl;
}
get(email) {
const entry = this.cache.get(email);
if (!entry) return null;
if (Date.now() - entry.timestamp > this.ttl) {
this.cache.delete(email);
return null;
}
return entry.result;
}
set(email, result) {
this.cache.set(email, {
result,
timestamp: Date.now()
});
// Prevent unbounded growth
if (this.cache.size > 1000) {
const oldest = this.cache.keys().next().value;
this.cache.delete(oldest);
}
}
}
const validationCache = new ValidationCache();
When the user is offline (detectable via navigator.onLine), fall back to syntax-only validation. Show the syntax score and a note that full validation requires an internet connection. This ensures the tool remains partially useful even without API access.
For batch validation, show results as they complete rather than waiting for the entire batch. Display a progress bar and update the statistics (average score, distribution chart) in real time. This keeps the user engaged during what could otherwise be a long wait for large lists.
Detect common typos in email domains: "gmial.com" to "gmail.com," "yaho.com" to "yahoo.com," "outlok.com" to "outlook.com." Use Levenshtein distance against a list of known providers to suggest corrections. This single feature can recover a significant percentage of invalid-looking emails that are simply mistyped.
Integrate with a Whois API to check when the email's domain was registered. Very recently created domains (less than 30 days old) are higher risk for fraud. Long-established domains are more trustworthy. This adds a temporal dimension to the quality score.
Add drag-and-drop CSV import and export. Parse the CSV, validate each email, and generate a downloadable report with quality scores, grades, and recommendations. Include summary statistics: percentage of emails by grade, most common failing factors, and an overall list health score.
Wrap the validation logic in a simple Express.js or Cloudflare Workers endpoint so other applications can call it as a service. This turns Email Bouncer from a standalone tool into a microservice that can be integrated into registration flows, CRM systems, or marketing automation platforms.
Track validation results over time and show trends. Is the percentage of disposable email signups increasing? Are there spikes in low-quality submissions that might indicate a bot attack? This kind of trend analysis turns a validation tool into a monitoring system.
Email Bouncer demonstrates a fundamental principle of robust validation: no single check is sufficient. Client-side regex catches syntax errors but cannot verify deliverability. Disify catches disposable providers but cannot assess spam reputation. EVA provides deliverability signals but may miss newly created disposable services. Only by combining all three layers do we get a validation pipeline that catches the vast majority of problematic email addresses.
The quality scoring approach is particularly transferable. Rather than making binary valid/invalid decisions, we produce a nuanced score that lets downstream systems make contextual decisions. A registration form might accept anything above 40 and require double opt-in for anything between 40 and 70. A marketing platform might only send to addresses scoring above 60. A fraud detection system might flag anything below 30 for manual review. The same validation pipeline supports all these use cases because it exposes a score rather than a verdict.
Email validation is a problem that will exist as long as email exists. The specific APIs may change, the disposable provider landscape will evolve, and new validation signals will emerge. But the multi-layer pipeline architecture, the quality scoring methodology, and the principle of combining multiple independent signals for robust assessment will remain relevant for years to come. Build the pipeline, calibrate the weights, and let the score guide your decisions.