@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.
1,699 lines (1,690 loc) • 96.3 kB
JavaScript
import { isValid, parse } from 'psl';
import { lru } from 'tiny-lru';
import { stringSimilarity } from 'string-similarity-js';
import { promises } from 'node:dns';
import * as net from 'node:net';
import * as tls from 'node:tls';
class LRUAdapter {
constructor(maxSize = 1e3, ttlMs = 36e5) {
this.lru = lru(maxSize, ttlMs);
}
get(key) {
const value = this.lru.get(key);
return value === void 0 ? null : value;
}
async set(key, value, ttlMs) {
if (ttlMs !== void 0) {
this.lru.set(key, value);
} else {
this.lru.set(key, value);
}
}
async delete(key) {
this.lru.delete(key);
return true;
}
async has(key) {
return this.lru.has(key);
}
async clear() {
this.lru.clear();
}
size() {
return this.lru.size;
}
/**
* Get the underlying LRU instance for advanced operations
*/
getLRU() {
return this.lru;
}
}
const DEFAULT_CACHE_OPTIONS = {
ttl: {
mx: 36e5,
disposable: 864e5,
free: 864e5,
domainValid: 864e5,
smtp: 18e5,
smtpPort: 864e5,
domainSuggestion: 864e5,
whois: 36e5
},
maxSize: {
mx: 1e4,
disposable: 1e4,
free: 1e4,
domainValid: 1e4,
smtp: 1e4,
smtpPort: 1e4,
domainSuggestion: 1e4,
whois: 1e4
}
};
let defaultCacheInstance = null;
function getDefaultCache() {
if (!defaultCacheInstance) {
defaultCacheInstance = {
mx: new LRUAdapter(DEFAULT_CACHE_OPTIONS.maxSize.mx, DEFAULT_CACHE_OPTIONS.ttl.mx),
disposable: new LRUAdapter(DEFAULT_CACHE_OPTIONS.maxSize.disposable, DEFAULT_CACHE_OPTIONS.ttl.disposable),
free: new LRUAdapter(DEFAULT_CACHE_OPTIONS.maxSize.free, DEFAULT_CACHE_OPTIONS.ttl.free),
domainValid: new LRUAdapter(DEFAULT_CACHE_OPTIONS.maxSize.domainValid, DEFAULT_CACHE_OPTIONS.ttl.domainValid),
smtp: new LRUAdapter(DEFAULT_CACHE_OPTIONS.maxSize.smtp, DEFAULT_CACHE_OPTIONS.ttl.smtp),
smtpPort: new LRUAdapter(DEFAULT_CACHE_OPTIONS.maxSize.smtpPort, DEFAULT_CACHE_OPTIONS.ttl.smtpPort),
domainSuggestion: new LRUAdapter(DEFAULT_CACHE_OPTIONS.maxSize.domainSuggestion, DEFAULT_CACHE_OPTIONS.ttl.domainSuggestion),
whois: new LRUAdapter(DEFAULT_CACHE_OPTIONS.maxSize.whois, DEFAULT_CACHE_OPTIONS.ttl.whois)
};
}
return defaultCacheInstance;
}
function getCacheStore(cache, key) {
return (cache === null || cache === void 0 ? void 0 : cache[key]) || getDefaultCache()[key];
}
function clearDefaultCache() {
if (defaultCacheInstance) {
defaultCacheInstance.mx.clear();
defaultCacheInstance.disposable.clear();
defaultCacheInstance.free.clear();
defaultCacheInstance.domainValid.clear();
defaultCacheInstance.smtp.clear();
defaultCacheInstance.smtpPort.clear();
defaultCacheInstance.domainSuggestion.clear();
defaultCacheInstance.whois.clear();
}
}
function resetDefaultCache() {
defaultCacheInstance = null;
}
const commonEmailDomains = [
// Popular free email providers
"gmail.com",
"yahoo.com",
"outlook.com",
"hotmail.com",
"icloud.com",
"aol.com",
"mail.com",
"protonmail.com",
"proton.me",
"yandex.com",
// Business/Professional email providers
"google.com",
"microsoft.com",
"apple.com",
"amazon.com",
"amazonaws.com",
// Secure/Privacy-focused providers
"tutanota.com",
"tuta.com",
"fastmail.com",
"posteo.de",
"mailbox.org",
"runbox.com",
"startmail.com",
// European providers
"gmx.com",
"gmx.de",
"gmx.net",
"web.de",
"freenet.de",
"t-online.de",
"arcor.de",
// Business email hosting
"zoho.com",
"titan.email",
"neo.space",
"rackspace.com",
"ionos.com",
"hostinger.com",
"bluehost.com",
"siteground.com",
"dreamhost.com",
"inmotionhosting.com",
"godaddy.com",
"namecheap.com",
"migadu.com",
"purelymail.com",
"infomaniak.com",
"uberspace.de",
"manitu.de",
"hetzner.com",
"scalahosting.com",
"hostpapa.com",
"liquidweb.com",
"icewarp.com",
"hostgator.com",
"a2hosting.com",
"spacemail.com",
"mxroute.com",
// Regional providers
"qq.com",
"163.com",
"126.com",
"sina.com",
"naver.com",
"daum.net",
"kakao.com"
];
const TYPO_PATTERNS = {
"gmail.com": ["gmai.com", "gmial.com", "gmali.com", "gmil.com", "gmaill.com", "gmail.co", "gmail.cm"],
"yahoo.com": ["yaho.com", "yahooo.com", "yahoo.co", "yahoo.cm", "yhaoo.com"],
"hotmail.com": ["hotmai.com", "hotmial.com", "hotmal.com", "hotmil.com", "hotmail.co", "hotmail.cm"],
"outlook.com": ["outlok.com", "outloo.com", "outlook.co", "outlook.cm", "outlokk.com"]
};
function getSimilarityThreshold(domain) {
const length = domain.length;
if (length <= 6)
return 0.85;
if (length <= 10)
return 0.8;
return 0.75;
}
function defaultDomainSuggestionMethod(domain, commonDomains) {
const domainsToCheck = commonDomains || commonEmailDomains;
const lowerDomain = domain.toLowerCase();
if (domainsToCheck.includes(lowerDomain)) {
return null;
}
for (const [correctDomain, typos] of Object.entries(TYPO_PATTERNS)) {
if (typos.includes(lowerDomain)) {
return {
original: domain,
suggested: correctDomain,
confidence: 0.95
// High confidence for known typo patterns
};
}
}
let bestMatch = null;
const threshold = getSimilarityThreshold(lowerDomain);
for (const commonDomain of domainsToCheck) {
const similarity = stringSimilarity(lowerDomain, commonDomain.toLowerCase());
if (similarity >= threshold) {
if (!bestMatch || similarity > bestMatch.similarity) {
bestMatch = { domain: commonDomain, similarity };
}
}
}
if (!bestMatch) {
for (const commonDomain of domainsToCheck) {
if (Math.abs(lowerDomain.length - commonDomain.length) <= 2) {
const similarity = stringSimilarity(lowerDomain, commonDomain.toLowerCase());
if (similarity >= 0.7) {
if (!bestMatch || similarity > bestMatch.similarity) {
bestMatch = { domain: commonDomain, similarity };
}
}
}
}
}
if (bestMatch) {
if (bestMatch.domain.charAt(0) !== lowerDomain.charAt(0) && bestMatch.similarity < 0.9) {
return null;
}
return {
original: domain,
suggested: bestMatch.domain,
confidence: bestMatch.similarity
};
}
return null;
}
async function defaultDomainSuggestionMethodAsync(domain, commonDomains, cache) {
return defaultDomainSuggestionMethodImpl(domain, commonDomains, cache);
}
async function defaultDomainSuggestionMethodImpl(domain, commonDomains, cache) {
if (!domain || domain.length < 3) {
return null;
}
const domainsToCheck = commonDomains || commonEmailDomains;
const lowerDomain = domain.toLowerCase();
const cacheKey = `${lowerDomain}:${domainsToCheck.length}`;
const cacheStore = getCacheStore(cache, "domainSuggestion");
const cached = await cacheStore.get(cacheKey);
const resolved = cached && typeof cached === "object" && "then" in cached ? await cached : cached;
if (resolved !== null && resolved !== void 0) {
return resolved;
}
if (domainsToCheck.includes(lowerDomain)) {
await cacheStore.set(cacheKey, null);
return null;
}
for (const [correctDomain, typos] of Object.entries(TYPO_PATTERNS)) {
if (typos.includes(lowerDomain)) {
const result = {
original: domain,
suggested: correctDomain,
confidence: 0.95
// High confidence for known typo patterns
};
await cacheStore.set(cacheKey, result);
return result;
}
}
let bestMatch = null;
const threshold = getSimilarityThreshold(lowerDomain);
for (const commonDomain of domainsToCheck) {
const similarity = stringSimilarity(lowerDomain, commonDomain.toLowerCase());
if (similarity >= threshold) {
if (!bestMatch || similarity > bestMatch.similarity) {
bestMatch = { domain: commonDomain, similarity };
}
}
}
if (!bestMatch) {
for (const commonDomain of domainsToCheck) {
if (Math.abs(lowerDomain.length - commonDomain.length) <= 2) {
const similarity = stringSimilarity(lowerDomain, commonDomain.toLowerCase());
if (similarity >= 0.7) {
if (!bestMatch || similarity > bestMatch.similarity) {
bestMatch = { domain: commonDomain, similarity };
}
}
}
}
}
if (bestMatch) {
if (bestMatch.domain.charAt(0) !== lowerDomain.charAt(0) && bestMatch.similarity < 0.9) {
await cacheStore.set(cacheKey, null);
return null;
}
const result = {
original: domain,
suggested: bestMatch.domain,
confidence: bestMatch.similarity
};
await cacheStore.set(cacheKey, result);
return result;
}
await cacheStore.set(cacheKey, null);
return null;
}
function suggestDomain(params) {
const { domain, customMethod, commonDomains } = params;
if (!domain || domain.length < 3) {
return null;
}
if (customMethod) {
try {
return customMethod(domain);
} catch (error) {
console.warn("Custom domain suggestion method failed, falling back to default:", error);
}
}
return defaultDomainSuggestionMethod(domain, commonDomains);
}
async function suggestEmailDomain(email, commonDomains, cache) {
if (!email || !email.includes("@")) {
return null;
}
const [localPart, domain] = email.split("@");
if (!domain || !localPart) {
return null;
}
const suggestion = await defaultDomainSuggestionMethodAsync(domain, commonDomains, cache);
if (suggestion) {
return {
original: email,
suggested: `${localPart}@${suggestion.suggested}`,
confidence: suggestion.confidence
};
}
return null;
}
function isCommonDomain(domain, commonDomains) {
const domainsToCheck = commonDomains || commonEmailDomains;
return domainsToCheck.includes(domain.toLowerCase());
}
function getDomainSimilarity(domain1, domain2) {
return stringSimilarity(domain1.toLowerCase(), domain2.toLowerCase());
}
async function isValidEmailDomain(emailOrDomain, cache) {
let [localPart, emailDomain] = (emailOrDomain === null || emailOrDomain === void 0 ? void 0 : emailOrDomain.split("@")) || [];
if (!emailDomain) {
emailDomain = localPart;
}
if (!emailDomain) {
return false;
}
const cacheStore = getCacheStore(cache, "domainValid");
const cached = await cacheStore.get(emailDomain);
if (cached !== null && cached !== void 0) {
return cached.isValid;
}
try {
const isValidResult = isValid(emailDomain) || false;
const richResult = {
isValid: isValidResult,
hasMX: false,
// MX not checked in this function
checkedAt: Date.now()
};
await cacheStore.set(emailDomain, richResult);
return isValidResult;
} catch (validationError) {
const errorResult = {
isValid: false,
hasMX: false,
checkedAt: Date.now()
};
await cacheStore.set(emailDomain, errorResult);
return false;
}
}
function isValidEmail(emailAddress) {
if (!emailAddress || typeof emailAddress !== "string") {
return false;
}
const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
const emailLower = emailAddress.toLowerCase();
if (emailLower.indexOf(".+") !== -1)
return false;
if (emailLower.indexOf("..") !== -1)
return false;
if (emailLower.startsWith(".") || emailLower.endsWith("."))
return false;
const parts = emailAddress.split("@");
if (parts.length !== 2)
return false;
const [localPart, domain] = parts;
if (!localPart || !domain)
return false;
if (localPart.length > 64)
return false;
if (domain.length > 253)
return false;
return re.test(emailLower);
}
async function resolveMxRecords(params) {
const { domain, cache, logger } = params;
const log = logger || (() => {
});
const cacheStore = getCacheStore(cache, "mx");
const cached = await cacheStore.get(domain);
if (cached !== null && cached !== void 0) {
log(`[resolveMxRecords] Cache hit for ${domain}: ${cached === null || cached === void 0 ? void 0 : cached.length} MX records`);
return cached;
}
log(`[resolveMxRecords] Performing DNS MX lookup for ${domain}`);
try {
const records = await promises.resolveMx(domain);
records === null || records === void 0 ? void 0 : records.sort((a, b) => {
if (a.priority < b.priority) {
return -1;
}
if (a.priority > b.priority) {
return 1;
}
return 0;
});
const exchanges = records === null || records === void 0 ? void 0 : records.map((record) => record.exchange);
log(`[resolveMxRecords] Found ${exchanges === null || exchanges === void 0 ? void 0 : exchanges.length} MX records for ${domain}: [${exchanges === null || exchanges === void 0 ? void 0 : exchanges.join(", ")}]`);
await cacheStore.set(domain, exchanges);
log(`[resolveMxRecords] Cached ${exchanges === null || exchanges === void 0 ? void 0 : exchanges.length} MX records for ${domain}`);
return exchanges;
} catch (error) {
log(`[resolveMxRecords] MX lookup failed for ${domain}, caching empty result`);
await cacheStore.set(domain, []);
throw error;
}
}
const nameSeparator = [".", "_", "-"];
const commonNameSuffixes = [
"mail",
"email",
"contact",
"info",
"admin",
"support",
"sales",
"help",
"noreply",
"no-reply",
"donotreply",
"notifications",
"alerts"
];
const contextualSuffixes = [
"dev",
"company",
"team",
"group",
"office",
"work",
"personal",
"home",
"private",
"business",
"corp",
"inc",
"ltd",
"llc",
"org"
];
const COMMON_TITLES = [
"mr",
"mrs",
"ms",
"miss",
"dr",
"prof",
"sir",
"lord",
"lady",
"captain",
"rev",
"father",
"sister"
];
const commonFirstName = /* @__PURE__ */ new Set([
// English names
"james",
"john",
"robert",
"michael",
"william",
"david",
"richard",
"joseph",
"thomas",
"charles",
"christopher",
"daniel",
"matthew",
"kenneth",
"anthony",
"mark",
"donald",
"steven",
"brian",
"paul",
"andrew",
"joshua",
"kevin",
"eric",
"stephen",
"benjamin",
"samuel",
"gregory",
"frank",
"alexander",
"mary",
"patricia",
"jennifer",
"linda",
"elizabeth",
"barbara",
"susan",
"jessica",
"sarah",
"margaret",
"karen",
"lisa",
"nancy",
"betty",
"helen",
"sandra",
"donna",
"carol",
"ruth",
"sharon",
"michelle",
"laura",
"kimberly",
"deborah",
"amy",
"angela",
"ashley",
"brenda",
"catherine",
"emma",
"olivia",
"sophia",
"ava",
"isabella",
"mia",
"charlotte",
"amelia",
"harper",
"evelyn",
"abigail",
// Spanish/Latin names
"juan",
"carlos",
"jose",
"luis",
"antonio",
"francisco",
"manuel",
"miguel",
"pedro",
"jorge",
"maria",
"carmen",
"isabel",
"ana",
"lucia",
"sofia",
"valentina",
"camila",
"gabriela",
"daniela",
// Arabic names
"mohammed",
"mohamed",
"ahmed",
"ali",
"hassan",
"ibrahim",
"omar",
"khalid",
"abdullah",
"yusuf",
"fatima",
"aisha",
"zainab",
"maryam",
"khadija",
"sara",
"layla",
"noor",
"hana",
"amira",
// Asian names
"wei",
"jing",
"chen",
"li",
"wang",
"zhang",
"liu",
"yang",
"huang",
"zhao",
"satoshi",
"takeshi",
"hiroshi",
"yuki",
"akira",
"kenji",
"taro",
"naoko",
"yuko",
"keiko",
"raj",
"arjun",
"rohit",
"amit",
"vikram",
"priya",
"anjali",
"neha",
"pooja",
"divya",
// European names
"jean",
"pierre",
"jacques",
"marie",
"francois",
"giuseppe",
"marco",
"alessandro",
"francesca",
"giulia",
"hans",
"klaus",
"peter",
"anna",
"katrin",
"vladimir",
"sergey",
"olga",
"natasha",
"ivan",
// African names
"kwame",
"kofi",
"amara",
"fatou",
"aminata",
"ousmane",
"amadou",
"ibrahim",
"sekou",
"mariama"
]);
const commonLastName = /* @__PURE__ */ new Set([
// English surnames
"smith",
"johnson",
"williams",
"brown",
"jones",
"garcia",
"miller",
"davis",
"rodriguez",
"martinez",
"hernandez",
"lopez",
"gonzalez",
"wilson",
"anderson",
"thomas",
"taylor",
"moore",
"jackson",
"martin",
"lee",
"thompson",
"white",
"harris",
"clark",
"lewis",
"robinson",
"walker",
"young",
"king",
// Spanish surnames
"sanchez",
"ramirez",
"torres",
"flores",
"rivera",
"gomez",
"diaz",
"reyes",
"morales",
"cruz",
"ortiz",
"gutierrez",
"chavez",
"ruiz",
"alvarez",
"castillo",
"jimenez",
"vasquez",
"romero",
"herrera",
// Asian surnames
"wang",
"li",
"zhang",
"chen",
"liu",
"yang",
"huang",
"zhao",
"wu",
"zhou",
"tanaka",
"watanabe",
"ito",
"yamamoto",
"nakamura",
"kim",
"park",
"choi",
"jung",
"kang",
"patel",
"sharma",
"kumar",
"singh",
"gupta",
"khan",
"reddy",
"rao",
"mehta",
"shah",
// Other common surnames
"nguyen",
"tran",
"pham",
"murphy",
"kelly",
"sullivan",
"walsh",
"connor",
"ryan",
"byrne",
"schmidt",
"mueller",
"schneider",
"fischer",
"weber",
"rossi",
"russo",
"ferrari",
"bianchi",
"romano",
"silva",
"santos",
"ferreira",
"pereira",
"costa",
"ivanov",
"petrov",
"sidorov",
"smirnov",
"volkov"
]);
function isYearLike(str) {
return /^(19|20)\d{2}$/.test(str);
}
function isKnownFirstName(str) {
return commonFirstName.has(str.toLowerCase());
}
function isKnownLastName(str) {
return commonLastName.has(str.toLowerCase());
}
function isTitle(str) {
return COMMON_TITLES.includes(str.toLowerCase().replace(".", ""));
}
function getFirstNameScore(str) {
const lower = str.toLowerCase();
if (commonFirstName.has(lower))
return 1;
if (commonLastName.has(lower))
return 0.3;
if (str.length >= 2 && str.length <= 15 && /^[a-zA-Z]+$/.test(str))
return 0.5;
return 0;
}
function getLastNameScore(str) {
const lower = str.toLowerCase();
if (commonLastName.has(lower))
return 1;
if (commonFirstName.has(lower))
return 0.3;
if (str.length >= 2 && str.length <= 20 && /^[a-zA-Z]+$/.test(str))
return 0.5;
return 0;
}
function capitalizeName(str) {
if (!str)
return "";
const specialPatterns = [
{
pattern: /^(o')([a-z]+)/i,
format: (m) => `O'${m[2].charAt(0).toUpperCase()}${m[2].slice(1).toLowerCase()}`
},
{
pattern: /^(mac)([a-z]+)/i,
format: (m) => `Mac${m[2].charAt(0).toUpperCase()}${m[2].slice(1).toLowerCase()}`
},
{
pattern: /^(mc)([a-z]+)/i,
format: (m) => `Mc${m[2].charAt(0).toUpperCase()}${m[2].slice(1).toLowerCase()}`
},
{
pattern: /^(van|von|de|del|dela|da|di|le|la|du|den|der|ter|ten)(\s+)([a-z]+)/i,
format: (m) => m[1].toLowerCase() + m[2] + m[3].charAt(0).toUpperCase() + m[3].slice(1).toLowerCase()
}
];
for (const { pattern, format } of specialPatterns) {
const match = str.match(pattern);
if (match) {
return format(match);
}
}
if (str.includes("-")) {
return str.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join("-");
}
if (str.includes("'") && str.indexOf("'") > 0) {
const parts = str.split("'");
return parts.map((part, i) => i === 0 ? part.charAt(0).toUpperCase() + part.slice(1).toLowerCase() : part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join("'");
}
if (/^[a-zA-Z]/.test(str)) {
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}
return str;
}
function parseCompositeNamePart(part) {
const hasNumbers = /\d/.test(part);
let confidence = 1;
let cleaned = part;
const leetSpeakMap = {
"0": "o",
"1": "i",
"3": "e",
"4": "a",
"5": "s",
"7": "t",
"8": "b"
};
if (hasNumbers && part.length >= 3) {
let leetCleaned = part.toLowerCase();
for (const [num, letter] of Object.entries(leetSpeakMap)) {
leetCleaned = leetCleaned.replace(new RegExp(num, "g"), letter);
}
if (isKnownFirstName(leetCleaned) || isKnownLastName(leetCleaned)) {
cleaned = leetCleaned;
confidence = 0.7;
}
}
if (cleaned === part) {
const withoutYear = part.replace(/(19|20)\d{2}$/, "");
if (withoutYear !== part && withoutYear.length >= 2) {
cleaned = withoutYear;
confidence = 0.8;
} else {
const pureAlpha = part.replace(/\d+/g, "");
if (pureAlpha.length >= 2) {
cleaned = pureAlpha;
confidence = hasNumbers ? 0.85 : 1;
} else if (pureAlpha.length === 1 && /^[a-zA-Z]$/.test(pureAlpha)) {
cleaned = pureAlpha;
confidence = 0.6;
} else {
const baseMatch2 = part.match(/^([a-zA-Z]+)\d*$/);
if (baseMatch2) {
cleaned = baseMatch2[1];
confidence = 0.75;
} else {
cleaned = part;
confidence = 0.4;
}
}
}
}
if (cleaned !== part) {
if (isKnownFirstName(cleaned) || isKnownLastName(cleaned)) {
confidence = Math.min(1, confidence + 0.2);
}
}
const baseMatch = part.match(/^([a-zA-Z]+[a-zA-Z0-9]*?)\d*$/);
const base = baseMatch ? baseMatch[1] : part;
return { base, hasNumbers, cleaned, confidence };
}
function isLikelyName(str, allowNumbers = false, allowSingleLetter = false) {
if (!str)
return false;
if (str.length < 2 && !allowSingleLetter)
return false;
if (str.length === 1 && allowSingleLetter && /^[a-zA-Z]$/.test(str))
return true;
if (commonNameSuffixes.includes(str.toLowerCase()))
return false;
if (allowNumbers) {
if (/^\d+$/.test(str))
return false;
return true;
}
const digitCount = (str.match(/\d/g) || []).length;
if (digitCount > str.length * 0.3)
return false;
return true;
}
function defaultNameDetectionMethod(email) {
if (!email || !email.includes("@")) {
return null;
}
const [localPart] = email.split("@");
if (!localPart) {
return null;
}
const localWithoutAlias = localPart.split("+")[0];
const cleanedLocal = localWithoutAlias;
if (cleanedLocal.toLowerCase() === "no-reply" || cleanedLocal.toLowerCase() === "noreply") {
return null;
}
let firstName;
let lastName;
let confidence = 0;
const camelCasePatterns = [
/^([a-z]+)([A-Z][a-z]+)$/,
// johnDoe
/^([A-Z][a-z]+)([A-Z][a-z]+)$/,
// JohnSmith
/^([a-z]+)([A-Z][a-z]+)([A-Z][a-z]+)$/
// johnMcDonald
];
for (const pattern of camelCasePatterns) {
const match = cleanedLocal.match(pattern);
if (match) {
if (match.length === 3) {
const [, first, last] = match;
if (isLikelyName(first) && isLikelyName(last)) {
const firstScore = getFirstNameScore(first);
const lastScore = getLastNameScore(last);
firstName = capitalizeName(first);
lastName = capitalizeName(last);
confidence = 0.7 + (firstScore + lastScore) * 0.15;
break;
}
} else if (match.length === 4) {
const [, first, middle, last] = match;
const combined = middle + last;
if (isLikelyName(first)) {
firstName = capitalizeName(first);
lastName = capitalizeName(combined);
confidence = 0.75;
break;
}
}
}
}
if (!firstName && !lastName) {
for (const separator of nameSeparator) {
if (cleanedLocal.includes(separator)) {
const parts = cleanedLocal.split(separator).filter((p) => p.length > 0);
if (parts.length === 2) {
const [first, last] = parts;
if (isTitle(first)) {
continue;
}
const firstParsed = parseCompositeNamePart(first);
const lastParsed = parseCompositeNamePart(last);
const firstNameScore = getFirstNameScore(firstParsed.cleaned);
const lastNameScore = getLastNameScore(lastParsed.cleaned);
const reverseScore = getLastNameScore(firstParsed.cleaned) + getFirstNameScore(lastParsed.cleaned);
const normalScore = firstNameScore + lastNameScore;
const firstLikely = isLikelyName(first, true, true);
const lastLikely = isLikelyName(last, true, true);
const oneIsSingleLetter = first.length === 1 || last.length === 1;
const otherIsLongEnough = oneIsSingleLetter ? first.length === 1 ? last.length >= 2 : first.length >= 2 : true;
const bothPartsValid = firstLikely && lastLikely && otherIsLongEnough;
if (bothPartsValid) {
const useReversed = reverseScore > normalScore * 1.2;
if (firstParsed.hasNumbers || lastParsed.hasNumbers) {
const cleanedFirst = firstParsed.cleaned;
const cleanedLast = lastParsed.cleaned;
if (isLikelyName(cleanedFirst, false, true) && isLikelyName(cleanedLast, false, true)) {
if (useReversed) {
firstName = capitalizeName(cleanedLast);
lastName = capitalizeName(cleanedFirst);
} else {
firstName = capitalizeName(cleanedFirst);
lastName = capitalizeName(cleanedLast);
}
const baseConfidence = 0.7;
const separatorBonus = separator === "." ? 0.15 : separator === "_" ? 0.05 : 0;
const nameRecognitionBonus = normalScore > 0.5 ? 0.1 : 0;
const cleaningPenalty = (firstParsed.confidence + lastParsed.confidence) / 2 - 1;
confidence = Math.min(0.95, baseConfidence + separatorBonus + nameRecognitionBonus + cleaningPenalty);
} else {
firstName = capitalizeName(first);
lastName = capitalizeName(last);
confidence = 0.5;
}
} else {
if (useReversed) {
firstName = capitalizeName(last);
lastName = capitalizeName(first);
} else {
firstName = capitalizeName(first);
lastName = capitalizeName(last);
}
const baseConfidence = 0.75;
const separatorBonus = separator === "." ? 0.15 : separator === "_" ? 0.05 : 0;
const nameRecognitionBonus = Math.min(0.15, normalScore * 0.15);
confidence = Math.min(0.95, baseConfidence + separatorBonus + nameRecognitionBonus);
}
break;
}
} else if (parts.length === 3) {
const [first, middle, last] = parts;
const firstParsed = parseCompositeNamePart(first);
const middleParsed = parseCompositeNamePart(middle);
const lastParsed = parseCompositeNamePart(last);
const isLastSuffix = commonNameSuffixes.includes(last.toLowerCase()) || contextualSuffixes.includes(last.toLowerCase()) || isYearLike(last);
if (isLastSuffix) {
if (isLikelyName(first, true, true) && isLikelyName(middle, true, true)) {
const cleanedFirst = firstParsed.hasNumbers ? firstParsed.cleaned : first;
const cleanedMiddle = middleParsed.hasNumbers ? middleParsed.cleaned : middle;
if (isLikelyName(cleanedFirst, false, true) && isLikelyName(cleanedMiddle, false, true)) {
firstName = capitalizeName(cleanedFirst);
lastName = capitalizeName(cleanedMiddle);
confidence = 0.7;
} else {
firstName = capitalizeName(first);
lastName = capitalizeName(middle);
confidence = 0.65;
}
break;
}
} else if (isLikelyName(first, true, true) && isLikelyName(last, true, true)) {
const cleanedFirst = firstParsed.hasNumbers ? firstParsed.cleaned : first;
const cleanedLast = lastParsed.hasNumbers ? lastParsed.cleaned : last;
if (isLikelyName(cleanedFirst, false, true) && isLikelyName(cleanedLast, false, true)) {
firstName = capitalizeName(cleanedFirst);
lastName = capitalizeName(cleanedLast);
confidence = 0.75;
} else {
firstName = capitalizeName(first);
lastName = capitalizeName(last);
confidence = 0.55;
}
break;
}
} else if (parts.length > 3) {
const firstPart = parts[0];
const lastPartLower = parts[parts.length - 1].toLowerCase();
const isLastPartSuffix = commonNameSuffixes.includes(lastPartLower) || contextualSuffixes.includes(lastPartLower) || isYearLike(parts[parts.length - 1]);
const effectiveLastIndex = isLastPartSuffix ? parts.length - 2 : parts.length - 1;
const lastToUse = effectiveLastIndex >= 0 ? parts[effectiveLastIndex] : null;
if (lastToUse && isLikelyName(firstPart, true, true) && isLikelyName(lastToUse, true, true)) {
const firstParsed = parseCompositeNamePart(firstPart);
const lastParsed = parseCompositeNamePart(lastToUse);
const cleanedFirst = firstParsed.hasNumbers ? firstParsed.cleaned : firstPart;
const cleanedLast = lastParsed.hasNumbers ? lastParsed.cleaned : lastToUse;
if (isLikelyName(cleanedFirst, false, true) && isLikelyName(cleanedLast, false, true)) {
firstName = capitalizeName(cleanedFirst);
lastName = capitalizeName(cleanedLast);
confidence = 0.6;
} else {
firstName = capitalizeName(firstPart);
lastName = capitalizeName(lastToUse);
confidence = 0.5;
}
break;
}
}
}
}
}
if (!firstName && !lastName) {
const underscoreMatch = cleanedLocal.match(/^([A-Z_]+)$/);
if (underscoreMatch) {
const parts = cleanedLocal.toLowerCase().split("_").filter((p) => p.length > 0);
if (parts.length === 2) {
const [first, last] = parts;
if (isLikelyName(first) && isLikelyName(last)) {
firstName = capitalizeName(first);
lastName = capitalizeName(last);
confidence = 0.65;
}
}
}
}
if (!firstName && !lastName) {
const parsed = parseCompositeNamePart(cleanedLocal);
if (isLikelyName(cleanedLocal, true, false)) {
if (/^[a-zA-Z]+$/.test(cleanedLocal)) {
const nameScore = Math.max(getFirstNameScore(cleanedLocal), getLastNameScore(cleanedLocal));
if (getFirstNameScore(cleanedLocal) >= getLastNameScore(cleanedLocal)) {
firstName = capitalizeName(cleanedLocal);
} else {
lastName = capitalizeName(cleanedLocal);
}
confidence = 0.4 + nameScore * 0.3;
} else if (parsed.hasNumbers && parsed.cleaned.length >= 2) {
const cleanedScore = Math.max(getFirstNameScore(parsed.cleaned), getLastNameScore(parsed.cleaned));
if (getFirstNameScore(parsed.cleaned) >= getLastNameScore(parsed.cleaned)) {
firstName = capitalizeName(parsed.cleaned);
} else {
lastName = capitalizeName(parsed.cleaned);
}
confidence = 0.3 + cleanedScore * 0.2 + parsed.confidence * 0.2;
}
}
}
if (!firstName && !lastName) {
return null;
}
return {
firstName,
lastName,
confidence: Math.max(0, Math.min(1, confidence))
// Ensure confidence is between 0 and 1
};
}
function detectNameFromEmail(params) {
const { email, customMethod } = params;
if (!email || !email.includes("@")) {
return null;
}
if (customMethod) {
try {
return customMethod(email);
} catch (error) {
console.warn("Custom name detection method failed, falling back to default:", error);
}
}
return defaultNameDetectionMethod(email);
}
function cleanNameForAlgorithm(name) {
if (!name)
return "";
let cleaned = name.replace(/[._*]/g, "");
cleaned = cleaned.replace(/\s+/g, " ").trim();
if (!cleaned) {
return name;
}
return cleaned;
}
function detectNameForAlgorithm(email) {
const detectedName = detectName(email);
if (!detectedName) {
return null;
}
const cleanedFirstName = detectedName.firstName ? cleanNameForAlgorithm(detectedName.firstName) : void 0;
const cleanedLastName = detectedName.lastName ? cleanNameForAlgorithm(detectedName.lastName) : void 0;
if (!cleanedFirstName && !cleanedLastName) {
return null;
}
return {
firstName: cleanedFirstName,
lastName: cleanedLastName,
confidence: detectedName.confidence * 0.95
// Slightly reduce confidence due to cleaning
};
}
function detectName(email) {
return detectNameFromEmail({ email });
}
var VerificationErrorCode;
(function(VerificationErrorCode2) {
VerificationErrorCode2["invalidFormat"] = "INVALID_FORMAT";
VerificationErrorCode2["invalidDomain"] = "INVALID_DOMAIN";
VerificationErrorCode2["noMxRecords"] = "NO_MX_RECORDS";
VerificationErrorCode2["smtpConnectionFailed"] = "SMTP_CONNECTION_FAILED";
VerificationErrorCode2["smtpTimeout"] = "SMTP_TIMEOUT";
VerificationErrorCode2["mailboxNotFound"] = "MAILBOX_NOT_FOUND";
VerificationErrorCode2["mailboxFull"] = "MAILBOX_FULL";
VerificationErrorCode2["networkError"] = "NETWORK_ERROR";
VerificationErrorCode2["disposableEmail"] = "DISPOSABLE_EMAIL";
VerificationErrorCode2["freeEmailProvider"] = "FREE_EMAIL_PROVIDER";
})(VerificationErrorCode || (VerificationErrorCode = {}));
var EmailProvider;
(function(EmailProvider2) {
EmailProvider2["gmail"] = "gmail";
EmailProvider2["hotmailB2b"] = "hotmail_b2b";
EmailProvider2["hotmailB2c"] = "hotmail_b2c";
EmailProvider2["proofpoint"] = "proofpoint";
EmailProvider2["mimecast"] = "mimecast";
EmailProvider2["yahoo"] = "yahoo";
EmailProvider2["everythingElse"] = "everything_else";
})(EmailProvider || (EmailProvider = {}));
function parseSmtpError(errorMessage) {
const lowerError = errorMessage.toLowerCase();
const networkErrorPatterns = [
"etimedout",
"econnrefused",
"enotfound",
"econnreset",
"socket hang up",
"connection_timeout",
"socket_timeout",
"connection_error",
"connection_closed"
];
const isNetworkError = networkErrorPatterns.some((pattern) => lowerError.includes(pattern));
if (isNetworkError) {
return {
isDisabled: false,
hasFullInbox: false,
isInvalid: true,
isCatchAll: false
};
}
const disabledPatterns = [
"account disabled",
"account is disabled",
"user disabled",
"user is disabled",
"account locked",
"account is locked",
"user blocked",
"user is blocked",
"mailbox disabled",
"delivery not authorized",
"message rejected",
"access denied",
"permission denied",
"recipient unknown",
"recipient address rejected",
"user unknown",
"address unknown",
"invalid recipient",
"not a valid recipient",
"recipient does not exist",
"no such user",
"user does not exist",
"mailbox unavailable",
"recipient unavailable",
"address rejected",
"550",
"551",
"553",
"not_found",
"ambiguous"
];
const fullInboxPatterns = [
"mailbox full",
"inbox full",
"quota exceeded",
"over quota",
"storage limit exceeded",
"message too large",
"insufficient storage",
"mailbox over quota",
"over the quota",
"mailbox size limit exceeded",
"account over quota",
"storage space",
"overquota",
"452",
"552",
"over_quota"
];
const catchAllPatterns = [
"accept all mail",
"catch-all",
"catchall",
"wildcard",
"accepts any recipient",
"recipient address accepted"
];
const rateLimitPatterns = [
"receiving mail at a rate that",
"rate limit",
"too many messages",
"temporarily rejected",
"try again later",
"greylisted",
"greylist",
"deferring",
"temporarily deferred",
"421",
"450",
"451",
"temporary_failure"
];
const isDisabled = disabledPatterns.some((pattern) => lowerError.includes(pattern)) || lowerError.startsWith("550") || lowerError.startsWith("551") || lowerError.startsWith("553");
const hasFullInbox = fullInboxPatterns.some((pattern) => lowerError.includes(pattern)) || lowerError.startsWith("452") || lowerError.startsWith("552");
const isCatchAll = catchAllPatterns.some((pattern) => lowerError.includes(pattern));
const isInvalid = !isDisabled && !hasFullInbox && !isCatchAll && !rateLimitPatterns.some((pattern) => lowerError.includes(pattern)) && !lowerError.startsWith("421") && !lowerError.startsWith("450") && !lowerError.startsWith("451");
return {
isDisabled,
hasFullInbox,
isInvalid,
isCatchAll
};
}
var SMTPStep;
(function(SMTPStep2) {
SMTPStep2["greeting"] = "GREETING";
SMTPStep2["ehlo"] = "EHLO";
SMTPStep2["helo"] = "HELO";
SMTPStep2["startTls"] = "STARTTLS";
SMTPStep2["mailFrom"] = "MAIL_FROM";
SMTPStep2["rcptTo"] = "RCPT_TO";
SMTPStep2["vrfy"] = "VRFY";
SMTPStep2["quit"] = "QUIT";
})(SMTPStep || (SMTPStep = {}));
function isIPAddress(host) {
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (ipv4Regex.test(host)) {
const octets = host.split(".");
return octets.every((octet) => parseInt(octet, 10) <= 255);
}
const ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^(?:[0-9a-fA-F]{1,4}:){1,7}:$|^(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}$|^(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}$|^(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}$|^(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}$|^(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}$|^[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})$|^::1(?::(?::[0-9a-fA-F]{1,4}){1,7})|$|:(?:(?::[0-9a-fA-F]{1,4}){1,7}:)$/;
return ipv6Regex.test(host);
}
function isHighVolume(smtpReply) {
return Boolean(smtpReply && /high number of/gi.test(smtpReply));
}
function isOverQuota(smtpReply) {
return Boolean(smtpReply && /(over quota)/gi.test(smtpReply));
}
function isInvalidMailboxError(smtpReply) {
return Boolean(smtpReply && /^(510|511|513|550|551|553)/.test(smtpReply) && !/(junk|spam|openspf|spoofing|host|rbl.+blocked)/gi.test(smtpReply));
}
function isMultilineGreet(smtpReply) {
return Boolean(smtpReply && /^(250|220)-/.test(smtpReply));
}
const DEFAULT_PORTS = [25, 587, 465];
const DEFAULT_TIMEOUT = 3e3;
const DEFAULT_MAX_RETRIES = 1;
const PORT_CONFIGS = {
25: { tls: false, starttls: true },
587: { tls: false, starttls: true },
465: { tls: true, starttls: false }
};
async function verifyMailboxSMTP(params) {
const { local, domain, mxRecords = [], options = {} } = params;
const { ports = DEFAULT_PORTS, timeout = DEFAULT_TIMEOUT, maxRetries = DEFAULT_MAX_RETRIES, tls: tlsConfig = true, hostname = "localhost", useVRFY = true, cache, debug = false, sequence } = options;
const log = debug ? (...args) => console.log("[SMTP]", ...args) : () => {
};
const createSmtpResult = (result, port, tlsUsed, mxHost2) => {
const reason = result === true ? "valid" : result === null ? "ambiguous" : "not_found";
const parsedError = parseSmtpError(reason);
return {
canConnectSmtp: result !== null,
hasFullInbox: parsedError.hasFullInbox,
isCatchAll: parsedError.isCatchAll,
isDeliverable: result === true,
isDisabled: result === false && parsedError.isDisabled,
error: result === null ? reason : result === false ? reason : void 0,
providerUsed: EmailProvider.everythingElse,
checkedAt: Date.now()
};
};
const createFailureResult = (error) => ({
canConnectSmtp: false,
hasFullInbox: false,
isCatchAll: false,
isDeliverable: false,
isDisabled: false,
error,
providerUsed: EmailProvider.everythingElse,
checkedAt: Date.now()
});
if (!mxRecords || mxRecords.length === 0) {
log("No MX records found");
return {
smtpResult: createFailureResult("No MX records found"),
cached: false,
port: 0,
portCached: false
};
}
const mxHost = mxRecords[0];
log(`Verifying ${local}@${domain} via ${mxHost}`);
const smtpCacheStore = cache ? getCacheStore(cache, "smtp") : null;
if (smtpCacheStore) {
let cachedResult;
try {
cachedResult = await smtpCacheStore.get(`${mxHost}:${local}@${domain}`);
if (cachedResult !== void 0 && cachedResult !== null) {
log(`Using cached SMTP result: ${cachedResult.isDeliverable}`);
return {
smtpResult: cachedResult,
cached: true,
port: 0,
portCached: false
};
}
} catch (ignoredError) {
cachedResult = void 0;
}
}
const smtpPortCacheStore = cache ? getCacheStore(cache, "smtpPort") : null;
if (smtpPortCacheStore) {
let cachedPort;
try {
cachedPort = await smtpPortCacheStore.get(mxHost);
} catch (ignoredError) {
cachedPort = null;
}
if (cachedPort) {
log(`Using cached port: ${cachedPort}`);
const result = await testSMTPConnection({
mxHost,
port: cachedPort,
local,
domain,
timeout,
tlsConfig,
hostname,
useVRFY,
sequence,
log
});
const smtpResult = createSmtpResult(result);
if (smtpCacheStore) {
try {
await smtpCacheStore.set(`${mxHost}:${local}@${domain}`, smtpResult);
log(`Cached SMTP result ${result} for ${local}@${domain} via ${mxHost}`);
} catch (ignoredError) {
}
}
return { smtpResult, cached: false, port: cachedPort, portCached: true };
}
}
for (const port of ports) {
log(`Testing port ${port}`);
for (let attempt = 0; attempt < maxRetries; attempt++) {
if (attempt > 0) {
const delay = Math.min(200 * 2 ** (attempt - 1), 800);
log(`Retry ${attempt + 1}, waiting ${delay}ms`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
const result = await testSMTPConnection({
mxHost,
port,
local,
domain,
timeout,
tlsConfig,
hostname,
useVRFY,
sequence,
log
});
const smtpResult = createSmtpResult(result);
if (smtpCacheStore) {
try {
await smtpCacheStore.set(`${mxHost}:${local}@${domain}`, smtpResult);
log(`Cached SMTP result ${result} for ${local}@${domain} via ${mxHost}`);
} catch (ignoredError) {
}
}
if (result !== null) {
if (smtpPortCacheStore) {
try {
await smtpPortCacheStore.set(mxHost, port);
log(`Cached port ${port} for ${mxHost}`);
} catch (ignoredError) {
}
}
return { smtpResult, cached: false, port, portCached: false };
}
}
}
log("All ports failed");
return {
smtpResult: createFailureResult("All SMTP connection attempts failed"),
cached: false,
port: 0,
portCached: false
};
}
async function testSMTPConnection(params) {
const { mxHost, port, local, domain, timeout, tlsConfig, hostname, useVRFY, sequence, log } = params;
const portConfig = PORT_CONFIGS[port] || { tls: false, starttls: false };
const useTLS = tlsConfig !== false && (portConfig.tls || portConfig.starttls);
const implicitTLS = portConfig.tls;
const defaultSequence = {
steps: [SMTPStep.greeting, SMTPStep.ehlo, SMTPStep.mailFrom, SMTPStep.rcptTo]
};
const activeSequence = sequence || defaultSequence;
if (port === 25) {
activeSequence.steps = activeSequence.steps.map((step) => step === SMTPStep.ehlo ? SMTPStep.helo : step);
}
const tlsOptions = {
host: mxHost,
servername: isIPAddress(mxHost) ? void 0 : mxHost,
// Don't set servername for IP addresses
rejectUnauthorized: false,
minVersion: "TLSv1.2",
...typeof tlsConfig === "object" ? tlsConfig : {}
};
return new Promise((resolve) => {
let socket;
let buffer = "";
let isTLS = implicitTLS;
let currentStepIndex = 0;
let resolved = false;
let supportsSTARTTLS = false;
let supportsVRFY = false;
let cleanup = () => {
if (resolved)
return;
resolved = true;
try {
socket === null || socket === void 0 ? void 0 : socket.write("QUIT\r\n");
} catch {
}
setTimeout(() => socket === null || socket === void 0 ? void 0 : socket.destroy(), 100);
};
const finish = (result, reason) => {
if (resolved)
return;
log(`${port}: ${reason || (result ? "valid" : "invalid")}`);
cleanup();
resolve(result);
};
const sendCommand = (cmd) => {
if (resolved)
return;
log(`\u2192 ${cmd}`);
socket === null || socket === void 0 ? void 0 : socket.write(`${cmd}\r
`);
};
const nextStep = () => {
currentStepIndex++;
if (currentStepIndex >= activeSequence.steps.length) {
finish(true, "sequence_complete");
return;
}
executeStep(activeSequence.steps[currentStepIndex]);
};
const executeStep = (step) => {
if (resolved)
return;
switch (step) {
case SMTPStep.ehlo:
sendCommand(`EHLO ${hostname}`);
break;
case SMTPStep.helo:
sendCommand(`HELO ${domain}`);
break;
case SMTPStep.greeting:
break;
case SMTPStep.startTls:
sendCommand("STARTTLS");
break;
case SMTPStep.mailFrom: {
const from = activeSequence.from || `<${local}@${domain}>`;
sendCommand(`MAIL FROM:${from}`);
break;
}
case SMTPStep.rcptTo:
sendCommand(`RCPT TO:<${local}@${domain}>`);
break;
case SMTPStep.vrfy: {
const vrfyTarget = activeSequence.vrfyTarget || local;
sendCommand(`VRFY ${vrfyTarget}`);
break;
}
case SMTPStep.quit:
sendCommand("QUIT");
break;
}
};
const processResponse = (response) => {
if (resolved)
return;
const code = response.substring(0, 3);
const isMultiline = response.length > 3 && response[3] === "-";
log(`\u2190 ${response}`);
if (isMultilineGreet(response)) {
return;
}
if (isHighVolume(response)) {
finish(true, "high_volume");
return;
}
if (isOverQuota(response)) {
finish(false, "over_quota");
return;
}
if (isInvalidMailboxError(response)) {
finish(false, "not_found");
return;
}
if (isMultiline) {
const currentStep2 = activeSequence.steps[currentStepIndex];
if (currentStep2 === SMTPStep.ehlo && code === "250") {
const upper = response.toUpperCase();
if (upper.includes("STARTTLS"))
supportsSTARTTLS = true;
if (upper.includes("VRFY"))
supportsVRFY = true;
}
if (currentStep2 === SMTPStep.helo && code === "250") {
const upper = response.toUpperCase();
if (upper.includes("VRFY"))
supportsVRFY = true;
}
return;
}
if (!response.includes("220") && !response.includes("250") && !response.includes("550") && !response.includes("552")) {
finish(null, "unrecognized_response");
return;
}
const currentStep = activeSequence.steps[currentStepIndex];
switch (currentStep) {
case SMTPStep.greeting:
if (code.startsWith("220")) {
nextStep();
} else {
finish(null, "no_greeting");
}
break;
case SMTPStep.ehlo:
if (code.startsWith("250")) {
const hasSTARTTLS = activeSequence.steps.includes(SMTPStep.startTls);
if (!isTLS && useTLS && supportsSTARTTLS && !implicitTLS && hasSTARTTLS) {
currentStepIndex = activeSequence.steps.indexOf(SMTPStep.startTls);
executeStep(SMTPStep.startTls);
} else {
nextStep();
}
} else {
finish(null, "ehlo_failed");
}
break;
case SMTPStep.helo:
if (code.startsWith("250")) {
nextStep();
} else {
finish(null, "helo_failed");
}
break;
case SMTPStep.startTls:
if (code.startsWith("220")) {
const plainSocket = socket;
socket = tls.connect({
...tlsOptions,
socket: plainSocket,
servername: isIPAddress(mxHost) ? void 0 : mxHost
}, () => {
isTLS = true;
log("TLS upgraded");
buffer = "";
const starttlsIndex = activeSequence.steps.indexOf(SMTPStep.startTls);
currentStepIndex = starttlsIndex;
nextStep();
});
socket.on("data", handleData);
socket.on("error", () => finish(null, "tls_error"));
} else {
nextStep();
}
break;
case SMTPStep.mailFrom:
if (code.startsWith("250")) {
nextStep();
} else {
finish(null, "mail_from_rejected");
}
break;
case SMTPStep.rcptTo:
if (code.startsWith("250") || code.startsWith("251")) {
finish(true, "valid");
} else if (code.startsWith("552") || code.startsWith("452")) {
finish(false, "over_quota");
} else if (code.startsWith("4")) {
finish(null, "temporary_failure");
} else if (useVRFY && supportsVRFY && code.startsWith("5") && activeSequence.steps.includes(SMTPStep.vrfy)) {
currentStepIndex = activeSequence.steps.indexOf(SMTPStep.vrfy);
executeStep(SMTPStep.vrfy);
} else {
finish(null, "ambiguous");
}
break;