@emailcheck/email-validator-js 4.0.0-beta.1 → 4.0.0-beta.3

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/README.md CHANGED
@@ -1485,6 +1485,41 @@ const parsed = parseSmtpError('552 5.2.2 mailbox over quota');
1485
1485
 
1486
1486
  The four flags are orthogonal — a single message can fire multiple. See `__tests__/0112-smtp-error-parser.test.ts` for the full classification matrix.
1487
1487
 
1488
+ ### Refining a coarse reason via the RFC 3463 enhanced status
1489
+
1490
+ `verifyMailboxSMTP` returns a coarse `error` reason (`not_found` / `over_quota` / `temporary_failure` / `ambiguous` / …) that's stable across MX implementations. When the MX includes an enhanced status code (RFC 3463) — e.g. `5.1.1` for "user unknown" vs. `5.7.1` for "policy block" — pipe both through `refineReasonByEnhancedStatus` to get a more specific reason:
1491
+
1492
+ ```typescript
1493
+ import { refineReasonByEnhancedStatus, verifyMailboxSMTP } from '@emailcheck/email-validator-js';
1494
+
1495
+ const { smtpResult } = await verifyMailboxSMTP({
1496
+ local: 'alice', domain: 'example.com', mxRecords: ['mx.example.com'],
1497
+ });
1498
+ const refined = refineReasonByEnhancedStatus(smtpResult.error, smtpResult.enhancedStatus);
1499
+ // e.g. 'mailbox_does_not_exist' instead of 'not_found' when the MX returned 550 5.1.1
1500
+ ```
1501
+
1502
+ Mapping (codes not in the table return the original reason unchanged):
1503
+
1504
+ | DSN code | Refined reason |
1505
+ | --------- | ------------------------------------ |
1506
+ | `5.1.1` | `mailbox_does_not_exist` |
1507
+ | `5.1.2` | `bad_destination_system` |
1508
+ | `5.1.3` | `bad_destination_address` |
1509
+ | `5.1.6` | `mailbox_moved` |
1510
+ | `5.1.10` | `recipient_address_has_null_mx` |
1511
+ | `5.2.0` | `mailbox_status_other` |
1512
+ | `5.2.1` | `mailbox_disabled` |
1513
+ | `5.2.2` | `mailbox_full` |
1514
+ | `5.2.3` | `message_too_long` |
1515
+ | `5.2.4` | `mailing_list_expansion_problem` |
1516
+ | `4.4.1` | `no_answer_from_host` |
1517
+ | `4.4.2` | `bad_connection` |
1518
+ | `5.7.0` | `security_other` |
1519
+ | `5.7.1` | `delivery_not_authorized` |
1520
+ | `5.7.25` | `no_reverse_dns` |
1521
+ | `5.7.26` | `multiple_authentication_failures` |
1522
+
1488
1523
  ## 📊 Performance & Caching
1489
1524
 
1490
1525
  The library includes intelligent caching to improve performance:
package/dist/cli/index.js CHANGED
@@ -308,16 +308,15 @@ var VerificationErrorCode = /* @__PURE__ */ ((VerificationErrorCode2) => {
308
308
  VerificationErrorCode2["smtpConnectionFailed"] = "SMTP_CONNECTION_FAILED";
309
309
  VerificationErrorCode2["smtpTimeout"] = "SMTP_TIMEOUT";
310
310
  VerificationErrorCode2["mailboxNotFound"] = "MAILBOX_NOT_FOUND";
311
- VerificationErrorCode2["mailboxFull"] = "MAILBOX_FULL";
312
311
  VerificationErrorCode2["networkError"] = "NETWORK_ERROR";
313
312
  VerificationErrorCode2["disposableEmail"] = "DISPOSABLE_EMAIL";
314
- VerificationErrorCode2["freeEmailProvider"] = "FREE_EMAIL_PROVIDER";
315
313
  return VerificationErrorCode2;
316
314
  })(VerificationErrorCode || {});
317
315
  var SMTPStep = /* @__PURE__ */ ((SMTPStep2) => {
318
316
  SMTPStep2["greeting"] = "GREETING";
319
317
  SMTPStep2["ehlo"] = "EHLO";
320
318
  SMTPStep2["helo"] = "HELO";
319
+ SMTPStep2["startTls"] = "STARTTLS";
321
320
  SMTPStep2["mailFrom"] = "MAIL_FROM";
322
321
  SMTPStep2["rcptTo"] = "RCPT_TO";
323
322
  return SMTPStep2;
@@ -1081,7 +1080,8 @@ async function verifyMailboxSMTP(params) {
1081
1080
  sequence,
1082
1081
  log,
1083
1082
  catchAllProbeLocal: options.catchAllProbeLocal,
1084
- pipelining: options.pipelining ?? "auto"
1083
+ pipelining: options.pipelining ?? "auto",
1084
+ startTls: options.startTls ?? "auto"
1085
1085
  };
1086
1086
  const verdictCache = cache ? getCacheStore(cache, "smtp") : null;
1087
1087
  const verdictKey = `${primaryMx}:${local}@${domain}`;
@@ -1204,10 +1204,21 @@ class SMTPProbeConnection {
1204
1204
  this.buffer = "";
1205
1205
  this.resolved = false;
1206
1206
  this.currentStepIndex = 0;
1207
+ /** True between sending STARTTLS and the TLS handshake completing. */
1208
+ this.tlsUpgrading = false;
1209
+ /**
1210
+ * True when we sent the second EHLO after a successful STARTTLS upgrade.
1211
+ * The EHLO response arrives while `currentStepIndex` is still on `startTls`
1212
+ * — this flag tells the dispatcher to advance past startTls when the
1213
+ * post-upgrade EHLO returns 250, instead of treating it as a STARTTLS
1214
+ * acknowledgement.
1215
+ */
1216
+ this.postUpgradeReEhlo = false;
1207
1217
  this.transcript = [];
1208
1218
  this.commands = [];
1209
1219
  // ── EHLO capability advertisement ────────────────────────────────────────
1210
1220
  this.supportsPipelining = false;
1221
+ this.supportsStartTls = false;
1211
1222
  this.dualPhase = "idle";
1212
1223
  this.realOutcome = "pending";
1213
1224
  this.probeOutcome = "pending";
@@ -1229,7 +1240,7 @@ class SMTPProbeConnection {
1229
1240
  this.processLine(line);
1230
1241
  }
1231
1242
  };
1232
- const defaultSteps = [SMTPStep.greeting, SMTPStep.ehlo, SMTPStep.mailFrom, SMTPStep.rcptTo];
1243
+ const defaultSteps = [SMTPStep.greeting, SMTPStep.ehlo, SMTPStep.startTls, SMTPStep.mailFrom, SMTPStep.rcptTo];
1233
1244
  this.steps = [...p.sequence?.steps ?? defaultSteps];
1234
1245
  this.isTLS = PORT_TLS[p.port] === true;
1235
1246
  const servername = isIPAddress(p.mxHost) ? void 0 : p.mxHost;
@@ -1249,7 +1260,8 @@ class SMTPProbeConnection {
1249
1260
  this.connect();
1250
1261
  this.armConnectionTimer();
1251
1262
  } catch (error) {
1252
- this.finish(null, `connect_throw:${error instanceof Error ? error.message : "unknown"}`);
1263
+ this.p.log(`connect threw: ${error instanceof Error ? error.message : "unknown"}`);
1264
+ this.finish(null, "connection_error");
1253
1265
  }
1254
1266
  });
1255
1267
  }
@@ -1302,6 +1314,9 @@ class SMTPProbeConnection {
1302
1314
  case SMTPStep.helo:
1303
1315
  this.send(`HELO ${this.p.hostname}`);
1304
1316
  return;
1317
+ case SMTPStep.startTls:
1318
+ this.executeStartTls();
1319
+ return;
1305
1320
  case SMTPStep.mailFrom: {
1306
1321
  const from = this.p.sequence?.from ?? `<${this.p.local}@${this.p.domain}>`;
1307
1322
  this.send(`MAIL FROM:${from}`);
@@ -1312,6 +1327,73 @@ class SMTPProbeConnection {
1312
1327
  return;
1313
1328
  }
1314
1329
  }
1330
+ /**
1331
+ * Conditional STARTTLS upgrade. Skipped (advances to next step) when:
1332
+ * - already TLS (implicit-TLS port like 465 or already-upgraded)
1333
+ * - `startTls === 'never'`
1334
+ * - `startTls === 'auto'` AND the MX didn't advertise STARTTLS in EHLO
1335
+ *
1336
+ * Sends `STARTTLS` and waits for 220 when:
1337
+ * - `startTls === 'force'` (regardless of advertisement)
1338
+ * - `startTls === 'auto'` AND the MX advertised it
1339
+ *
1340
+ * On 220, `tls.connect()` wraps the existing socket. After the handshake
1341
+ * we re-EHLO (mandatory per RFC 3207 §4.2 — pre-TLS state must be
1342
+ * discarded) before continuing to MAIL FROM.
1343
+ */
1344
+ executeStartTls() {
1345
+ const mode = this.p.startTls;
1346
+ const wantsUpgrade = !this.isTLS && mode !== "never" && (mode === "force" || mode === "auto" && this.supportsStartTls);
1347
+ if (!wantsUpgrade) {
1348
+ this.nextStep();
1349
+ return;
1350
+ }
1351
+ this.send("STARTTLS");
1352
+ }
1353
+ /**
1354
+ * Wrap the plaintext socket with TLS in place. Called after the server
1355
+ * answers our STARTTLS with 220. Detaches the plaintext-socket listeners
1356
+ * (TLS owns the underlying transport now), re-installs them on the wrapped
1357
+ * socket, resets EHLO-derived capabilities, and re-issues EHLO once the
1358
+ * handshake completes (RFC 3207 §4.2 mandates re-EHLO after upgrade —
1359
+ * pre-TLS state must be discarded).
1360
+ */
1361
+ upgradeToTls() {
1362
+ const plainSocket = this.socket;
1363
+ if (!plainSocket) {
1364
+ this.finish(null, "tls_upgrade_failed");
1365
+ return;
1366
+ }
1367
+ const detach = plainSocket.removeAllListeners;
1368
+ if (typeof detach === "function") {
1369
+ detach.call(plainSocket, "data");
1370
+ detach.call(plainSocket, "error");
1371
+ detach.call(plainSocket, "close");
1372
+ detach.call(plainSocket, "timeout");
1373
+ }
1374
+ try {
1375
+ plainSocket.setTimeout(0);
1376
+ } catch {
1377
+ }
1378
+ this.tlsUpgrading = true;
1379
+ const servername = isIPAddress(this.p.mxHost) ? void 0 : this.p.mxHost;
1380
+ const tlsSocket = tls__namespace.connect({ ...this.tlsOptions, socket: plainSocket, servername }, () => {
1381
+ this.tlsUpgrading = false;
1382
+ this.isTLS = true;
1383
+ this.buffer = "";
1384
+ this.supportsStartTls = false;
1385
+ this.supportsPipelining = false;
1386
+ this.postUpgradeReEhlo = true;
1387
+ this.send(`EHLO ${this.p.hostname}`);
1388
+ });
1389
+ this.socket = tlsSocket;
1390
+ this.socket.on("data", this.onData);
1391
+ this.socket.on("error", () => {
1392
+ this.finish(null, this.tlsUpgrading ? "tls_handshake_failed" : "connection_error");
1393
+ });
1394
+ this.socket.on("close", () => this.finish(null, "connection_closed"));
1395
+ this.socket.setTimeout(this.p.timeout, () => this.finish(null, "socket_timeout"));
1396
+ }
1315
1397
  /**
1316
1398
  * Send the dual-probe envelope (real RCPT + probe RCPT + RSET). Pipelined
1317
1399
  * when the MX advertised PIPELINING (or `pipelining: 'force'`); sequential
@@ -1366,9 +1448,11 @@ ${rsetCmd}\r
1366
1448
  }
1367
1449
  if (MULTILINE_RE.test(line)) {
1368
1450
  const step = this.steps[this.currentStepIndex];
1369
- if ((step === SMTPStep.ehlo || step === SMTPStep.helo) && line.startsWith("250-")) {
1451
+ const isEhloLike = step === SMTPStep.ehlo || step === SMTPStep.helo || step === SMTPStep.startTls && this.postUpgradeReEhlo;
1452
+ if (isEhloLike && line.startsWith("250-")) {
1370
1453
  const upper = line.toUpperCase();
1371
1454
  if (upper.includes("PIPELINING")) this.supportsPipelining = true;
1455
+ if (upper.includes("STARTTLS")) this.supportsStartTls = true;
1372
1456
  }
1373
1457
  return;
1374
1458
  }
@@ -1397,6 +1481,19 @@ ${rsetCmd}\r
1397
1481
  if (code === 250) this.nextStep();
1398
1482
  else this.finish(null, "helo_failed");
1399
1483
  return;
1484
+ case SMTPStep.startTls:
1485
+ if (this.postUpgradeReEhlo) {
1486
+ this.postUpgradeReEhlo = false;
1487
+ if (code === 250) this.nextStep();
1488
+ else this.finish(null, "ehlo_failed");
1489
+ return;
1490
+ }
1491
+ if (code === 220) {
1492
+ this.upgradeToTls();
1493
+ } else {
1494
+ this.finish(null, "tls_upgrade_failed");
1495
+ }
1496
+ return;
1400
1497
  case SMTPStep.mailFrom:
1401
1498
  if (code === 250) this.nextStep();
1402
1499
  else this.finish(null, "mail_from_rejected");