@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.js CHANGED
@@ -4,6 +4,7 @@ var tinyLru = require('tiny-lru');
4
4
  var psl = require('psl');
5
5
  var stringSimilarityJs = require('string-similarity-js');
6
6
  var node_dns = require('node:dns');
7
+ var node_crypto = require('node:crypto');
7
8
  var net$1 = require('node:net');
8
9
  var tls = require('node:tls');
9
10
 
@@ -196,6 +197,7 @@ exports.SMTPStep = void 0;
196
197
  SMTPStep2["greeting"] = "GREETING";
197
198
  SMTPStep2["ehlo"] = "EHLO";
198
199
  SMTPStep2["helo"] = "HELO";
200
+ SMTPStep2["startTls"] = "STARTTLS";
199
201
  SMTPStep2["mailFrom"] = "MAIL_FROM";
200
202
  SMTPStep2["rcptTo"] = "RCPT_TO";
201
203
  })(exports.SMTPStep || (exports.SMTPStep = {}));
@@ -680,9 +682,9 @@ function parseCompositeNamePart(part) {
680
682
  cleaned = pureAlpha;
681
683
  confidence = 0.6;
682
684
  } else {
683
- const baseMatch2 = part.match(/^([a-zA-Z]+)\d*$/);
684
- if (baseMatch2) {
685
- cleaned = baseMatch2[1];
685
+ const baseMatch = part.match(/^([a-zA-Z]+)\d*$/);
686
+ if (baseMatch) {
687
+ cleaned = baseMatch[1];
686
688
  confidence = 0.75;
687
689
  } else {
688
690
  cleaned = part;
@@ -696,9 +698,7 @@ function parseCompositeNamePart(part) {
696
698
  confidence = Math.min(1, confidence + 0.2);
697
699
  }
698
700
  }
699
- const baseMatch = part.match(/^([a-zA-Z]+[a-zA-Z0-9]*?)\d*$/);
700
- const base = baseMatch ? baseMatch[1] : part;
701
- return { base, hasNumbers, cleaned, confidence };
701
+ return { hasNumbers, cleaned, confidence };
702
702
  }
703
703
  function isLikelyName(str, allowNumbers = false, allowSingleLetter = false) {
704
704
  if (!str)
@@ -989,6 +989,9 @@ function parseDsn(reply) {
989
989
  return null;
990
990
  return { class: Number(match[1]), subject: Number(match[2]), detail: Number(match[3]) };
991
991
  }
992
+ function dsnToString(dsn) {
993
+ return `${dsn.class}.${dsn.subject}.${dsn.detail}`;
994
+ }
992
995
  function isPolicyBlock(reply) {
993
996
  const dsn = parseDsn(reply);
994
997
  return (dsn === null || dsn === void 0 ? void 0 : dsn.class) === 5 && (dsn === null || dsn === void 0 ? void 0 : dsn.subject) === 7;
@@ -1009,8 +1012,11 @@ function isInvalidMailboxError(reply) {
1009
1012
  return false;
1010
1013
  return true;
1011
1014
  }
1015
+ function defaultProbeLocal() {
1016
+ return `${node_crypto.randomBytes(8).toString("hex")}-noexist`;
1017
+ }
1012
1018
  async function verifyMailboxSMTP(params) {
1013
- var _a, _b, _c, _d, _e, _f, _g;
1019
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
1014
1020
  const { local, domain, options = {} } = params;
1015
1021
  const mxRecords = (_a = params.mxRecords) !== null && _a !== void 0 ? _a : [];
1016
1022
  const ports = ((_b = options.ports) !== null && _b !== void 0 ? _b : DEFAULT_PORTS).filter((port) => Number.isInteger(port) && port > 0 && port < 65536);
@@ -1023,16 +1029,30 @@ async function verifyMailboxSMTP(params) {
1023
1029
  const cache = options.cache;
1024
1030
  const log = debug ? (...args) => console.log("[SMTP]", ...args) : () => {
1025
1031
  };
1026
- const mxHost = mxRecords[0];
1027
- if (!mxHost) {
1032
+ const startedAtMs = Date.now();
1033
+ const primaryMx = mxRecords[0];
1034
+ if (!primaryMx) {
1028
1035
  log("No MX records found");
1029
- return { smtpResult: failureResult("No MX records found"), cached: false, port: 0, portCached: false };
1036
+ const metrics2 = makeMetrics([], 0, 0, void 0, startedAtMs);
1037
+ return { smtpResult: failureResult("no_mx_records", metrics2), cached: false, port: 0, portCached: false };
1030
1038
  }
1031
- log(`Verifying ${local}@${domain} via ${mxHost}`);
1039
+ log(`Verifying ${local}@${domain} via ${primaryMx} (mx count=${mxRecords.length})`);
1032
1040
  const transcript = [];
1033
1041
  const commands = [];
1042
+ const probeOptions = {
1043
+ local,
1044
+ domain,
1045
+ timeout,
1046
+ tlsConfig,
1047
+ hostname,
1048
+ sequence,
1049
+ log,
1050
+ catchAllProbeLocal: options.catchAllProbeLocal,
1051
+ pipelining: (_h = options.pipelining) !== null && _h !== void 0 ? _h : "auto",
1052
+ startTls: (_j = options.startTls) !== null && _j !== void 0 ? _j : "auto"
1053
+ };
1034
1054
  const verdictCache = cache ? getCacheStore(cache, "smtp") : null;
1035
- const verdictKey = `${mxHost}:${local}@${domain}`;
1055
+ const verdictKey = `${primaryMx}:${local}@${domain}`;
1036
1056
  if (verdictCache) {
1037
1057
  const cachedResult = await safeCacheGet(verdictCache, verdictKey);
1038
1058
  if (cachedResult) {
@@ -1041,81 +1061,99 @@ async function verifyMailboxSMTP(params) {
1041
1061
  }
1042
1062
  }
1043
1063
  const portCache = cache ? getCacheStore(cache, "smtpPort") : null;
1044
- if (portCache) {
1045
- const cachedPort = await safeCacheGet(portCache, mxHost);
1046
- if (cachedPort) {
1047
- log(`Using cached port: ${cachedPort}`);
1048
- const probe = await runProbe({
1049
- mxHost,
1050
- port: cachedPort,
1051
- local,
1052
- domain,
1053
- timeout,
1054
- tlsConfig,
1055
- hostname,
1056
- sequence,
1057
- log
1058
- });
1059
- collectTranscript(transcript, commands, probe, cachedPort);
1060
- const smtpResult = toSmtpVerificationResult(probe.result, captureTranscript ? { transcript, commands } : void 0);
1061
- await safeCacheSet(verdictCache, verdictKey, smtpResult);
1062
- return { smtpResult, cached: false, port: cachedPort, portCached: true };
1063
- }
1064
- }
1065
- for (const port of ports) {
1066
- log(`Testing port ${port}`);
1067
- const probe = await runProbe({ mxHost, port, local, domain, timeout, tlsConfig, hostname, sequence, log });
1068
- collectTranscript(transcript, commands, probe, port);
1069
- const smtpResult = toSmtpVerificationResult(probe.result, captureTranscript ? { transcript, commands } : void 0);
1070
- await safeCacheSet(verdictCache, verdictKey, smtpResult);
1071
- if (probe.result !== null) {
1072
- await safeCacheSet(portCache, mxHost, port);
1073
- return { smtpResult, cached: false, port, portCached: false };
1064
+ const cachedPort = portCache ? await safeCacheGet(portCache, primaryMx) : null;
1065
+ const mxHostsTried = [];
1066
+ let mxAttempts = 0;
1067
+ let portAttempts = 0;
1068
+ let lastReason = "all_attempts_failed";
1069
+ let lastEnhancedStatus;
1070
+ let lastResponseCode;
1071
+ for (const mxHost of mxRecords) {
1072
+ mxHostsTried.push(mxHost);
1073
+ mxAttempts++;
1074
+ const portsForThisMx = mxHost === primaryMx && cachedPort ? [cachedPort, ...ports.filter((p) => p !== cachedPort)] : ports;
1075
+ for (const port of portsForThisMx) {
1076
+ portAttempts++;
1077
+ log(`Testing ${mxHost}:${port}`);
1078
+ const probe = await runProbe({ ...probeOptions, mxHost, port });
1079
+ collectTranscript(transcript, commands, probe, mxHost, port);
1080
+ lastReason = probe.reason;
1081
+ if (probe.enhancedStatus !== void 0)
1082
+ lastEnhancedStatus = probe.enhancedStatus;
1083
+ if (probe.responseCode !== void 0)
1084
+ lastResponseCode = probe.responseCode;
1085
+ if (probe.result !== null) {
1086
+ const metrics2 = makeMetrics(mxHostsTried, mxAttempts, portAttempts, mxHost, startedAtMs);
1087
+ const smtpResult2 = toSmtpVerificationResult(probe, {
1088
+ transcript: captureTranscript ? transcript : void 0,
1089
+ commands: captureTranscript ? commands : void 0,
1090
+ metrics: metrics2
1091
+ });
1092
+ await safeCacheSet(verdictCache, verdictKey, smtpResult2);
1093
+ if (mxHost === primaryMx)
1094
+ await safeCacheSet(portCache, primaryMx, port);
1095
+ return { smtpResult: smtpResult2, cached: false, port, portCached: cachedPort === port };
1096
+ }
1074
1097
  }
1075
1098
  }
1076
- log("All ports failed");
1099
+ log(`All MX\xD7port attempts failed (mx=${mxAttempts}, port=${portAttempts})`);
1100
+ const metrics = makeMetrics(mxHostsTried, mxAttempts, portAttempts, void 0, startedAtMs);
1101
+ const smtpResult = {
1102
+ ...failureResult(lastReason, metrics),
1103
+ ...lastEnhancedStatus !== void 0 ? { enhancedStatus: lastEnhancedStatus } : {},
1104
+ ...lastResponseCode !== void 0 ? { responseCode: lastResponseCode } : {},
1105
+ ...captureTranscript ? { transcript: [...transcript], commands: [...commands] } : {}
1106
+ };
1107
+ return { smtpResult, cached: false, port: 0, portCached: false };
1108
+ }
1109
+ function makeMetrics(mxHostsTried, mxAttempts, portAttempts, mxHostUsed, startedAtMs) {
1077
1110
  return {
1078
- smtpResult: {
1079
- ...failureResult("All SMTP connection attempts failed"),
1080
- ...captureTranscript ? { transcript: [...transcript], commands: [...commands] } : {}
1081
- },
1082
- cached: false,
1083
- port: 0,
1084
- portCached: false
1111
+ mxAttempts,
1112
+ portAttempts,
1113
+ mxHostsTried: [...mxHostsTried],
1114
+ ...mxHostUsed !== void 0 ? { mxHostUsed } : {},
1115
+ totalDurationMs: Date.now() - startedAtMs
1085
1116
  };
1086
1117
  }
1087
- function collectTranscript(transcript, commands, probe, port) {
1118
+ function collectTranscript(transcript, commands, probe, mxHost, port) {
1119
+ const prefix = `${mxHost}:${port}`;
1088
1120
  for (const line of probe.transcript)
1089
- transcript.push(`${port}|s| ${line}`);
1121
+ transcript.push(`${prefix}|s| ${line}`);
1090
1122
  for (const cmd of probe.commands)
1091
- commands.push(`${port}|c| ${cmd}`);
1123
+ commands.push(`${prefix}|c| ${cmd}`);
1092
1124
  }
1093
- function failureResult(error) {
1125
+ function failureResult(reason, metrics) {
1094
1126
  return {
1095
1127
  canConnectSmtp: false,
1096
1128
  hasFullInbox: false,
1097
1129
  isCatchAll: false,
1098
1130
  isDeliverable: false,
1099
1131
  isDisabled: false,
1100
- error,
1101
- providerUsed: exports.EmailProvider.everythingElse,
1102
- checkedAt: Date.now()
1132
+ error: reason,
1133
+ checkedAt: Date.now(),
1134
+ metrics
1103
1135
  };
1104
1136
  }
1105
- function toSmtpVerificationResult(result, capture) {
1106
- const base = {
1137
+ function toSmtpVerificationResult(probe, extras) {
1138
+ var _a;
1139
+ const result = probe.result;
1140
+ const out = {
1107
1141
  canConnectSmtp: result !== null,
1108
- hasFullInbox: false,
1109
- isCatchAll: false,
1142
+ hasFullInbox: probe.reason === "over_quota",
1143
+ isCatchAll: (_a = probe.isCatchAll) !== null && _a !== void 0 ? _a : false,
1110
1144
  isDeliverable: result === true,
1111
1145
  isDisabled: result === false,
1112
- error: result === true ? void 0 : result === null ? "ambiguous" : "not_found",
1113
- providerUsed: exports.EmailProvider.everythingElse,
1114
- checkedAt: Date.now()
1146
+ error: result === true ? void 0 : probe.reason,
1147
+ checkedAt: Date.now(),
1148
+ metrics: extras.metrics,
1149
+ ...probe.enhancedStatus !== void 0 ? { enhancedStatus: probe.enhancedStatus } : {},
1150
+ ...probe.responseCode !== void 0 ? { responseCode: probe.responseCode } : {}
1115
1151
  };
1116
- if (!capture)
1117
- return base;
1118
- return { ...base, transcript: [...capture.transcript], commands: [...capture.commands] };
1152
+ if (extras.transcript)
1153
+ out.transcript = [...extras.transcript];
1154
+ if (extras.commands)
1155
+ out.commands = [...extras.commands];
1156
+ return out;
1119
1157
  }
1120
1158
  async function safeCacheGet(store, key) {
1121
1159
  if (!store)
@@ -1145,8 +1183,17 @@ class SMTPProbeConnection {
1145
1183
  this.buffer = "";
1146
1184
  this.resolved = false;
1147
1185
  this.currentStepIndex = 0;
1186
+ this.tlsUpgrading = false;
1187
+ this.postUpgradeReEhlo = false;
1148
1188
  this.transcript = [];
1149
1189
  this.commands = [];
1190
+ this.supportsPipelining = false;
1191
+ this.supportsStartTls = false;
1192
+ this.dualPhase = "idle";
1193
+ this.realOutcome = "pending";
1194
+ this.probeOutcome = "pending";
1195
+ this.dualPipelined = false;
1196
+ this.pendingDecision = null;
1150
1197
  this.onData = (data) => {
1151
1198
  if (this.resolved)
1152
1199
  return;
@@ -1159,7 +1206,7 @@ class SMTPProbeConnection {
1159
1206
  this.processLine(line);
1160
1207
  }
1161
1208
  };
1162
- const defaultSteps = [exports.SMTPStep.greeting, exports.SMTPStep.ehlo, exports.SMTPStep.mailFrom, exports.SMTPStep.rcptTo];
1209
+ const defaultSteps = [exports.SMTPStep.greeting, exports.SMTPStep.ehlo, exports.SMTPStep.startTls, exports.SMTPStep.mailFrom, exports.SMTPStep.rcptTo];
1163
1210
  this.steps = [...(_b = (_a = p.sequence) === null || _a === void 0 ? void 0 : _a.steps) !== null && _b !== void 0 ? _b : defaultSteps];
1164
1211
  this.isTLS = PORT_TLS[p.port] === true;
1165
1212
  const servername = isIPAddress(p.mxHost) ? void 0 : p.mxHost;
@@ -1170,6 +1217,7 @@ class SMTPProbeConnection {
1170
1217
  minVersion: "TLSv1.2",
1171
1218
  ...typeof p.tlsConfig === "object" ? p.tlsConfig : {}
1172
1219
  };
1220
+ this.probeLocal = p.catchAllProbeLocal ? p.catchAllProbeLocal(p.local, p.domain) : defaultProbeLocal();
1173
1221
  }
1174
1222
  run() {
1175
1223
  return new Promise((resolve) => {
@@ -1178,7 +1226,8 @@ class SMTPProbeConnection {
1178
1226
  this.connect();
1179
1227
  this.armConnectionTimer();
1180
1228
  } catch (error) {
1181
- this.finish(null, `connect_throw:${error instanceof Error ? error.message : "unknown"}`);
1229
+ this.p.log(`connect threw: ${error instanceof Error ? error.message : "unknown"}`);
1230
+ this.finish(null, "connection_error");
1182
1231
  }
1183
1232
  });
1184
1233
  }
@@ -1230,51 +1279,173 @@ class SMTPProbeConnection {
1230
1279
  switch (step) {
1231
1280
  case exports.SMTPStep.greeting:
1232
1281
  return;
1282
+ // server-driven; nothing to send
1233
1283
  case exports.SMTPStep.ehlo:
1234
1284
  this.send(`EHLO ${this.p.hostname}`);
1235
1285
  return;
1236
1286
  case exports.SMTPStep.helo:
1237
1287
  this.send(`HELO ${this.p.hostname}`);
1238
1288
  return;
1289
+ case exports.SMTPStep.startTls:
1290
+ this.executeStartTls();
1291
+ return;
1239
1292
  case exports.SMTPStep.mailFrom: {
1240
1293
  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}>`;
1241
1294
  this.send(`MAIL FROM:${from}`);
1242
1295
  return;
1243
1296
  }
1244
1297
  case exports.SMTPStep.rcptTo:
1245
- this.send(`RCPT TO:<${this.p.local}@${this.p.domain}>`);
1298
+ this.executeEnvelope();
1246
1299
  return;
1247
1300
  }
1248
1301
  }
1249
- processLine(line) {
1250
- if (this.resolved)
1302
+ /**
1303
+ * Conditional STARTTLS upgrade. Skipped (advances to next step) when:
1304
+ * - already TLS (implicit-TLS port like 465 or already-upgraded)
1305
+ * - `startTls === 'never'`
1306
+ * - `startTls === 'auto'` AND the MX didn't advertise STARTTLS in EHLO
1307
+ *
1308
+ * Sends `STARTTLS` and waits for 220 when:
1309
+ * - `startTls === 'force'` (regardless of advertisement)
1310
+ * - `startTls === 'auto'` AND the MX advertised it
1311
+ *
1312
+ * On 220, `tls.connect()` wraps the existing socket. After the handshake
1313
+ * we re-EHLO (mandatory per RFC 3207 §4.2 — pre-TLS state must be
1314
+ * discarded) before continuing to MAIL FROM.
1315
+ */
1316
+ executeStartTls() {
1317
+ const mode = this.p.startTls;
1318
+ const wantsUpgrade = !this.isTLS && mode !== "never" && (mode === "force" || mode === "auto" && this.supportsStartTls);
1319
+ if (!wantsUpgrade) {
1320
+ this.nextStep();
1251
1321
  return;
1252
- this.transcript.push(line);
1253
- this.p.log(`\u2190 ${line}`);
1254
- if (isHighVolume(line)) {
1255
- this.finish(true, "high_volume");
1322
+ }
1323
+ this.send("STARTTLS");
1324
+ }
1325
+ /**
1326
+ * Wrap the plaintext socket with TLS in place. Called after the server
1327
+ * answers our STARTTLS with 220. Detaches the plaintext-socket listeners
1328
+ * (TLS owns the underlying transport now), re-installs them on the wrapped
1329
+ * socket, resets EHLO-derived capabilities, and re-issues EHLO once the
1330
+ * handshake completes (RFC 3207 §4.2 mandates re-EHLO after upgrade —
1331
+ * pre-TLS state must be discarded).
1332
+ */
1333
+ upgradeToTls() {
1334
+ const plainSocket = this.socket;
1335
+ if (!plainSocket) {
1336
+ this.finish(null, "tls_upgrade_failed");
1256
1337
  return;
1257
1338
  }
1258
- if (isOverQuota(line)) {
1259
- this.finish(false, "over_quota");
1339
+ const detach = plainSocket.removeAllListeners;
1340
+ if (typeof detach === "function") {
1341
+ detach.call(plainSocket, "data");
1342
+ detach.call(plainSocket, "error");
1343
+ detach.call(plainSocket, "close");
1344
+ detach.call(plainSocket, "timeout");
1345
+ }
1346
+ try {
1347
+ plainSocket.setTimeout(0);
1348
+ } catch {
1349
+ }
1350
+ this.tlsUpgrading = true;
1351
+ const servername = isIPAddress(this.p.mxHost) ? void 0 : this.p.mxHost;
1352
+ const tlsSocket = tls__namespace.connect({ ...this.tlsOptions, socket: plainSocket, servername }, () => {
1353
+ this.tlsUpgrading = false;
1354
+ this.isTLS = true;
1355
+ this.buffer = "";
1356
+ this.supportsStartTls = false;
1357
+ this.supportsPipelining = false;
1358
+ this.postUpgradeReEhlo = true;
1359
+ this.send(`EHLO ${this.p.hostname}`);
1360
+ });
1361
+ this.socket = tlsSocket;
1362
+ this.socket.on("data", this.onData);
1363
+ this.socket.on("error", () => {
1364
+ this.finish(null, this.tlsUpgrading ? "tls_handshake_failed" : "connection_error");
1365
+ });
1366
+ this.socket.on("close", () => this.finish(null, "connection_closed"));
1367
+ this.socket.setTimeout(this.p.timeout, () => this.finish(null, "socket_timeout"));
1368
+ }
1369
+ /**
1370
+ * Send the dual-probe envelope (real RCPT + probe RCPT + RSET). Pipelined
1371
+ * when the MX advertised PIPELINING (or `pipelining: 'force'`); sequential
1372
+ * otherwise.
1373
+ */
1374
+ executeEnvelope() {
1375
+ var _a;
1376
+ const wantsPipelining = this.p.pipelining === "force" || this.p.pipelining === "auto" && this.supportsPipelining;
1377
+ const realCmd = `RCPT TO:<${this.p.local}@${this.p.domain}>`;
1378
+ if (wantsPipelining) {
1379
+ this.dualPipelined = true;
1380
+ const probeCmd = `RCPT TO:<${this.probeLocal}@${this.p.domain}>`;
1381
+ const rsetCmd = "RSET";
1382
+ this.commands.push(realCmd, probeCmd, rsetCmd);
1383
+ this.p.log(`\u2192 ${realCmd}`);
1384
+ this.p.log(`\u2192 ${probeCmd}`);
1385
+ this.p.log(`\u2192 ${rsetCmd}`);
1386
+ (_a = this.socket) === null || _a === void 0 ? void 0 : _a.write(`${realCmd}\r
1387
+ ${probeCmd}\r
1388
+ ${rsetCmd}\r
1389
+ `);
1390
+ this.dualPhase = "rcpt_real";
1260
1391
  return;
1261
1392
  }
1262
- if (isInvalidMailboxError(line)) {
1263
- this.finish(false, "not_found");
1393
+ this.dualPipelined = false;
1394
+ this.send(realCmd);
1395
+ this.dualPhase = "rcpt_real";
1396
+ }
1397
+ processLine(line) {
1398
+ if (this.resolved)
1264
1399
  return;
1400
+ this.transcript.push(line);
1401
+ this.p.log(`\u2190 ${line}`);
1402
+ const codeStr = line.slice(0, 3);
1403
+ const numericCode = /^\d{3}$/.test(codeStr) ? parseInt(codeStr, 10) : null;
1404
+ if (numericCode !== null)
1405
+ this.lastResponseCode = numericCode;
1406
+ const dsn = parseDsn(line);
1407
+ if (dsn)
1408
+ this.lastEnhancedStatus = dsnToString(dsn);
1409
+ if (this.dualPhase === "idle" || this.dualPhase === "rcpt_real") {
1410
+ if (isHighVolume(line)) {
1411
+ this.finish(true, "high_volume");
1412
+ return;
1413
+ }
1414
+ if (isOverQuota(line)) {
1415
+ this.isCatchAllFlag = false;
1416
+ this.finish(false, "over_quota");
1417
+ return;
1418
+ }
1419
+ if (isInvalidMailboxError(line)) {
1420
+ this.isCatchAllFlag = false;
1421
+ this.finish(false, "not_found");
1422
+ return;
1423
+ }
1265
1424
  }
1266
- if (MULTILINE_RE.test(line))
1425
+ if (MULTILINE_RE.test(line)) {
1426
+ const step = this.steps[this.currentStepIndex];
1427
+ const isEhloLike = step === exports.SMTPStep.ehlo || step === exports.SMTPStep.helo || step === exports.SMTPStep.startTls && this.postUpgradeReEhlo;
1428
+ if (isEhloLike && line.startsWith("250-")) {
1429
+ const upper = line.toUpperCase();
1430
+ if (upper.includes("PIPELINING"))
1431
+ this.supportsPipelining = true;
1432
+ if (upper.includes("STARTTLS"))
1433
+ this.supportsStartTls = true;
1434
+ }
1267
1435
  return;
1268
- const code = line.slice(0, 3);
1269
- const numericCode = /^\d{3}$/.test(code) ? parseInt(code, 10) : null;
1436
+ }
1270
1437
  if (numericCode === null) {
1271
1438
  this.finish(null, "unrecognized_response");
1272
1439
  return;
1273
1440
  }
1274
- this.dispatch(numericCode);
1441
+ this.dispatch(numericCode, line);
1275
1442
  }
1276
- dispatch(code) {
1443
+ dispatch(code, line) {
1277
1444
  const step = this.steps[this.currentStepIndex];
1445
+ if (this.dualPhase !== "idle" && step === exports.SMTPStep.rcptTo) {
1446
+ this.handleEnvelopeReply(code, line);
1447
+ return;
1448
+ }
1278
1449
  switch (step) {
1279
1450
  case exports.SMTPStep.greeting:
1280
1451
  if (code === 220)
@@ -1294,6 +1465,21 @@ class SMTPProbeConnection {
1294
1465
  else
1295
1466
  this.finish(null, "helo_failed");
1296
1467
  return;
1468
+ case exports.SMTPStep.startTls:
1469
+ if (this.postUpgradeReEhlo) {
1470
+ this.postUpgradeReEhlo = false;
1471
+ if (code === 250)
1472
+ this.nextStep();
1473
+ else
1474
+ this.finish(null, "ehlo_failed");
1475
+ return;
1476
+ }
1477
+ if (code === 220) {
1478
+ this.upgradeToTls();
1479
+ } else {
1480
+ this.finish(null, "tls_upgrade_failed");
1481
+ }
1482
+ return;
1297
1483
  case exports.SMTPStep.mailFrom:
1298
1484
  if (code === 250)
1299
1485
  this.nextStep();
@@ -1301,15 +1487,91 @@ class SMTPProbeConnection {
1301
1487
  this.finish(null, "mail_from_rejected");
1302
1488
  return;
1303
1489
  case exports.SMTPStep.rcptTo:
1304
- if (code === 250 || code === 251)
1305
- this.finish(true, "valid");
1306
- else if (code === 552 || code === 452)
1307
- this.finish(false, "over_quota");
1308
- else if (code >= 400 && code < 500)
1309
- this.finish(null, "temporary_failure");
1310
- else
1311
- this.finish(null, "ambiguous");
1490
+ this.handleEnvelopeReply(code, line);
1491
+ return;
1492
+ }
1493
+ }
1494
+ /**
1495
+ * Dual-probe / pipelined-envelope reply router. Demuxes server replies for
1496
+ * the three queued commands (real RCPT, probe RCPT, RSET) and resolves
1497
+ * with the catch-all-aware verdict.
1498
+ */
1499
+ handleEnvelopeReply(code, line) {
1500
+ if (this.dualPhase === "rcpt_real") {
1501
+ this.realOutcome = classifyRcpt(code);
1502
+ if (code === 552 || code === 452 || isOverQuota(line)) {
1503
+ this.isCatchAllFlag = false;
1504
+ if (this.dualPipelined) {
1505
+ this.pendingDecision = { result: false, reason: "over_quota" };
1506
+ this.dualPhase = "rcpt_probe";
1507
+ return;
1508
+ }
1509
+ this.finish(false, "over_quota");
1510
+ return;
1511
+ }
1512
+ if (this.realOutcome === "soft_reject") {
1513
+ if (this.dualPipelined) {
1514
+ this.pendingDecision = { result: null, reason: "temporary_failure" };
1515
+ this.dualPhase = "rcpt_probe";
1516
+ return;
1517
+ }
1518
+ this.finish(null, "temporary_failure");
1519
+ return;
1520
+ }
1521
+ if (this.realOutcome === "hard_reject") {
1522
+ const reason = isInvalidMailboxError(line) ? "not_found" : "ambiguous";
1523
+ const result = reason === "not_found" ? false : null;
1524
+ this.isCatchAllFlag = false;
1525
+ if (this.dualPipelined) {
1526
+ this.pendingDecision = { result, reason };
1527
+ this.dualPhase = "rcpt_probe";
1528
+ return;
1529
+ }
1530
+ this.finish(result, reason);
1312
1531
  return;
1532
+ }
1533
+ if (this.dualPipelined) {
1534
+ this.dualPhase = "rcpt_probe";
1535
+ } else {
1536
+ this.send(`RCPT TO:<${this.probeLocal}@${this.p.domain}>`);
1537
+ this.dualPhase = "rcpt_probe";
1538
+ }
1539
+ return;
1540
+ }
1541
+ if (this.dualPhase === "rcpt_probe") {
1542
+ this.probeOutcome = classifyRcpt(code);
1543
+ if (this.dualPipelined) {
1544
+ this.dualPhase = "rset";
1545
+ } else {
1546
+ this.send("RSET");
1547
+ this.dualPhase = "rset";
1548
+ }
1549
+ return;
1550
+ }
1551
+ if (this.dualPhase === "rset") {
1552
+ if (this.pendingDecision) {
1553
+ this.finish(this.pendingDecision.result, this.pendingDecision.reason);
1554
+ return;
1555
+ }
1556
+ this.decideDualProbe();
1557
+ return;
1558
+ }
1559
+ }
1560
+ /** Final decision after both RCPT outcomes are known. Catch-all only when both 250. */
1561
+ decideDualProbe() {
1562
+ if (this.realOutcome === "accept" && this.probeOutcome === "accept") {
1563
+ this.isCatchAllFlag = true;
1564
+ this.finish(true, "valid");
1565
+ } else if (this.realOutcome === "accept") {
1566
+ this.isCatchAllFlag = false;
1567
+ this.finish(true, "valid");
1568
+ } else if (this.realOutcome === "hard_reject") {
1569
+ this.isCatchAllFlag = false;
1570
+ this.finish(false, "not_found");
1571
+ } else if (this.realOutcome === "soft_reject") {
1572
+ this.finish(null, "temporary_failure");
1573
+ } else {
1574
+ this.finish(null, "ambiguous");
1313
1575
  }
1314
1576
  }
1315
1577
  finish(result, reason) {
@@ -1335,9 +1597,24 @@ class SMTPProbeConnection {
1335
1597
  return (_a2 = this.socket) === null || _a2 === void 0 ? void 0 : _a2.destroy();
1336
1598
  }, QUIT_DRAIN_MS);
1337
1599
  (_c = drain.unref) === null || _c === void 0 ? void 0 : _c.call(drain);
1338
- this.resolveFn({ result, transcript: this.transcript, commands: this.commands });
1600
+ this.resolveFn({
1601
+ result,
1602
+ reason,
1603
+ ...this.lastEnhancedStatus !== void 0 ? { enhancedStatus: this.lastEnhancedStatus } : {},
1604
+ ...this.lastResponseCode !== void 0 ? { responseCode: this.lastResponseCode } : {},
1605
+ ...this.isCatchAllFlag !== void 0 ? { isCatchAll: this.isCatchAllFlag } : {},
1606
+ transcript: this.transcript,
1607
+ commands: this.commands
1608
+ });
1339
1609
  }
1340
1610
  }
1611
+ function classifyRcpt(code) {
1612
+ if (code === 250 || code === 251)
1613
+ return "accept";
1614
+ if (code >= 400 && code < 500)
1615
+ return "soft_reject";
1616
+ return "hard_reject";
1617
+ }
1341
1618
 
1342
1619
  class ArrayTranscriptCollector {
1343
1620
  constructor() {
@@ -2010,12 +2287,11 @@ async function verifyEmail(params) {
2010
2287
  const skipMx = ((_f = params.skipMxForDisposable) !== null && _f !== void 0 ? _f : false) && result.isDisposable;
2011
2288
  const skipWhois = ((_g = params.skipDomainWhoisForDisposable) !== null && _g !== void 0 ? _g : false) && result.isDisposable;
2012
2289
  await runWhoisChecks(domain, params, result, skipWhois, log, collector);
2013
- if (((_h = params.verifyMx) !== null && _h !== void 0 ? _h : true) || ((_j = params.verifySmtp) !== null && _j !== void 0 ? _j : false)) {
2014
- if (skipMx) {
2015
- log(`[verifyEmail] skipping MX/SMTP for disposable: ${params.emailAddress}`);
2016
- } else {
2017
- await runMxAndSmtp(local, domain, params, result, log, collector);
2018
- }
2290
+ const wantsMxOrSmtp = ((_h = params.verifyMx) !== null && _h !== void 0 ? _h : true) || ((_j = params.verifySmtp) !== null && _j !== void 0 ? _j : false);
2291
+ if (wantsMxOrSmtp && skipMx) {
2292
+ log(`[verifyEmail] skipping MX/SMTP for disposable: ${params.emailAddress}`);
2293
+ } else if (wantsMxOrSmtp) {
2294
+ await runMxAndSmtp(local, domain, params, result, log, collector);
2019
2295
  }
2020
2296
  result.metadata.verificationTime = Date.now() - startTime;
2021
2297
  if (captureTranscript)
@@ -2365,6 +2641,36 @@ function isSpamName(name) {
2365
2641
  return true;
2366
2642
  }
2367
2643
 
2644
+ const REFINEMENT_TABLE = {
2645
+ // X.1.x — addressing
2646
+ "5.1.1": "mailbox_does_not_exist",
2647
+ "5.1.2": "bad_destination_system",
2648
+ "5.1.3": "bad_destination_address",
2649
+ "5.1.6": "mailbox_moved",
2650
+ "5.1.10": "recipient_address_has_null_mx",
2651
+ // X.2.x — mailbox status
2652
+ "5.2.0": "mailbox_status_other",
2653
+ "5.2.1": "mailbox_disabled",
2654
+ "5.2.2": "mailbox_full",
2655
+ "5.2.3": "message_too_long",
2656
+ "5.2.4": "mailing_list_expansion_problem",
2657
+ // X.4.x — network / routing
2658
+ "4.4.1": "no_answer_from_host",
2659
+ "4.4.2": "bad_connection",
2660
+ // X.7.x — security / policy
2661
+ "5.7.0": "security_other",
2662
+ "5.7.1": "delivery_not_authorized",
2663
+ "5.7.25": "no_reverse_dns",
2664
+ "5.7.26": "multiple_authentication_failures"
2665
+ };
2666
+ function refineReasonByEnhancedStatus(reason, enhancedStatus) {
2667
+ var _a;
2668
+ const base = reason !== null && reason !== void 0 ? reason : "unknown";
2669
+ if (!enhancedStatus)
2670
+ return base;
2671
+ return (_a = REFINEMENT_TABLE[enhancedStatus]) !== null && _a !== void 0 ? _a : base;
2672
+ }
2673
+
2368
2674
  const NETWORK_ERROR_PATTERNS = [
2369
2675
  "etimedout",
2370
2676
  "econnrefused",
@@ -2495,6 +2801,7 @@ exports.isSpamName = isSpamName;
2495
2801
  exports.isValidEmail = isValidEmail;
2496
2802
  exports.isValidEmailDomain = isValidEmailDomain;
2497
2803
  exports.parseSmtpError = parseSmtpError;
2804
+ exports.refineReasonByEnhancedStatus = refineReasonByEnhancedStatus;
2498
2805
  exports.suggestDomain = suggestDomain;
2499
2806
  exports.suggestEmailDomain = suggestEmailDomain;
2500
2807
  exports.verifyEmail = verifyEmail;