@emailcheck/email-validator-js 3.4.4 → 4.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -2,6 +2,7 @@ import { lru } from 'tiny-lru';
2
2
  import { isValid, parse } from 'psl';
3
3
  import { stringSimilarity } from 'string-similarity-js';
4
4
  import { promises } from 'node:dns';
5
+ import { randomBytes } from 'node:crypto';
5
6
  import * as net$1 from 'node:net';
6
7
  import * as tls from 'node:tls';
7
8
 
@@ -174,6 +175,7 @@ var SMTPStep;
174
175
  SMTPStep2["greeting"] = "GREETING";
175
176
  SMTPStep2["ehlo"] = "EHLO";
176
177
  SMTPStep2["helo"] = "HELO";
178
+ SMTPStep2["startTls"] = "STARTTLS";
177
179
  SMTPStep2["mailFrom"] = "MAIL_FROM";
178
180
  SMTPStep2["rcptTo"] = "RCPT_TO";
179
181
  })(SMTPStep || (SMTPStep = {}));
@@ -658,9 +660,9 @@ function parseCompositeNamePart(part) {
658
660
  cleaned = pureAlpha;
659
661
  confidence = 0.6;
660
662
  } else {
661
- const baseMatch2 = part.match(/^([a-zA-Z]+)\d*$/);
662
- if (baseMatch2) {
663
- cleaned = baseMatch2[1];
663
+ const baseMatch = part.match(/^([a-zA-Z]+)\d*$/);
664
+ if (baseMatch) {
665
+ cleaned = baseMatch[1];
664
666
  confidence = 0.75;
665
667
  } else {
666
668
  cleaned = part;
@@ -674,9 +676,7 @@ function parseCompositeNamePart(part) {
674
676
  confidence = Math.min(1, confidence + 0.2);
675
677
  }
676
678
  }
677
- const baseMatch = part.match(/^([a-zA-Z]+[a-zA-Z0-9]*?)\d*$/);
678
- const base = baseMatch ? baseMatch[1] : part;
679
- return { base, hasNumbers, cleaned, confidence };
679
+ return { hasNumbers, cleaned, confidence };
680
680
  }
681
681
  function isLikelyName(str, allowNumbers = false, allowSingleLetter = false) {
682
682
  if (!str)
@@ -967,6 +967,9 @@ function parseDsn(reply) {
967
967
  return null;
968
968
  return { class: Number(match[1]), subject: Number(match[2]), detail: Number(match[3]) };
969
969
  }
970
+ function dsnToString(dsn) {
971
+ return `${dsn.class}.${dsn.subject}.${dsn.detail}`;
972
+ }
970
973
  function isPolicyBlock(reply) {
971
974
  const dsn = parseDsn(reply);
972
975
  return (dsn === null || dsn === void 0 ? void 0 : dsn.class) === 5 && (dsn === null || dsn === void 0 ? void 0 : dsn.subject) === 7;
@@ -987,8 +990,11 @@ function isInvalidMailboxError(reply) {
987
990
  return false;
988
991
  return true;
989
992
  }
993
+ function defaultProbeLocal() {
994
+ return `${randomBytes(8).toString("hex")}-noexist`;
995
+ }
990
996
  async function verifyMailboxSMTP(params) {
991
- var _a, _b, _c, _d, _e, _f, _g;
997
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
992
998
  const { local, domain, options = {} } = params;
993
999
  const mxRecords = (_a = params.mxRecords) !== null && _a !== void 0 ? _a : [];
994
1000
  const ports = ((_b = options.ports) !== null && _b !== void 0 ? _b : DEFAULT_PORTS).filter((port) => Number.isInteger(port) && port > 0 && port < 65536);
@@ -1001,16 +1007,30 @@ async function verifyMailboxSMTP(params) {
1001
1007
  const cache = options.cache;
1002
1008
  const log = debug ? (...args) => console.log("[SMTP]", ...args) : () => {
1003
1009
  };
1004
- const mxHost = mxRecords[0];
1005
- if (!mxHost) {
1010
+ const startedAtMs = Date.now();
1011
+ const primaryMx = mxRecords[0];
1012
+ if (!primaryMx) {
1006
1013
  log("No MX records found");
1007
- return { smtpResult: failureResult("No MX records found"), cached: false, port: 0, portCached: false };
1014
+ const metrics2 = makeMetrics([], 0, 0, void 0, startedAtMs);
1015
+ return { smtpResult: failureResult("no_mx_records", metrics2), cached: false, port: 0, portCached: false };
1008
1016
  }
1009
- log(`Verifying ${local}@${domain} via ${mxHost}`);
1017
+ log(`Verifying ${local}@${domain} via ${primaryMx} (mx count=${mxRecords.length})`);
1010
1018
  const transcript = [];
1011
1019
  const commands = [];
1020
+ const probeOptions = {
1021
+ local,
1022
+ domain,
1023
+ timeout,
1024
+ tlsConfig,
1025
+ hostname,
1026
+ sequence,
1027
+ log,
1028
+ catchAllProbeLocal: options.catchAllProbeLocal,
1029
+ pipelining: (_h = options.pipelining) !== null && _h !== void 0 ? _h : "auto",
1030
+ startTls: (_j = options.startTls) !== null && _j !== void 0 ? _j : "auto"
1031
+ };
1012
1032
  const verdictCache = cache ? getCacheStore(cache, "smtp") : null;
1013
- const verdictKey = `${mxHost}:${local}@${domain}`;
1033
+ const verdictKey = `${primaryMx}:${local}@${domain}`;
1014
1034
  if (verdictCache) {
1015
1035
  const cachedResult = await safeCacheGet(verdictCache, verdictKey);
1016
1036
  if (cachedResult) {
@@ -1019,81 +1039,99 @@ async function verifyMailboxSMTP(params) {
1019
1039
  }
1020
1040
  }
1021
1041
  const portCache = cache ? getCacheStore(cache, "smtpPort") : null;
1022
- if (portCache) {
1023
- const cachedPort = await safeCacheGet(portCache, mxHost);
1024
- if (cachedPort) {
1025
- log(`Using cached port: ${cachedPort}`);
1026
- const probe = await runProbe({
1027
- mxHost,
1028
- port: cachedPort,
1029
- local,
1030
- domain,
1031
- timeout,
1032
- tlsConfig,
1033
- hostname,
1034
- sequence,
1035
- log
1036
- });
1037
- collectTranscript(transcript, commands, probe, cachedPort);
1038
- const smtpResult = toSmtpVerificationResult(probe.result, captureTranscript ? { transcript, commands } : void 0);
1039
- await safeCacheSet(verdictCache, verdictKey, smtpResult);
1040
- return { smtpResult, cached: false, port: cachedPort, portCached: true };
1041
- }
1042
- }
1043
- for (const port of ports) {
1044
- log(`Testing port ${port}`);
1045
- const probe = await runProbe({ mxHost, port, local, domain, timeout, tlsConfig, hostname, sequence, log });
1046
- collectTranscript(transcript, commands, probe, port);
1047
- const smtpResult = toSmtpVerificationResult(probe.result, captureTranscript ? { transcript, commands } : void 0);
1048
- await safeCacheSet(verdictCache, verdictKey, smtpResult);
1049
- if (probe.result !== null) {
1050
- await safeCacheSet(portCache, mxHost, port);
1051
- return { smtpResult, cached: false, port, portCached: false };
1042
+ const cachedPort = portCache ? await safeCacheGet(portCache, primaryMx) : null;
1043
+ const mxHostsTried = [];
1044
+ let mxAttempts = 0;
1045
+ let portAttempts = 0;
1046
+ let lastReason = "all_attempts_failed";
1047
+ let lastEnhancedStatus;
1048
+ let lastResponseCode;
1049
+ for (const mxHost of mxRecords) {
1050
+ mxHostsTried.push(mxHost);
1051
+ mxAttempts++;
1052
+ const portsForThisMx = mxHost === primaryMx && cachedPort ? [cachedPort, ...ports.filter((p) => p !== cachedPort)] : ports;
1053
+ for (const port of portsForThisMx) {
1054
+ portAttempts++;
1055
+ log(`Testing ${mxHost}:${port}`);
1056
+ const probe = await runProbe({ ...probeOptions, mxHost, port });
1057
+ collectTranscript(transcript, commands, probe, mxHost, port);
1058
+ lastReason = probe.reason;
1059
+ if (probe.enhancedStatus !== void 0)
1060
+ lastEnhancedStatus = probe.enhancedStatus;
1061
+ if (probe.responseCode !== void 0)
1062
+ lastResponseCode = probe.responseCode;
1063
+ if (probe.result !== null) {
1064
+ const metrics2 = makeMetrics(mxHostsTried, mxAttempts, portAttempts, mxHost, startedAtMs);
1065
+ const smtpResult2 = toSmtpVerificationResult(probe, {
1066
+ transcript: captureTranscript ? transcript : void 0,
1067
+ commands: captureTranscript ? commands : void 0,
1068
+ metrics: metrics2
1069
+ });
1070
+ await safeCacheSet(verdictCache, verdictKey, smtpResult2);
1071
+ if (mxHost === primaryMx)
1072
+ await safeCacheSet(portCache, primaryMx, port);
1073
+ return { smtpResult: smtpResult2, cached: false, port, portCached: cachedPort === port };
1074
+ }
1052
1075
  }
1053
1076
  }
1054
- log("All ports failed");
1077
+ log(`All MX\xD7port attempts failed (mx=${mxAttempts}, port=${portAttempts})`);
1078
+ const metrics = makeMetrics(mxHostsTried, mxAttempts, portAttempts, void 0, startedAtMs);
1079
+ const smtpResult = {
1080
+ ...failureResult(lastReason, metrics),
1081
+ ...lastEnhancedStatus !== void 0 ? { enhancedStatus: lastEnhancedStatus } : {},
1082
+ ...lastResponseCode !== void 0 ? { responseCode: lastResponseCode } : {},
1083
+ ...captureTranscript ? { transcript: [...transcript], commands: [...commands] } : {}
1084
+ };
1085
+ return { smtpResult, cached: false, port: 0, portCached: false };
1086
+ }
1087
+ function makeMetrics(mxHostsTried, mxAttempts, portAttempts, mxHostUsed, startedAtMs) {
1055
1088
  return {
1056
- smtpResult: {
1057
- ...failureResult("All SMTP connection attempts failed"),
1058
- ...captureTranscript ? { transcript: [...transcript], commands: [...commands] } : {}
1059
- },
1060
- cached: false,
1061
- port: 0,
1062
- portCached: false
1089
+ mxAttempts,
1090
+ portAttempts,
1091
+ mxHostsTried: [...mxHostsTried],
1092
+ ...mxHostUsed !== void 0 ? { mxHostUsed } : {},
1093
+ totalDurationMs: Date.now() - startedAtMs
1063
1094
  };
1064
1095
  }
1065
- function collectTranscript(transcript, commands, probe, port) {
1096
+ function collectTranscript(transcript, commands, probe, mxHost, port) {
1097
+ const prefix = `${mxHost}:${port}`;
1066
1098
  for (const line of probe.transcript)
1067
- transcript.push(`${port}|s| ${line}`);
1099
+ transcript.push(`${prefix}|s| ${line}`);
1068
1100
  for (const cmd of probe.commands)
1069
- commands.push(`${port}|c| ${cmd}`);
1101
+ commands.push(`${prefix}|c| ${cmd}`);
1070
1102
  }
1071
- function failureResult(error) {
1103
+ function failureResult(reason, metrics) {
1072
1104
  return {
1073
1105
  canConnectSmtp: false,
1074
1106
  hasFullInbox: false,
1075
1107
  isCatchAll: false,
1076
1108
  isDeliverable: false,
1077
1109
  isDisabled: false,
1078
- error,
1079
- providerUsed: EmailProvider.everythingElse,
1080
- checkedAt: Date.now()
1110
+ error: reason,
1111
+ checkedAt: Date.now(),
1112
+ metrics
1081
1113
  };
1082
1114
  }
1083
- function toSmtpVerificationResult(result, capture) {
1084
- const base = {
1115
+ function toSmtpVerificationResult(probe, extras) {
1116
+ var _a;
1117
+ const result = probe.result;
1118
+ const out = {
1085
1119
  canConnectSmtp: result !== null,
1086
- hasFullInbox: false,
1087
- isCatchAll: false,
1120
+ hasFullInbox: probe.reason === "over_quota",
1121
+ isCatchAll: (_a = probe.isCatchAll) !== null && _a !== void 0 ? _a : false,
1088
1122
  isDeliverable: result === true,
1089
1123
  isDisabled: result === false,
1090
- error: result === true ? void 0 : result === null ? "ambiguous" : "not_found",
1091
- providerUsed: EmailProvider.everythingElse,
1092
- checkedAt: Date.now()
1124
+ error: result === true ? void 0 : probe.reason,
1125
+ checkedAt: Date.now(),
1126
+ metrics: extras.metrics,
1127
+ ...probe.enhancedStatus !== void 0 ? { enhancedStatus: probe.enhancedStatus } : {},
1128
+ ...probe.responseCode !== void 0 ? { responseCode: probe.responseCode } : {}
1093
1129
  };
1094
- if (!capture)
1095
- return base;
1096
- return { ...base, transcript: [...capture.transcript], commands: [...capture.commands] };
1130
+ if (extras.transcript)
1131
+ out.transcript = [...extras.transcript];
1132
+ if (extras.commands)
1133
+ out.commands = [...extras.commands];
1134
+ return out;
1097
1135
  }
1098
1136
  async function safeCacheGet(store, key) {
1099
1137
  if (!store)
@@ -1123,8 +1161,17 @@ class SMTPProbeConnection {
1123
1161
  this.buffer = "";
1124
1162
  this.resolved = false;
1125
1163
  this.currentStepIndex = 0;
1164
+ this.tlsUpgrading = false;
1165
+ this.postUpgradeReEhlo = false;
1126
1166
  this.transcript = [];
1127
1167
  this.commands = [];
1168
+ this.supportsPipelining = false;
1169
+ this.supportsStartTls = false;
1170
+ this.dualPhase = "idle";
1171
+ this.realOutcome = "pending";
1172
+ this.probeOutcome = "pending";
1173
+ this.dualPipelined = false;
1174
+ this.pendingDecision = null;
1128
1175
  this.onData = (data) => {
1129
1176
  if (this.resolved)
1130
1177
  return;
@@ -1137,7 +1184,7 @@ class SMTPProbeConnection {
1137
1184
  this.processLine(line);
1138
1185
  }
1139
1186
  };
1140
- const defaultSteps = [SMTPStep.greeting, SMTPStep.ehlo, SMTPStep.mailFrom, SMTPStep.rcptTo];
1187
+ const defaultSteps = [SMTPStep.greeting, SMTPStep.ehlo, SMTPStep.startTls, SMTPStep.mailFrom, SMTPStep.rcptTo];
1141
1188
  this.steps = [...(_b = (_a = p.sequence) === null || _a === void 0 ? void 0 : _a.steps) !== null && _b !== void 0 ? _b : defaultSteps];
1142
1189
  this.isTLS = PORT_TLS[p.port] === true;
1143
1190
  const servername = isIPAddress(p.mxHost) ? void 0 : p.mxHost;
@@ -1148,6 +1195,7 @@ class SMTPProbeConnection {
1148
1195
  minVersion: "TLSv1.2",
1149
1196
  ...typeof p.tlsConfig === "object" ? p.tlsConfig : {}
1150
1197
  };
1198
+ this.probeLocal = p.catchAllProbeLocal ? p.catchAllProbeLocal(p.local, p.domain) : defaultProbeLocal();
1151
1199
  }
1152
1200
  run() {
1153
1201
  return new Promise((resolve) => {
@@ -1156,7 +1204,8 @@ class SMTPProbeConnection {
1156
1204
  this.connect();
1157
1205
  this.armConnectionTimer();
1158
1206
  } catch (error) {
1159
- this.finish(null, `connect_throw:${error instanceof Error ? error.message : "unknown"}`);
1207
+ this.p.log(`connect threw: ${error instanceof Error ? error.message : "unknown"}`);
1208
+ this.finish(null, "connection_error");
1160
1209
  }
1161
1210
  });
1162
1211
  }
@@ -1208,51 +1257,173 @@ class SMTPProbeConnection {
1208
1257
  switch (step) {
1209
1258
  case SMTPStep.greeting:
1210
1259
  return;
1260
+ // server-driven; nothing to send
1211
1261
  case SMTPStep.ehlo:
1212
1262
  this.send(`EHLO ${this.p.hostname}`);
1213
1263
  return;
1214
1264
  case SMTPStep.helo:
1215
1265
  this.send(`HELO ${this.p.hostname}`);
1216
1266
  return;
1267
+ case SMTPStep.startTls:
1268
+ this.executeStartTls();
1269
+ return;
1217
1270
  case SMTPStep.mailFrom: {
1218
1271
  const from = (_b = (_a = this.p.sequence) === null || _a === void 0 ? void 0 : _a.from) !== null && _b !== void 0 ? _b : `<${this.p.local}@${this.p.domain}>`;
1219
1272
  this.send(`MAIL FROM:${from}`);
1220
1273
  return;
1221
1274
  }
1222
1275
  case SMTPStep.rcptTo:
1223
- this.send(`RCPT TO:<${this.p.local}@${this.p.domain}>`);
1276
+ this.executeEnvelope();
1224
1277
  return;
1225
1278
  }
1226
1279
  }
1227
- processLine(line) {
1228
- if (this.resolved)
1280
+ /**
1281
+ * Conditional STARTTLS upgrade. Skipped (advances to next step) when:
1282
+ * - already TLS (implicit-TLS port like 465 or already-upgraded)
1283
+ * - `startTls === 'never'`
1284
+ * - `startTls === 'auto'` AND the MX didn't advertise STARTTLS in EHLO
1285
+ *
1286
+ * Sends `STARTTLS` and waits for 220 when:
1287
+ * - `startTls === 'force'` (regardless of advertisement)
1288
+ * - `startTls === 'auto'` AND the MX advertised it
1289
+ *
1290
+ * On 220, `tls.connect()` wraps the existing socket. After the handshake
1291
+ * we re-EHLO (mandatory per RFC 3207 §4.2 — pre-TLS state must be
1292
+ * discarded) before continuing to MAIL FROM.
1293
+ */
1294
+ executeStartTls() {
1295
+ const mode = this.p.startTls;
1296
+ const wantsUpgrade = !this.isTLS && mode !== "never" && (mode === "force" || mode === "auto" && this.supportsStartTls);
1297
+ if (!wantsUpgrade) {
1298
+ this.nextStep();
1229
1299
  return;
1230
- this.transcript.push(line);
1231
- this.p.log(`\u2190 ${line}`);
1232
- if (isHighVolume(line)) {
1233
- this.finish(true, "high_volume");
1300
+ }
1301
+ this.send("STARTTLS");
1302
+ }
1303
+ /**
1304
+ * Wrap the plaintext socket with TLS in place. Called after the server
1305
+ * answers our STARTTLS with 220. Detaches the plaintext-socket listeners
1306
+ * (TLS owns the underlying transport now), re-installs them on the wrapped
1307
+ * socket, resets EHLO-derived capabilities, and re-issues EHLO once the
1308
+ * handshake completes (RFC 3207 §4.2 mandates re-EHLO after upgrade —
1309
+ * pre-TLS state must be discarded).
1310
+ */
1311
+ upgradeToTls() {
1312
+ const plainSocket = this.socket;
1313
+ if (!plainSocket) {
1314
+ this.finish(null, "tls_upgrade_failed");
1234
1315
  return;
1235
1316
  }
1236
- if (isOverQuota(line)) {
1237
- this.finish(false, "over_quota");
1317
+ const detach = plainSocket.removeAllListeners;
1318
+ if (typeof detach === "function") {
1319
+ detach.call(plainSocket, "data");
1320
+ detach.call(plainSocket, "error");
1321
+ detach.call(plainSocket, "close");
1322
+ detach.call(plainSocket, "timeout");
1323
+ }
1324
+ try {
1325
+ plainSocket.setTimeout(0);
1326
+ } catch {
1327
+ }
1328
+ this.tlsUpgrading = true;
1329
+ const servername = isIPAddress(this.p.mxHost) ? void 0 : this.p.mxHost;
1330
+ const tlsSocket = tls.connect({ ...this.tlsOptions, socket: plainSocket, servername }, () => {
1331
+ this.tlsUpgrading = false;
1332
+ this.isTLS = true;
1333
+ this.buffer = "";
1334
+ this.supportsStartTls = false;
1335
+ this.supportsPipelining = false;
1336
+ this.postUpgradeReEhlo = true;
1337
+ this.send(`EHLO ${this.p.hostname}`);
1338
+ });
1339
+ this.socket = tlsSocket;
1340
+ this.socket.on("data", this.onData);
1341
+ this.socket.on("error", () => {
1342
+ this.finish(null, this.tlsUpgrading ? "tls_handshake_failed" : "connection_error");
1343
+ });
1344
+ this.socket.on("close", () => this.finish(null, "connection_closed"));
1345
+ this.socket.setTimeout(this.p.timeout, () => this.finish(null, "socket_timeout"));
1346
+ }
1347
+ /**
1348
+ * Send the dual-probe envelope (real RCPT + probe RCPT + RSET). Pipelined
1349
+ * when the MX advertised PIPELINING (or `pipelining: 'force'`); sequential
1350
+ * otherwise.
1351
+ */
1352
+ executeEnvelope() {
1353
+ var _a;
1354
+ const wantsPipelining = this.p.pipelining === "force" || this.p.pipelining === "auto" && this.supportsPipelining;
1355
+ const realCmd = `RCPT TO:<${this.p.local}@${this.p.domain}>`;
1356
+ if (wantsPipelining) {
1357
+ this.dualPipelined = true;
1358
+ const probeCmd = `RCPT TO:<${this.probeLocal}@${this.p.domain}>`;
1359
+ const rsetCmd = "RSET";
1360
+ this.commands.push(realCmd, probeCmd, rsetCmd);
1361
+ this.p.log(`\u2192 ${realCmd}`);
1362
+ this.p.log(`\u2192 ${probeCmd}`);
1363
+ this.p.log(`\u2192 ${rsetCmd}`);
1364
+ (_a = this.socket) === null || _a === void 0 ? void 0 : _a.write(`${realCmd}\r
1365
+ ${probeCmd}\r
1366
+ ${rsetCmd}\r
1367
+ `);
1368
+ this.dualPhase = "rcpt_real";
1238
1369
  return;
1239
1370
  }
1240
- if (isInvalidMailboxError(line)) {
1241
- this.finish(false, "not_found");
1371
+ this.dualPipelined = false;
1372
+ this.send(realCmd);
1373
+ this.dualPhase = "rcpt_real";
1374
+ }
1375
+ processLine(line) {
1376
+ if (this.resolved)
1242
1377
  return;
1378
+ this.transcript.push(line);
1379
+ this.p.log(`\u2190 ${line}`);
1380
+ const codeStr = line.slice(0, 3);
1381
+ const numericCode = /^\d{3}$/.test(codeStr) ? parseInt(codeStr, 10) : null;
1382
+ if (numericCode !== null)
1383
+ this.lastResponseCode = numericCode;
1384
+ const dsn = parseDsn(line);
1385
+ if (dsn)
1386
+ this.lastEnhancedStatus = dsnToString(dsn);
1387
+ if (this.dualPhase === "idle" || this.dualPhase === "rcpt_real") {
1388
+ if (isHighVolume(line)) {
1389
+ this.finish(true, "high_volume");
1390
+ return;
1391
+ }
1392
+ if (isOverQuota(line)) {
1393
+ this.isCatchAllFlag = false;
1394
+ this.finish(false, "over_quota");
1395
+ return;
1396
+ }
1397
+ if (isInvalidMailboxError(line)) {
1398
+ this.isCatchAllFlag = false;
1399
+ this.finish(false, "not_found");
1400
+ return;
1401
+ }
1243
1402
  }
1244
- if (MULTILINE_RE.test(line))
1403
+ if (MULTILINE_RE.test(line)) {
1404
+ const step = this.steps[this.currentStepIndex];
1405
+ const isEhloLike = step === SMTPStep.ehlo || step === SMTPStep.helo || step === SMTPStep.startTls && this.postUpgradeReEhlo;
1406
+ if (isEhloLike && line.startsWith("250-")) {
1407
+ const upper = line.toUpperCase();
1408
+ if (upper.includes("PIPELINING"))
1409
+ this.supportsPipelining = true;
1410
+ if (upper.includes("STARTTLS"))
1411
+ this.supportsStartTls = true;
1412
+ }
1245
1413
  return;
1246
- const code = line.slice(0, 3);
1247
- const numericCode = /^\d{3}$/.test(code) ? parseInt(code, 10) : null;
1414
+ }
1248
1415
  if (numericCode === null) {
1249
1416
  this.finish(null, "unrecognized_response");
1250
1417
  return;
1251
1418
  }
1252
- this.dispatch(numericCode);
1419
+ this.dispatch(numericCode, line);
1253
1420
  }
1254
- dispatch(code) {
1421
+ dispatch(code, line) {
1255
1422
  const step = this.steps[this.currentStepIndex];
1423
+ if (this.dualPhase !== "idle" && step === SMTPStep.rcptTo) {
1424
+ this.handleEnvelopeReply(code, line);
1425
+ return;
1426
+ }
1256
1427
  switch (step) {
1257
1428
  case SMTPStep.greeting:
1258
1429
  if (code === 220)
@@ -1272,6 +1443,21 @@ class SMTPProbeConnection {
1272
1443
  else
1273
1444
  this.finish(null, "helo_failed");
1274
1445
  return;
1446
+ case SMTPStep.startTls:
1447
+ if (this.postUpgradeReEhlo) {
1448
+ this.postUpgradeReEhlo = false;
1449
+ if (code === 250)
1450
+ this.nextStep();
1451
+ else
1452
+ this.finish(null, "ehlo_failed");
1453
+ return;
1454
+ }
1455
+ if (code === 220) {
1456
+ this.upgradeToTls();
1457
+ } else {
1458
+ this.finish(null, "tls_upgrade_failed");
1459
+ }
1460
+ return;
1275
1461
  case SMTPStep.mailFrom:
1276
1462
  if (code === 250)
1277
1463
  this.nextStep();
@@ -1279,15 +1465,91 @@ class SMTPProbeConnection {
1279
1465
  this.finish(null, "mail_from_rejected");
1280
1466
  return;
1281
1467
  case SMTPStep.rcptTo:
1282
- if (code === 250 || code === 251)
1283
- this.finish(true, "valid");
1284
- else if (code === 552 || code === 452)
1285
- this.finish(false, "over_quota");
1286
- else if (code >= 400 && code < 500)
1287
- this.finish(null, "temporary_failure");
1288
- else
1289
- this.finish(null, "ambiguous");
1468
+ this.handleEnvelopeReply(code, line);
1469
+ return;
1470
+ }
1471
+ }
1472
+ /**
1473
+ * Dual-probe / pipelined-envelope reply router. Demuxes server replies for
1474
+ * the three queued commands (real RCPT, probe RCPT, RSET) and resolves
1475
+ * with the catch-all-aware verdict.
1476
+ */
1477
+ handleEnvelopeReply(code, line) {
1478
+ if (this.dualPhase === "rcpt_real") {
1479
+ this.realOutcome = classifyRcpt(code);
1480
+ if (code === 552 || code === 452 || isOverQuota(line)) {
1481
+ this.isCatchAllFlag = false;
1482
+ if (this.dualPipelined) {
1483
+ this.pendingDecision = { result: false, reason: "over_quota" };
1484
+ this.dualPhase = "rcpt_probe";
1485
+ return;
1486
+ }
1487
+ this.finish(false, "over_quota");
1488
+ return;
1489
+ }
1490
+ if (this.realOutcome === "soft_reject") {
1491
+ if (this.dualPipelined) {
1492
+ this.pendingDecision = { result: null, reason: "temporary_failure" };
1493
+ this.dualPhase = "rcpt_probe";
1494
+ return;
1495
+ }
1496
+ this.finish(null, "temporary_failure");
1497
+ return;
1498
+ }
1499
+ if (this.realOutcome === "hard_reject") {
1500
+ const reason = isInvalidMailboxError(line) ? "not_found" : "ambiguous";
1501
+ const result = reason === "not_found" ? false : null;
1502
+ this.isCatchAllFlag = false;
1503
+ if (this.dualPipelined) {
1504
+ this.pendingDecision = { result, reason };
1505
+ this.dualPhase = "rcpt_probe";
1506
+ return;
1507
+ }
1508
+ this.finish(result, reason);
1290
1509
  return;
1510
+ }
1511
+ if (this.dualPipelined) {
1512
+ this.dualPhase = "rcpt_probe";
1513
+ } else {
1514
+ this.send(`RCPT TO:<${this.probeLocal}@${this.p.domain}>`);
1515
+ this.dualPhase = "rcpt_probe";
1516
+ }
1517
+ return;
1518
+ }
1519
+ if (this.dualPhase === "rcpt_probe") {
1520
+ this.probeOutcome = classifyRcpt(code);
1521
+ if (this.dualPipelined) {
1522
+ this.dualPhase = "rset";
1523
+ } else {
1524
+ this.send("RSET");
1525
+ this.dualPhase = "rset";
1526
+ }
1527
+ return;
1528
+ }
1529
+ if (this.dualPhase === "rset") {
1530
+ if (this.pendingDecision) {
1531
+ this.finish(this.pendingDecision.result, this.pendingDecision.reason);
1532
+ return;
1533
+ }
1534
+ this.decideDualProbe();
1535
+ return;
1536
+ }
1537
+ }
1538
+ /** Final decision after both RCPT outcomes are known. Catch-all only when both 250. */
1539
+ decideDualProbe() {
1540
+ if (this.realOutcome === "accept" && this.probeOutcome === "accept") {
1541
+ this.isCatchAllFlag = true;
1542
+ this.finish(true, "valid");
1543
+ } else if (this.realOutcome === "accept") {
1544
+ this.isCatchAllFlag = false;
1545
+ this.finish(true, "valid");
1546
+ } else if (this.realOutcome === "hard_reject") {
1547
+ this.isCatchAllFlag = false;
1548
+ this.finish(false, "not_found");
1549
+ } else if (this.realOutcome === "soft_reject") {
1550
+ this.finish(null, "temporary_failure");
1551
+ } else {
1552
+ this.finish(null, "ambiguous");
1291
1553
  }
1292
1554
  }
1293
1555
  finish(result, reason) {
@@ -1313,9 +1575,24 @@ class SMTPProbeConnection {
1313
1575
  return (_a2 = this.socket) === null || _a2 === void 0 ? void 0 : _a2.destroy();
1314
1576
  }, QUIT_DRAIN_MS);
1315
1577
  (_c = drain.unref) === null || _c === void 0 ? void 0 : _c.call(drain);
1316
- this.resolveFn({ result, transcript: this.transcript, commands: this.commands });
1578
+ this.resolveFn({
1579
+ result,
1580
+ reason,
1581
+ ...this.lastEnhancedStatus !== void 0 ? { enhancedStatus: this.lastEnhancedStatus } : {},
1582
+ ...this.lastResponseCode !== void 0 ? { responseCode: this.lastResponseCode } : {},
1583
+ ...this.isCatchAllFlag !== void 0 ? { isCatchAll: this.isCatchAllFlag } : {},
1584
+ transcript: this.transcript,
1585
+ commands: this.commands
1586
+ });
1317
1587
  }
1318
1588
  }
1589
+ function classifyRcpt(code) {
1590
+ if (code === 250 || code === 251)
1591
+ return "accept";
1592
+ if (code >= 400 && code < 500)
1593
+ return "soft_reject";
1594
+ return "hard_reject";
1595
+ }
1319
1596
 
1320
1597
  class ArrayTranscriptCollector {
1321
1598
  constructor() {
@@ -1988,12 +2265,11 @@ async function verifyEmail(params) {
1988
2265
  const skipMx = ((_f = params.skipMxForDisposable) !== null && _f !== void 0 ? _f : false) && result.isDisposable;
1989
2266
  const skipWhois = ((_g = params.skipDomainWhoisForDisposable) !== null && _g !== void 0 ? _g : false) && result.isDisposable;
1990
2267
  await runWhoisChecks(domain, params, result, skipWhois, log, collector);
1991
- if (((_h = params.verifyMx) !== null && _h !== void 0 ? _h : true) || ((_j = params.verifySmtp) !== null && _j !== void 0 ? _j : false)) {
1992
- if (skipMx) {
1993
- log(`[verifyEmail] skipping MX/SMTP for disposable: ${params.emailAddress}`);
1994
- } else {
1995
- await runMxAndSmtp(local, domain, params, result, log, collector);
1996
- }
2268
+ const wantsMxOrSmtp = ((_h = params.verifyMx) !== null && _h !== void 0 ? _h : true) || ((_j = params.verifySmtp) !== null && _j !== void 0 ? _j : false);
2269
+ if (wantsMxOrSmtp && skipMx) {
2270
+ log(`[verifyEmail] skipping MX/SMTP for disposable: ${params.emailAddress}`);
2271
+ } else if (wantsMxOrSmtp) {
2272
+ await runMxAndSmtp(local, domain, params, result, log, collector);
1997
2273
  }
1998
2274
  result.metadata.verificationTime = Date.now() - startTime;
1999
2275
  if (captureTranscript)
@@ -2343,6 +2619,36 @@ function isSpamName(name) {
2343
2619
  return true;
2344
2620
  }
2345
2621
 
2622
+ const REFINEMENT_TABLE = {
2623
+ // X.1.x — addressing
2624
+ "5.1.1": "mailbox_does_not_exist",
2625
+ "5.1.2": "bad_destination_system",
2626
+ "5.1.3": "bad_destination_address",
2627
+ "5.1.6": "mailbox_moved",
2628
+ "5.1.10": "recipient_address_has_null_mx",
2629
+ // X.2.x — mailbox status
2630
+ "5.2.0": "mailbox_status_other",
2631
+ "5.2.1": "mailbox_disabled",
2632
+ "5.2.2": "mailbox_full",
2633
+ "5.2.3": "message_too_long",
2634
+ "5.2.4": "mailing_list_expansion_problem",
2635
+ // X.4.x — network / routing
2636
+ "4.4.1": "no_answer_from_host",
2637
+ "4.4.2": "bad_connection",
2638
+ // X.7.x — security / policy
2639
+ "5.7.0": "security_other",
2640
+ "5.7.1": "delivery_not_authorized",
2641
+ "5.7.25": "no_reverse_dns",
2642
+ "5.7.26": "multiple_authentication_failures"
2643
+ };
2644
+ function refineReasonByEnhancedStatus(reason, enhancedStatus) {
2645
+ var _a;
2646
+ const base = reason !== null && reason !== void 0 ? reason : "unknown";
2647
+ if (!enhancedStatus)
2648
+ return base;
2649
+ return (_a = REFINEMENT_TABLE[enhancedStatus]) !== null && _a !== void 0 ? _a : base;
2650
+ }
2651
+
2346
2652
  const NETWORK_ERROR_PATTERNS = [
2347
2653
  "etimedout",
2348
2654
  "econnrefused",
@@ -2446,5 +2752,5 @@ function parseSmtpError(errorMessage) {
2446
2752
  return { isDisabled, hasFullInbox, isCatchAll, isInvalid };
2447
2753
  }
2448
2754
 
2449
- export { ArrayTranscriptCollector, DEFAULT_CACHE_OPTIONS, EmailProvider, LRUAdapter, NULL_COLLECTOR, RedisAdapter, SMTPStep, VerificationErrorCode, cleanNameForAlgorithm, clearDefaultCache, commonEmailDomains, defaultDomainSuggestionMethod, defaultNameDetectionMethod, detectName, detectNameForAlgorithm, detectNameFromEmail, domainPorts, getCacheStore, getDefaultCache, getDomainAge, getDomainRegistrationStatus, getDomainSimilarity, isCommonDomain, isDisposableEmail, isFreeEmail, isSpamEmail, isSpamName, isValidEmail, isValidEmailDomain, parseSmtpError, suggestDomain, suggestEmailDomain, verifyEmail, verifyEmailBatch };
2755
+ export { ArrayTranscriptCollector, DEFAULT_CACHE_OPTIONS, EmailProvider, LRUAdapter, NULL_COLLECTOR, RedisAdapter, SMTPStep, VerificationErrorCode, cleanNameForAlgorithm, clearDefaultCache, commonEmailDomains, defaultDomainSuggestionMethod, defaultNameDetectionMethod, detectName, detectNameForAlgorithm, detectNameFromEmail, domainPorts, getCacheStore, getDefaultCache, getDomainAge, getDomainRegistrationStatus, getDomainSimilarity, isCommonDomain, isDisposableEmail, isFreeEmail, isSpamEmail, isSpamName, isValidEmail, isValidEmailDomain, parseSmtpError, refineReasonByEnhancedStatus, suggestDomain, suggestEmailDomain, verifyEmail, verifyEmailBatch };
2450
2756
  //# sourceMappingURL=index.esm.js.map