@emailcheck/email-validator-js
Version:
Advanced email validation with MX records, SMTP verification, disposable email detection, batch processing, and caching. Production-ready with TypeScript support.
694 lines (693 loc) • 26.5 kB
TypeScript
import type { Cache } from './cache-interface';
/** Error codes set on `VerificationResult.metadata.error` when `verifyEmail` cannot deliver a definitive verdict. */
export declare enum VerificationErrorCode {
invalidFormat = "INVALID_FORMAT",
invalidDomain = "INVALID_DOMAIN",
noMxRecords = "NO_MX_RECORDS",
smtpConnectionFailed = "SMTP_CONNECTION_FAILED",
smtpTimeout = "SMTP_TIMEOUT",
mailboxNotFound = "MAILBOX_NOT_FOUND",
networkError = "NETWORK_ERROR",
disposableEmail = "DISPOSABLE_EMAIL"
}
/** Discriminator for `VerificationStep.kind`. */
export type VerificationStepKind = 'syntax' | 'domain-validation' | 'name-detection' | 'domain-suggestion' | 'disposable' | 'free' | 'mx-lookup' | 'smtp-probe' | 'whois-age' | 'whois-registration';
/**
* One unit of work in the verification pipeline. Produced when
* `VerifyEmailParams.captureTranscript === true`. The `details` shape varies
* per step — see the inline interfaces under each kind.
*/
export interface VerificationStep {
kind: VerificationStepKind;
startedAt: number;
durationMs: number;
/** Whether the step completed without throwing. Step-level result lives in `details`. */
ok: boolean;
details: Record<string, unknown>;
}
export interface VerificationResult {
email: string;
validFormat: boolean;
validMx: boolean | null;
validSmtp: boolean | null;
isDisposable: boolean;
isFree: boolean;
detectedName?: DetectedName | null;
domainAge?: DomainAgeInfo | null;
domainRegistration?: DomainRegistrationInfo | null;
domainSuggestion?: DomainSuggestion | null;
/** MX records found for the domain (if MX verification was performed) */
mxRecords?: string[] | null;
/** Whether SMTP connection was successful */
canConnectSmtp?: boolean;
/** Whether the mailbox is full */
hasFullInbox?: boolean;
/** Whether the domain has catch-all enabled */
isCatchAll?: boolean;
/** Whether the email is deliverable */
isDeliverable?: boolean;
/** Whether the email/account is disabled */
isDisabled?: boolean;
/** Always populated by `verifyEmail` — read directly without optional chaining. */
metadata: {
verificationTime: number;
cached: boolean;
error?: VerificationErrorCode;
};
/**
* Per-step trace of what `verifyEmail` did. Present only when
* `VerifyEmailParams.captureTranscript === true`. Each entry records timing
* and step-specific details (raw WHOIS data, SMTP transcript, MX records,
* cache hit/miss, etc.) for debugging and diagnostics.
*/
transcript?: VerificationStep[];
}
/**
* Parameters for `verifyEmail` — the high-level orchestrator that runs the
* full pipeline (syntax → typo / name / domain → disposable / free → WHOIS
* → MX → SMTP).
*/
export interface VerifyEmailParams {
/** The full email address to verify (`local@domain`). */
emailAddress: string;
/** Resolve MX records via DNS to confirm the domain accepts mail. Default: `true`. */
verifyMx?: boolean;
/** Open an SMTP connection to the highest-priority MX and probe `RCPT TO`. Default: `false`. */
verifySmtp?: boolean;
/** Check the address against the bundled disposable-provider list. Default: `true`. */
checkDisposable?: boolean;
/** Check the address against the bundled free-provider list. Default: `true`. */
checkFree?: boolean;
/** Extract first/last name from the local-part. Default: `false` (cheap, but not free). */
detectName?: boolean;
/** Suggest a corrected domain on typos (e.g. `gnail.com` → `gmail.com`). Default: `true`. */
suggestDomain?: boolean;
/** Look up the domain creation date via WHOIS. Slow + external dep. Default: `false`. */
checkDomainAge?: boolean;
/** Look up domain registration status (registered / expired / locked). Default: `false`. */
checkDomainRegistration?: boolean;
/**
* When the address is identified as disposable, skip the (expensive) MX +
* SMTP probe and accept the disposable verdict as the final answer.
* Default: `false`.
*/
skipMxForDisposable?: boolean;
/**
* When the address is identified as disposable, skip the WHOIS checks too.
* Default: `false`.
*/
skipDomainWhoisForDisposable?: boolean;
/** Override the default name-detection heuristic. Receives the email; returns `DetectedName | null`. */
nameDetectionMethod?: NameDetectionMethod;
/** Override the default domain-suggestion heuristic. Receives the domain; returns `DomainSuggestion | null`. */
domainSuggestionMethod?: DomainSuggestionMethod;
/** Custom domain list for the typo suggester. Defaults to the bundled common-domain set. */
commonDomains?: string[];
/**
* Per-attempt timeout for the SMTP probe, in milliseconds. Bounds both the
* TCP/TLS connection setup AND the inactivity gap between SMTP commands
* within an attempt. Default: `4000` ms.
*
* To bound the total wall-clock across all MX × port attempts, use
* `smtpTotalDeadlineMs` instead. To control retries on connection-class
* failures, use `smtpRetry`.
*/
smtpPerAttemptTimeoutMs?: number;
/**
* Hard cap on total wall-clock time for the SMTP probe across all MX × port
* × retry attempts. Reasonable when calling from a request handler with a
* tight latency budget. Default: unbounded.
*/
smtpTotalDeadlineMs?: number;
/**
* Stop the SMTP probe after this many connection-class failures in a row
* (counting `connection_error` / `connection_timeout` / `connection_closed`
* across MX × port attempts). Resets on any non-connection-class outcome.
* Default: unbounded.
*/
smtpMaxConsecutiveFailures?: number;
/**
* Hard cap on how many MX hostnames the SMTP probe will try, regardless of
* how many DNS returned. Default: unbounded — try them all.
*/
smtpMaxMxHosts?: number;
/** Optional retry policy for connection-class failures on a single MX × port. Default: no retries. */
smtpRetry?: RetryPolicy;
/**
* Force a specific port for the SMTP probe (e.g. `587`). When set, this
* port is the only one tried — overrides the default `[25, 587, 465]` walk
* and any per-MX hint cached from a previous probe.
*/
smtpPort?: number;
/** Per-WHOIS-query timeout in milliseconds. Default: `5000`. */
whoisTimeoutMs?: number;
/** Optional shared cache for MX, WHOIS, disposable / free, SMTP, and domain results. */
cache?: Cache;
/** When true, the pipeline writes a per-line trace to `console.debug`. Default: `false`. */
debug?: boolean;
/**
* When true, populates `result.transcript` with a per-step structured trace
* covering every subsystem (syntax / disposable / free / MX / SMTP / WHOIS /
* name detection / domain suggestion). Each step records timing +
* step-specific details (raw WHOIS data, SMTP transcript, MX records, cache
* hit/miss, etc.). Safe to leave off for production; turn on for diagnostics
* or debug UIs. Default: `false`.
*/
captureTranscript?: boolean;
}
/**
* Parameters for `verifyEmailBatch` — fan-out wrapper around `verifyEmail`
* that runs many addresses through the same pipeline with a concurrency cap.
*/
export interface BatchVerifyParams {
/** Email addresses to verify, in order. */
emailAddresses: string[];
/** Maximum number of in-flight `verifyEmail` calls. Default: `5`. */
concurrency?: number;
/** Per-attempt SMTP timeout in milliseconds (forwarded to each `verifyEmail` call). Default: `4000`. */
smtpPerAttemptTimeoutMs?: number;
/** Hard cap on total wall-clock for each individual SMTP probe. Forwarded to `verifyEmail`. */
smtpTotalDeadlineMs?: number;
/** Stop each individual SMTP probe after this many connection-class failures in a row. */
smtpMaxConsecutiveFailures?: number;
/** Hard cap on MX hostnames per individual SMTP probe. */
smtpMaxMxHosts?: number;
/** Optional retry policy per MX×port for each individual SMTP probe. */
smtpRetry?: RetryPolicy;
/** Resolve MX records per address. Default: `true`. */
verifyMx?: boolean;
/** Run the SMTP probe per address. Default: `false`. */
verifySmtp?: boolean;
/** Check disposable list per address. Default: `true`. */
checkDisposable?: boolean;
/** Check free-provider list per address. Default: `true`. */
checkFree?: boolean;
/** Extract first/last name from each local-part. Default: `false`. */
detectName?: boolean;
/** Override the name-detection heuristic. */
nameDetectionMethod?: NameDetectionMethod;
/** Suggest a corrected domain on typos. Default: `false` for batches (it's per-call cost). */
suggestDomain?: boolean;
/** Override the domain-suggestion heuristic. */
domainSuggestionMethod?: DomainSuggestionMethod;
/** Custom canonical-domain list for the typo suggester. */
commonDomains?: string[];
/** Skip MX/SMTP probe for disposable addresses. Default: `false`. */
skipMxForDisposable?: boolean;
/** Skip WHOIS lookups for disposable addresses. Default: `false`. */
skipDomainWhoisForDisposable?: boolean;
/** Optional shared cache (re-used across all addresses in the batch). */
cache?: Cache;
}
/**
* Rich cache result types for storing detailed verification results
*/
/**
* Result for disposable email detection with metadata
*/
export interface DisposableEmailResult {
/** Whether the email/domain is disposable */
isDisposable: boolean;
/** Source that identified this as disposable (e.g., list name, service) */
source?: string;
/** Category of disposable email (e.g., 'temp', 'alias', 'forwarding') */
category?: string;
/** Timestamp when this was checked */
checkedAt: number;
}
/**
* Result for free email provider detection with metadata
*/
export interface FreeEmailResult {
/** Whether the email/domain is from a free provider */
isFree: boolean;
/** Name of the free provider (e.g., 'gmail', 'yahoo', 'outlook') */
provider?: string;
/** Timestamp when this was checked */
checkedAt: number;
}
/**
* Result for domain validation with metadata
*/
export interface DomainValidResult {
/** Whether the domain is valid */
isValid: boolean;
/** Whether MX records were found */
hasMX: boolean;
/** The MX records that were found */
mxRecords?: string[];
/** Timestamp when this was checked */
checkedAt: number;
}
/**
* Email providers enum
*/
export declare enum EmailProvider {
gmail = "gmail",
hotmailB2b = "hotmail_b2b",
hotmailB2c = "hotmail_b2c",
proofpoint = "proofpoint",
mimecast = "mimecast",
yahoo = "yahoo",
everythingElse = "everything_else"
}
/**
* Verdict from one `verifyMailboxSMTP` call. Flat by design — every field is
* one boolean / scalar so callers can switch on a few keys instead of walking
* a tree.
*/
export interface SmtpVerificationResult {
/** True when at least one MX×port responded with an SMTP greeting. */
canConnectSmtp: boolean;
/** True when the MX returned `552` / `452` (over-quota / mailbox full). */
hasFullInbox: boolean;
/**
* True when both the real RCPT TO and the random-local probe RCPT TO
* returned `250` — the MX accepts every recipient and the deliverability
* signal for the real address is unreliable.
*/
isCatchAll: boolean;
/** True when the real RCPT TO returned `250` / `251`. */
isDeliverable: boolean;
/** True when the real RCPT TO was definitively rejected. */
isDisabled: boolean;
/**
* Short reason key when `isDeliverable === false`. Vocabulary:
* `not_found` | `over_quota` | `temporary_failure` | `ambiguous` |
* `connection_error` | `connection_timeout` | `connection_closed` |
* `tls_upgrade_failed` | `tls_handshake_failed` |
* `ehlo_failed` | `helo_failed` | `mail_from_rejected` |
* `no_greeting` | `no_mx_records` | `unrecognized_response` |
* `step_timeout` | `socket_timeout`
*
* Pass to `refineReasonByEnhancedStatus(error, enhancedStatus)` for a
* richer (mailbox-status-aware) reason when the MX returned a DSN.
*/
error?: string;
/** Most recent 3-digit SMTP code observed during the probe. */
responseCode?: number;
/**
* RFC 3463 enhanced status code from the most recent SMTP reply that
* carried one — e.g. `"5.1.1"` (mailbox unknown), `"5.7.1"` (policy
* block), `"4.7.0"` (transient policy). Undefined when no MX reply
* included an enhanced status.
*/
enhancedStatus?: string;
/** Operational counters — always populated. See `SmtpProbeMetrics`. */
metrics?: SmtpProbeMetrics;
/** Wall-clock timestamp this verdict was produced (set on every result). */
checkedAt?: number;
/**
* Server reply lines, in arrival order, prefixed `<host>:<port>|s| <line>`
* so multi-MX dialogues stay readable. Present only when
* `captureTranscript: true` was passed.
*/
transcript?: string[];
/**
* Client commands sent, in send order, prefixed `<host>:<port>|c| <cmd>`.
* Present only when `captureTranscript: true` was passed.
*/
commands?: string[];
}
/**
* Operational counters for one `verifyMailboxSMTP` call. The cost of
* collecting these is trivial — pure bookkeeping during the existing flow.
*/
export interface SmtpProbeMetrics {
/** How many MX hostnames the outer loop attempted before stopping. */
mxAttempts: number;
/** Total connection attempts across the whole call (sum across MX×port). */
portAttempts: number;
/** MX hostnames attempted in priority order (matches `mxRecords` slice). */
mxHostsTried: string[];
/**
* MX hostname that produced the final answer. Undefined when every MX
* failed (in which case `result.isDeliverable === false` and the SMTP
* reason is whatever the last attempted MX returned).
*/
mxHostUsed?: string;
/** Total wall-clock time the probe spent, in milliseconds. */
totalDurationMs: number;
}
/**
* Result for batch verification
*/
export interface BatchVerificationResult {
results: Map<string, VerificationResult>;
summary: {
total: number;
valid: number;
invalid: number;
errors: number;
processingTime: number;
};
}
/** TLS configuration options for the SMTP probe. */
export interface SMTPTLSConfig {
rejectUnauthorized?: boolean;
minVersion?: 'TLSv1.2' | 'TLSv1.3';
}
/**
* SMTP protocol steps walked by the verifier in order. `startTls` is a
* conditional step — it sends the STARTTLS command and upgrades the socket
* to TLS when the MX advertised support (controlled by
* `SMTPVerifyOptions.startTls`). On implicit-TLS ports (465) it's a no-op.
*/
export declare enum SMTPStep {
greeting = "GREETING",
ehlo = "EHLO",
helo = "HELO",
startTls = "STARTTLS",
mailFrom = "MAIL_FROM",
rcptTo = "RCPT_TO"
}
/** Custom SMTP step sequence for advanced callers. */
export interface SMTPSequence {
steps: SMTPStep[];
/** Override MAIL FROM payload — supply with angle brackets or `<>` for null sender. */
from?: string;
}
/**
* Optional retry policy for connection-class failures (timeout / connection
* error / connection closed) on a single MX × port. Definitive answers
* (250 / 550 / 552 / etc.) are never retried — they're stable verdicts.
*/
export interface RetryPolicy {
/**
* How many extra attempts to make on the same MX × port after a
* connection-class failure. `0` means no retry (the default).
*/
attempts: number;
/**
* Delay between retries, in milliseconds. With `backoff: 'exponential'`
* (the default), the actual delay is `delayMs * 2^(attemptIndex - 1)`.
* Default: `200` ms.
*/
delayMs?: number;
/**
* Backoff strategy between retries.
* - `'exponential'` (default): `delayMs * 2^(attemptIndex - 1)`.
* - `'fixed'`: every retry waits `delayMs` exactly.
*/
backoff?: 'exponential' | 'fixed';
}
/**
* Options for `verifyMailboxSMTP` and `verifyEmail`'s SMTP probe.
*
* Time-budget defaults are tuned for a 4-MX × 3-port worst case. If you
* call this from a request handler with a tight latency budget, set
* `totalDeadlineMs` so the probe gives up before your handler does.
*/
export interface SMTPVerifyOptions {
/**
* Ports to walk per MX, in priority order. The probe stops on the first
* port that yields a definitive answer; indeterminate outcomes (timeout /
* connection error / etc.) fall through to the next port.
*
* Default: `[25, 587, 465]` — plain → STARTTLS-able → implicit-TLS.
*/
ports?: number[];
/**
* Per-attempt timeout in milliseconds. Bounds both the TCP/TLS connection
* setup AND the inactivity gap between SMTP commands within an attempt.
* Each MX × port pair gets its own budget — to bound the total wall-clock,
* use `totalDeadlineMs` instead.
*
* Default: `3000` ms.
*/
perAttemptTimeoutMs?: number;
/**
* TLS configuration applied to implicit-TLS ports (465) and to STARTTLS
* upgrades on plaintext ports.
*
* - `true` (default): use sensible TLS defaults (`rejectUnauthorized: false`,
* `minVersion: 'TLSv1.2'`) — picks up CA quirks of long-tail MXes.
* - `false`: disable TLS entirely (port 465 will fail to handshake; STARTTLS
* step is skipped).
* - `SMTPTLSConfig` object: override individual fields. Merged onto the defaults.
*/
tlsConfig?: boolean | SMTPTLSConfig;
/**
* Hostname this client identifies itself as in the `EHLO` / `HELO` argument.
* Should be a real FQDN — `localhost` from a public IP is a textbook spam-bot
* signature and gets rejected by careful MXes.
*
* Default: `'localhost'`. Override with your delivery domain in production.
*/
heloHostname?: string;
/**
* Optional cache instance. When provided, the probe reuses prior verdicts
* keyed on `<primary mx>:<local>@<domain>` and remembers the last
* successful port per primary MX (so a re-probe skips the failed-port
* walk).
*
* Pass `null` or omit to skip caching entirely.
*/
cache?: Cache | null;
/**
* When true, a per-line `[SMTP] …` trace is written to `console.log`.
* Useful for diagnosing real-MX behavior; off by default for production.
*/
debug?: boolean;
/**
* When true, the returned `SmtpVerificationResult` carries `transcript`
* and `commands` arrays prefixed with `<host>:<port>|s| …` /
* `<host>:<port>|c| …`. Aggregated across every MX × port attempted.
*
* Default: `false`. The strings are O(N × wire-bytes); skip when you
* don't need the trace.
*/
captureTranscript?: boolean;
/**
* Hard cap on total wall-clock time for the entire probe (across all
* MX × port × retry attempts). When the deadline passes, the in-flight
* attempt is allowed to finish (it has its own per-attempt budget) and
* no new attempts are started.
*
* Use this to bound latency from a request-handler caller. A reasonable
* value matches your handler's deadline minus headroom for everything
* else it does.
*
* Default: unbounded — only `perAttemptTimeoutMs × ports.length × mxRecords.length`
* limits the worst case (e.g. `3000 × 3 × 4 = 36s`).
*/
totalDeadlineMs?: number;
/**
* Stop probing after this many connection-class failures in a row.
* Counts consecutive `connection_error` / `connection_timeout` /
* `connection_closed` outcomes across MX × port attempts; resets on any
* non-connection-class outcome. Useful for cutting off probes when the
* network path to the MX is wholly unreachable instead of waiting for
* every port × MX combination to time out.
*
* Default: unbounded.
*/
maxConsecutiveFailures?: number;
/**
* Hard cap on how many MX hostnames to try, regardless of how many were
* supplied in `mxRecords`. The probe walks them in priority order
* (`mxRecords[0]` first) and stops after this many.
*
* Default: unbounded.
*/
maxMxHosts?: number;
/**
* Optional retry policy for connection-class failures on a single MX × port.
* Definitive answers (250 / 550 / 552 / 421 / etc.) are never retried —
* they're stable verdicts.
*
* Default: no retries.
*/
retry?: RetryPolicy;
/**
* Override the per-attempt SMTP step list. Defaults to
* `[greeting, ehlo, startTls, mailFrom, rcptTo]` — covering the entire
* RFC 5321 envelope plus optional TLS upgrade. Most callers never need
* to override this; useful for advanced testing scenarios (e.g. probe
* RFC compliance with `[greeting, helo, mailFrom, rcptTo]`).
*/
sequence?: SMTPSequence;
/**
* Override the random local-part generator used by the catch-all dual
* probe. Useful for deterministic tests; receives the real local-part
* and domain so callers can derive a probe-local that matches the MX's
* syntax rules.
*
* Default: `<16 hex chars>-noexist` — long enough to never collide,
* structured so it's clearly synthetic, and passes common syntax filters.
*/
catchAllProbeLocal?: (realLocal: string, domain: string) => string;
/**
* SMTP PIPELINING (RFC 2920) — batch the envelope phase
* (RCPT TO real + RCPT TO probe + RSET) into one `socket.write()` when
* the MX advertises support.
*
* - `'auto'` (default): pipeline when EHLO multi-line includes `PIPELINING`,
* sequential otherwise.
* - `'never'`: always sequential — useful for deterministic wire-level
* assertions in tests or when investigating a pipeline-buggy MX.
* - `'force'`: pipeline without checking — testing escape hatch.
*/
pipelining?: 'auto' | 'never' | 'force';
/**
* STARTTLS upgrade on plaintext ports (25, 587). Implicit-TLS ports (465)
* ignore this option — they're already TLS from the start.
*
* - `'auto'` (default): upgrade if the MX advertises STARTTLS in EHLO.
* Submission-port (587) MXes typically require this — without it,
* `MAIL FROM` is rejected with `530 Must issue STARTTLS first`.
* - `'never'`: never upgrade — send `MAIL FROM` / `RCPT TO` in plaintext.
* - `'force'`: send `STARTTLS` unconditionally. Fails with
* `tls_upgrade_failed` when the MX doesn't support it. Testing only.
*/
startTls?: 'auto' | 'never' | 'force';
}
export interface VerifyMailboxSMTPParams {
local: string;
domain: string;
mxRecords: string[];
options?: SMTPVerifyOptions;
}
/**
* Domain suggestion for typo correction
*/
export interface DomainSuggestion {
original: string;
suggested: string;
confidence: number;
}
/**
* Custom domain suggestion function type
*/
export type DomainSuggestionMethod = (domain: string) => DomainSuggestion | null;
/**
* Parameters for domain suggestion
*/
export interface DomainSuggestionParams {
domain: string;
customMethod?: DomainSuggestionMethod;
commonDomains?: string[];
cache?: Cache;
}
/**
* Result of name detection from email
*/
export interface DetectedName {
firstName?: string;
lastName?: string;
confidence: number;
}
/**
* Custom name detection function type
*/
export type NameDetectionMethod = (email: string) => DetectedName | null;
/**
* Parameters for name detection
*/
export interface NameDetectionParams {
email: string;
customMethod?: NameDetectionMethod;
}
/**
* Domain age information
*/
export interface DomainAgeInfo {
domain: string;
creationDate: Date;
ageInDays: number;
ageInYears: number;
expirationDate: Date | null;
updatedDate: Date | null;
}
/**
* Domain registration status information
*/
export interface DomainRegistrationInfo {
domain: string;
isRegistered: boolean;
isAvailable: boolean;
status: string[];
registrar: string | null;
nameServers: string[];
expirationDate: Date | null;
isExpired: boolean;
daysUntilExpiration: number | null;
isPendingDelete?: boolean;
isLocked?: boolean;
}
/**
* Options for domain suggester
*/
export interface DomainSuggesterOptions {
threshold?: number;
customDomains?: string[];
}
/**
* Parameters for isDisposableEmail function
*/
export interface DisposableEmailCheckParams {
emailOrDomain: string;
cache?: Cache | null;
logger?: (...args: unknown[]) => void;
}
/**
* Parameters for isFreeEmail function
*/
export interface FreeEmailCheckParams {
emailOrDomain: string;
cache?: Cache | null;
logger?: (...args: unknown[]) => void;
}
/**
* Parameters for resolveMxRecords function
*/
export interface ResolveMxParams {
domain: string;
cache?: Cache | null;
logger?: (...args: unknown[]) => void;
}
/**
* Options for email validation (serverless compatible)
*/
export interface ValidateEmailOptions {
validateSyntax?: boolean;
validateTypo?: boolean;
validateDisposable?: boolean;
validateFree?: boolean;
validateMx?: boolean;
validateSMTP?: boolean;
skipCache?: boolean;
batchSize?: number;
domainSuggesterOptions?: DomainSuggesterOptions;
}
/**
* Result of email validation (serverless compatible)
*/
export interface EmailValidationResult {
valid: boolean;
email: string;
local?: string;
domain?: string;
validators: {
syntax?: ValidatorResult;
typo?: ValidatorResult & {
suggestion?: string;
};
disposable?: ValidatorResult;
free?: ValidatorResult;
mx?: ValidatorResult & {
records?: string[];
error?: string;
};
smtp?: ValidatorResult & {
error?: string;
};
};
}
/**
* Individual validator result
*/
export interface ValidatorResult {
valid: boolean;
}
export type { Cache, CacheStore } from './cache-interface';