APIMashupHub

Multi-Layer Email Validation: Building Email Bouncer

Introduction: Why Email Validation Is Harder Than You Think

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.

What you will build: A multi-layer email validation tool that checks syntax with RFC-aware regex, detects disposable/temporary email providers via Disify, validates domain MX records and mailbox existence via EVA, and combines all signals into a 0-100 quality score with detailed explanations.

The APIs: Disify and EVA

Disify: Disposable Email Detection

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.

Disify API Overview

PropertyDetail
Base URLhttps://disify.com/api/email/{email}
AuthNone required
Rate LimitReasonable use (undocumented)
FormatJSON
CORSEnabled

The response includes:

EVA: Email Verification API

EVA 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.

EVA API Overview

PropertyDetail
Base URLhttps://api.eva.pingutil.com/email?email={email}
AuthNone required
Rate LimitReasonable use
FormatJSON
CORSEnabled

EVA's response includes:

By 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.

Architecture: The Validation Pipeline

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.

Layer 1: Client-Side Syntax Validation

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.

Layer 2: Domain Intelligence (Disify)

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.

Layer 3: Deliverability Assessment (EVA)

Performs deeper checks including MX record validation, SMTP verification hints, and free email provider detection. This layer adds deliverability confidence.

Layer 4: Quality Scoring

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

Code Walkthrough: Building the Validation Engine

Step 1: RFC-Aware Syntax Validation

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
    };
  }
}

Step 2: Disify Integration

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();

Step 3: EVA Integration

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();

Step 4: The Quality Scoring Algorithm

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 }
    };
  }
}

Step 5: The Validation Orchestrator

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();

Step 6: The Results Renderer

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>
  `;
}

Challenges and Gotchas

1. The RFC 5321 Rabbit Hole

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.

2. Disposable Email Whack-a-Mole

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.

Important: Never reject an email solely because it is flagged as disposable by a single API. False positives happen. Some legitimate companies use email addresses that trigger disposable detection. Use the quality score as a signal for additional verification (like double opt-in), not as a hard block.

3. SMTP Verification Limitations

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).

4. Rate Limiting Without Documentation

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.

5. Free Email Provider Ambiguity

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.

6. Privacy Considerations

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.

Real-World Use Cases

Registration Form Enhancement

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.

Email List Cleaning

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.

Lead Quality Assessment

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.

Fraud Detection Layer

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.

User Onboarding Quality Gate

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.

Performance Tips

Debounce Input Validation

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);

Cache Recent Results

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();

Syntax-Only Mode for Offline

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.

Progressive Enhancement for Batch Mode

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.

Extension Ideas

1. Typo Correction Suggestions

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.

2. Domain Age and Whois Integration

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.

3. Bulk CSV Import/Export

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.

4. API Endpoint for Integration

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.

5. Historical Trend Analysis

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.

Conclusion

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.