@emailcheck/email-validator-js 4.0.0-beta.1 → 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
@@ -197,6 +197,7 @@ exports.SMTPStep = void 0;
197
197
  SMTPStep2["greeting"] = "GREETING";
198
198
  SMTPStep2["ehlo"] = "EHLO";
199
199
  SMTPStep2["helo"] = "HELO";
200
+ SMTPStep2["startTls"] = "STARTTLS";
200
201
  SMTPStep2["mailFrom"] = "MAIL_FROM";
201
202
  SMTPStep2["rcptTo"] = "RCPT_TO";
202
203
  })(exports.SMTPStep || (exports.SMTPStep = {}));
@@ -1015,7 +1016,7 @@ function defaultProbeLocal() {
1015
1016
  return `${node_crypto.randomBytes(8).toString("hex")}-noexist`;
1016
1017
  }
1017
1018
  async function verifyMailboxSMTP(params) {
1018
- var _a, _b, _c, _d, _e, _f, _g, _h;
1019
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
1019
1020
  const { local, domain, options = {} } = params;
1020
1021
  const mxRecords = (_a = params.mxRecords) !== null && _a !== void 0 ? _a : [];
1021
1022
  const ports = ((_b = options.ports) !== null && _b !== void 0 ? _b : DEFAULT_PORTS).filter((port) => Number.isInteger(port) && port > 0 && port < 65536);
@@ -1047,7 +1048,8 @@ async function verifyMailboxSMTP(params) {
1047
1048
  sequence,
1048
1049
  log,
1049
1050
  catchAllProbeLocal: options.catchAllProbeLocal,
1050
- pipelining: (_h = options.pipelining) !== null && _h !== void 0 ? _h : "auto"
1051
+ pipelining: (_h = options.pipelining) !== null && _h !== void 0 ? _h : "auto",
1052
+ startTls: (_j = options.startTls) !== null && _j !== void 0 ? _j : "auto"
1051
1053
  };
1052
1054
  const verdictCache = cache ? getCacheStore(cache, "smtp") : null;
1053
1055
  const verdictKey = `${primaryMx}:${local}@${domain}`;
@@ -1181,9 +1183,12 @@ class SMTPProbeConnection {
1181
1183
  this.buffer = "";
1182
1184
  this.resolved = false;
1183
1185
  this.currentStepIndex = 0;
1186
+ this.tlsUpgrading = false;
1187
+ this.postUpgradeReEhlo = false;
1184
1188
  this.transcript = [];
1185
1189
  this.commands = [];
1186
1190
  this.supportsPipelining = false;
1191
+ this.supportsStartTls = false;
1187
1192
  this.dualPhase = "idle";
1188
1193
  this.realOutcome = "pending";
1189
1194
  this.probeOutcome = "pending";
@@ -1201,7 +1206,7 @@ class SMTPProbeConnection {
1201
1206
  this.processLine(line);
1202
1207
  }
1203
1208
  };
1204
- 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];
1205
1210
  this.steps = [...(_b = (_a = p.sequence) === null || _a === void 0 ? void 0 : _a.steps) !== null && _b !== void 0 ? _b : defaultSteps];
1206
1211
  this.isTLS = PORT_TLS[p.port] === true;
1207
1212
  const servername = isIPAddress(p.mxHost) ? void 0 : p.mxHost;
@@ -1221,7 +1226,8 @@ class SMTPProbeConnection {
1221
1226
  this.connect();
1222
1227
  this.armConnectionTimer();
1223
1228
  } catch (error) {
1224
- 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");
1225
1231
  }
1226
1232
  });
1227
1233
  }
@@ -1280,6 +1286,9 @@ class SMTPProbeConnection {
1280
1286
  case exports.SMTPStep.helo:
1281
1287
  this.send(`HELO ${this.p.hostname}`);
1282
1288
  return;
1289
+ case exports.SMTPStep.startTls:
1290
+ this.executeStartTls();
1291
+ return;
1283
1292
  case exports.SMTPStep.mailFrom: {
1284
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}>`;
1285
1294
  this.send(`MAIL FROM:${from}`);
@@ -1290,6 +1299,73 @@ class SMTPProbeConnection {
1290
1299
  return;
1291
1300
  }
1292
1301
  }
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();
1321
+ return;
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");
1337
+ return;
1338
+ }
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
+ }
1293
1369
  /**
1294
1370
  * Send the dual-probe envelope (real RCPT + probe RCPT + RSET). Pipelined
1295
1371
  * when the MX advertised PIPELINING (or `pipelining: 'force'`); sequential
@@ -1348,10 +1424,13 @@ ${rsetCmd}\r
1348
1424
  }
1349
1425
  if (MULTILINE_RE.test(line)) {
1350
1426
  const step = this.steps[this.currentStepIndex];
1351
- if ((step === exports.SMTPStep.ehlo || step === exports.SMTPStep.helo) && line.startsWith("250-")) {
1427
+ const isEhloLike = step === exports.SMTPStep.ehlo || step === exports.SMTPStep.helo || step === exports.SMTPStep.startTls && this.postUpgradeReEhlo;
1428
+ if (isEhloLike && line.startsWith("250-")) {
1352
1429
  const upper = line.toUpperCase();
1353
1430
  if (upper.includes("PIPELINING"))
1354
1431
  this.supportsPipelining = true;
1432
+ if (upper.includes("STARTTLS"))
1433
+ this.supportsStartTls = true;
1355
1434
  }
1356
1435
  return;
1357
1436
  }
@@ -1386,6 +1465,21 @@ ${rsetCmd}\r
1386
1465
  else
1387
1466
  this.finish(null, "helo_failed");
1388
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;
1389
1483
  case exports.SMTPStep.mailFrom:
1390
1484
  if (code === 250)
1391
1485
  this.nextStep();
@@ -2547,6 +2641,36 @@ function isSpamName(name) {
2547
2641
  return true;
2548
2642
  }
2549
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
+
2550
2674
  const NETWORK_ERROR_PATTERNS = [
2551
2675
  "etimedout",
2552
2676
  "econnrefused",
@@ -2677,6 +2801,7 @@ exports.isSpamName = isSpamName;
2677
2801
  exports.isValidEmail = isValidEmail;
2678
2802
  exports.isValidEmailDomain = isValidEmailDomain;
2679
2803
  exports.parseSmtpError = parseSmtpError;
2804
+ exports.refineReasonByEnhancedStatus = refineReasonByEnhancedStatus;
2680
2805
  exports.suggestDomain = suggestDomain;
2681
2806
  exports.suggestEmailDomain = suggestEmailDomain;
2682
2807
  exports.verifyEmail = verifyEmail;