@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.d.ts CHANGED
@@ -18,6 +18,7 @@ export { isFreeEmail } from './is-free-email';
18
18
  export { isSpamEmail } from './is-spam-email';
19
19
  export { isSpamName } from './is-spam-name';
20
20
  export { cleanNameForAlgorithm, defaultNameDetectionMethod, detectName, detectNameForAlgorithm, detectNameFromEmail, } from './name-detector';
21
+ export { refineReasonByEnhancedStatus } from './refine-reason';
21
22
  export { type ParsedSmtpError, parseSmtpError } from './smtp-error-parser';
22
23
  export { ArrayTranscriptCollector, NULL_COLLECTOR, type TranscriptCollector, } from './transcript';
23
24
  export * from './types';
package/dist/index.esm.js CHANGED
@@ -175,6 +175,7 @@ var SMTPStep;
175
175
  SMTPStep2["greeting"] = "GREETING";
176
176
  SMTPStep2["ehlo"] = "EHLO";
177
177
  SMTPStep2["helo"] = "HELO";
178
+ SMTPStep2["startTls"] = "STARTTLS";
178
179
  SMTPStep2["mailFrom"] = "MAIL_FROM";
179
180
  SMTPStep2["rcptTo"] = "RCPT_TO";
180
181
  })(SMTPStep || (SMTPStep = {}));
@@ -993,7 +994,7 @@ function defaultProbeLocal() {
993
994
  return `${randomBytes(8).toString("hex")}-noexist`;
994
995
  }
995
996
  async function verifyMailboxSMTP(params) {
996
- var _a, _b, _c, _d, _e, _f, _g, _h;
997
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
997
998
  const { local, domain, options = {} } = params;
998
999
  const mxRecords = (_a = params.mxRecords) !== null && _a !== void 0 ? _a : [];
999
1000
  const ports = ((_b = options.ports) !== null && _b !== void 0 ? _b : DEFAULT_PORTS).filter((port) => Number.isInteger(port) && port > 0 && port < 65536);
@@ -1025,7 +1026,8 @@ async function verifyMailboxSMTP(params) {
1025
1026
  sequence,
1026
1027
  log,
1027
1028
  catchAllProbeLocal: options.catchAllProbeLocal,
1028
- pipelining: (_h = options.pipelining) !== null && _h !== void 0 ? _h : "auto"
1029
+ pipelining: (_h = options.pipelining) !== null && _h !== void 0 ? _h : "auto",
1030
+ startTls: (_j = options.startTls) !== null && _j !== void 0 ? _j : "auto"
1029
1031
  };
1030
1032
  const verdictCache = cache ? getCacheStore(cache, "smtp") : null;
1031
1033
  const verdictKey = `${primaryMx}:${local}@${domain}`;
@@ -1159,9 +1161,12 @@ class SMTPProbeConnection {
1159
1161
  this.buffer = "";
1160
1162
  this.resolved = false;
1161
1163
  this.currentStepIndex = 0;
1164
+ this.tlsUpgrading = false;
1165
+ this.postUpgradeReEhlo = false;
1162
1166
  this.transcript = [];
1163
1167
  this.commands = [];
1164
1168
  this.supportsPipelining = false;
1169
+ this.supportsStartTls = false;
1165
1170
  this.dualPhase = "idle";
1166
1171
  this.realOutcome = "pending";
1167
1172
  this.probeOutcome = "pending";
@@ -1179,7 +1184,7 @@ class SMTPProbeConnection {
1179
1184
  this.processLine(line);
1180
1185
  }
1181
1186
  };
1182
- const defaultSteps = [SMTPStep.greeting, SMTPStep.ehlo, SMTPStep.mailFrom, SMTPStep.rcptTo];
1187
+ const defaultSteps = [SMTPStep.greeting, SMTPStep.ehlo, SMTPStep.startTls, SMTPStep.mailFrom, SMTPStep.rcptTo];
1183
1188
  this.steps = [...(_b = (_a = p.sequence) === null || _a === void 0 ? void 0 : _a.steps) !== null && _b !== void 0 ? _b : defaultSteps];
1184
1189
  this.isTLS = PORT_TLS[p.port] === true;
1185
1190
  const servername = isIPAddress(p.mxHost) ? void 0 : p.mxHost;
@@ -1199,7 +1204,8 @@ class SMTPProbeConnection {
1199
1204
  this.connect();
1200
1205
  this.armConnectionTimer();
1201
1206
  } catch (error) {
1202
- 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");
1203
1209
  }
1204
1210
  });
1205
1211
  }
@@ -1258,6 +1264,9 @@ class SMTPProbeConnection {
1258
1264
  case SMTPStep.helo:
1259
1265
  this.send(`HELO ${this.p.hostname}`);
1260
1266
  return;
1267
+ case SMTPStep.startTls:
1268
+ this.executeStartTls();
1269
+ return;
1261
1270
  case SMTPStep.mailFrom: {
1262
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}>`;
1263
1272
  this.send(`MAIL FROM:${from}`);
@@ -1268,6 +1277,73 @@ class SMTPProbeConnection {
1268
1277
  return;
1269
1278
  }
1270
1279
  }
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();
1299
+ return;
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");
1315
+ return;
1316
+ }
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
+ }
1271
1347
  /**
1272
1348
  * Send the dual-probe envelope (real RCPT + probe RCPT + RSET). Pipelined
1273
1349
  * when the MX advertised PIPELINING (or `pipelining: 'force'`); sequential
@@ -1326,10 +1402,13 @@ ${rsetCmd}\r
1326
1402
  }
1327
1403
  if (MULTILINE_RE.test(line)) {
1328
1404
  const step = this.steps[this.currentStepIndex];
1329
- if ((step === SMTPStep.ehlo || step === SMTPStep.helo) && line.startsWith("250-")) {
1405
+ const isEhloLike = step === SMTPStep.ehlo || step === SMTPStep.helo || step === SMTPStep.startTls && this.postUpgradeReEhlo;
1406
+ if (isEhloLike && line.startsWith("250-")) {
1330
1407
  const upper = line.toUpperCase();
1331
1408
  if (upper.includes("PIPELINING"))
1332
1409
  this.supportsPipelining = true;
1410
+ if (upper.includes("STARTTLS"))
1411
+ this.supportsStartTls = true;
1333
1412
  }
1334
1413
  return;
1335
1414
  }
@@ -1364,6 +1443,21 @@ ${rsetCmd}\r
1364
1443
  else
1365
1444
  this.finish(null, "helo_failed");
1366
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;
1367
1461
  case SMTPStep.mailFrom:
1368
1462
  if (code === 250)
1369
1463
  this.nextStep();
@@ -2525,6 +2619,36 @@ function isSpamName(name) {
2525
2619
  return true;
2526
2620
  }
2527
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
+
2528
2652
  const NETWORK_ERROR_PATTERNS = [
2529
2653
  "etimedout",
2530
2654
  "econnrefused",
@@ -2628,5 +2752,5 @@ function parseSmtpError(errorMessage) {
2628
2752
  return { isDisabled, hasFullInbox, isCatchAll, isInvalid };
2629
2753
  }
2630
2754
 
2631
- 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 };
2632
2756
  //# sourceMappingURL=index.esm.js.map