@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/README.md +35 -0
- package/dist/cli/index.js +103 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +130 -6
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +130 -5
- package/dist/index.js.map +1 -1
- package/dist/refine-reason.d.ts +1 -0
- package/dist/types.d.ts +25 -6
- package/package.json +1 -1
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
|
@@ -318,6 +318,7 @@ var SMTPStep = /* @__PURE__ */ ((SMTPStep2) => {
|
|
|
318
318
|
SMTPStep2["greeting"] = "GREETING";
|
|
319
319
|
SMTPStep2["ehlo"] = "EHLO";
|
|
320
320
|
SMTPStep2["helo"] = "HELO";
|
|
321
|
+
SMTPStep2["startTls"] = "STARTTLS";
|
|
321
322
|
SMTPStep2["mailFrom"] = "MAIL_FROM";
|
|
322
323
|
SMTPStep2["rcptTo"] = "RCPT_TO";
|
|
323
324
|
return SMTPStep2;
|
|
@@ -1081,7 +1082,8 @@ async function verifyMailboxSMTP(params) {
|
|
|
1081
1082
|
sequence,
|
|
1082
1083
|
log,
|
|
1083
1084
|
catchAllProbeLocal: options.catchAllProbeLocal,
|
|
1084
|
-
pipelining: options.pipelining ?? "auto"
|
|
1085
|
+
pipelining: options.pipelining ?? "auto",
|
|
1086
|
+
startTls: options.startTls ?? "auto"
|
|
1085
1087
|
};
|
|
1086
1088
|
const verdictCache = cache ? getCacheStore(cache, "smtp") : null;
|
|
1087
1089
|
const verdictKey = `${primaryMx}:${local}@${domain}`;
|
|
@@ -1204,10 +1206,21 @@ class SMTPProbeConnection {
|
|
|
1204
1206
|
this.buffer = "";
|
|
1205
1207
|
this.resolved = false;
|
|
1206
1208
|
this.currentStepIndex = 0;
|
|
1209
|
+
/** True between sending STARTTLS and the TLS handshake completing. */
|
|
1210
|
+
this.tlsUpgrading = false;
|
|
1211
|
+
/**
|
|
1212
|
+
* True when we sent the second EHLO after a successful STARTTLS upgrade.
|
|
1213
|
+
* The EHLO response arrives while `currentStepIndex` is still on `startTls`
|
|
1214
|
+
* — this flag tells the dispatcher to advance past startTls when the
|
|
1215
|
+
* post-upgrade EHLO returns 250, instead of treating it as a STARTTLS
|
|
1216
|
+
* acknowledgement.
|
|
1217
|
+
*/
|
|
1218
|
+
this.postUpgradeReEhlo = false;
|
|
1207
1219
|
this.transcript = [];
|
|
1208
1220
|
this.commands = [];
|
|
1209
1221
|
// ── EHLO capability advertisement ────────────────────────────────────────
|
|
1210
1222
|
this.supportsPipelining = false;
|
|
1223
|
+
this.supportsStartTls = false;
|
|
1211
1224
|
this.dualPhase = "idle";
|
|
1212
1225
|
this.realOutcome = "pending";
|
|
1213
1226
|
this.probeOutcome = "pending";
|
|
@@ -1229,7 +1242,7 @@ class SMTPProbeConnection {
|
|
|
1229
1242
|
this.processLine(line);
|
|
1230
1243
|
}
|
|
1231
1244
|
};
|
|
1232
|
-
const defaultSteps = [SMTPStep.greeting, SMTPStep.ehlo, SMTPStep.mailFrom, SMTPStep.rcptTo];
|
|
1245
|
+
const defaultSteps = [SMTPStep.greeting, SMTPStep.ehlo, SMTPStep.startTls, SMTPStep.mailFrom, SMTPStep.rcptTo];
|
|
1233
1246
|
this.steps = [...p.sequence?.steps ?? defaultSteps];
|
|
1234
1247
|
this.isTLS = PORT_TLS[p.port] === true;
|
|
1235
1248
|
const servername = isIPAddress(p.mxHost) ? void 0 : p.mxHost;
|
|
@@ -1249,7 +1262,8 @@ class SMTPProbeConnection {
|
|
|
1249
1262
|
this.connect();
|
|
1250
1263
|
this.armConnectionTimer();
|
|
1251
1264
|
} catch (error) {
|
|
1252
|
-
this.
|
|
1265
|
+
this.p.log(`connect threw: ${error instanceof Error ? error.message : "unknown"}`);
|
|
1266
|
+
this.finish(null, "connection_error");
|
|
1253
1267
|
}
|
|
1254
1268
|
});
|
|
1255
1269
|
}
|
|
@@ -1302,6 +1316,9 @@ class SMTPProbeConnection {
|
|
|
1302
1316
|
case SMTPStep.helo:
|
|
1303
1317
|
this.send(`HELO ${this.p.hostname}`);
|
|
1304
1318
|
return;
|
|
1319
|
+
case SMTPStep.startTls:
|
|
1320
|
+
this.executeStartTls();
|
|
1321
|
+
return;
|
|
1305
1322
|
case SMTPStep.mailFrom: {
|
|
1306
1323
|
const from = this.p.sequence?.from ?? `<${this.p.local}@${this.p.domain}>`;
|
|
1307
1324
|
this.send(`MAIL FROM:${from}`);
|
|
@@ -1312,6 +1329,73 @@ class SMTPProbeConnection {
|
|
|
1312
1329
|
return;
|
|
1313
1330
|
}
|
|
1314
1331
|
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Conditional STARTTLS upgrade. Skipped (advances to next step) when:
|
|
1334
|
+
* - already TLS (implicit-TLS port like 465 or already-upgraded)
|
|
1335
|
+
* - `startTls === 'never'`
|
|
1336
|
+
* - `startTls === 'auto'` AND the MX didn't advertise STARTTLS in EHLO
|
|
1337
|
+
*
|
|
1338
|
+
* Sends `STARTTLS` and waits for 220 when:
|
|
1339
|
+
* - `startTls === 'force'` (regardless of advertisement)
|
|
1340
|
+
* - `startTls === 'auto'` AND the MX advertised it
|
|
1341
|
+
*
|
|
1342
|
+
* On 220, `tls.connect()` wraps the existing socket. After the handshake
|
|
1343
|
+
* we re-EHLO (mandatory per RFC 3207 §4.2 — pre-TLS state must be
|
|
1344
|
+
* discarded) before continuing to MAIL FROM.
|
|
1345
|
+
*/
|
|
1346
|
+
executeStartTls() {
|
|
1347
|
+
const mode = this.p.startTls;
|
|
1348
|
+
const wantsUpgrade = !this.isTLS && mode !== "never" && (mode === "force" || mode === "auto" && this.supportsStartTls);
|
|
1349
|
+
if (!wantsUpgrade) {
|
|
1350
|
+
this.nextStep();
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
this.send("STARTTLS");
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Wrap the plaintext socket with TLS in place. Called after the server
|
|
1357
|
+
* answers our STARTTLS with 220. Detaches the plaintext-socket listeners
|
|
1358
|
+
* (TLS owns the underlying transport now), re-installs them on the wrapped
|
|
1359
|
+
* socket, resets EHLO-derived capabilities, and re-issues EHLO once the
|
|
1360
|
+
* handshake completes (RFC 3207 §4.2 mandates re-EHLO after upgrade —
|
|
1361
|
+
* pre-TLS state must be discarded).
|
|
1362
|
+
*/
|
|
1363
|
+
upgradeToTls() {
|
|
1364
|
+
const plainSocket = this.socket;
|
|
1365
|
+
if (!plainSocket) {
|
|
1366
|
+
this.finish(null, "tls_upgrade_failed");
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
const detach = plainSocket.removeAllListeners;
|
|
1370
|
+
if (typeof detach === "function") {
|
|
1371
|
+
detach.call(plainSocket, "data");
|
|
1372
|
+
detach.call(plainSocket, "error");
|
|
1373
|
+
detach.call(plainSocket, "close");
|
|
1374
|
+
detach.call(plainSocket, "timeout");
|
|
1375
|
+
}
|
|
1376
|
+
try {
|
|
1377
|
+
plainSocket.setTimeout(0);
|
|
1378
|
+
} catch {
|
|
1379
|
+
}
|
|
1380
|
+
this.tlsUpgrading = true;
|
|
1381
|
+
const servername = isIPAddress(this.p.mxHost) ? void 0 : this.p.mxHost;
|
|
1382
|
+
const tlsSocket = tls__namespace.connect({ ...this.tlsOptions, socket: plainSocket, servername }, () => {
|
|
1383
|
+
this.tlsUpgrading = false;
|
|
1384
|
+
this.isTLS = true;
|
|
1385
|
+
this.buffer = "";
|
|
1386
|
+
this.supportsStartTls = false;
|
|
1387
|
+
this.supportsPipelining = false;
|
|
1388
|
+
this.postUpgradeReEhlo = true;
|
|
1389
|
+
this.send(`EHLO ${this.p.hostname}`);
|
|
1390
|
+
});
|
|
1391
|
+
this.socket = tlsSocket;
|
|
1392
|
+
this.socket.on("data", this.onData);
|
|
1393
|
+
this.socket.on("error", () => {
|
|
1394
|
+
this.finish(null, this.tlsUpgrading ? "tls_handshake_failed" : "connection_error");
|
|
1395
|
+
});
|
|
1396
|
+
this.socket.on("close", () => this.finish(null, "connection_closed"));
|
|
1397
|
+
this.socket.setTimeout(this.p.timeout, () => this.finish(null, "socket_timeout"));
|
|
1398
|
+
}
|
|
1315
1399
|
/**
|
|
1316
1400
|
* Send the dual-probe envelope (real RCPT + probe RCPT + RSET). Pipelined
|
|
1317
1401
|
* when the MX advertised PIPELINING (or `pipelining: 'force'`); sequential
|
|
@@ -1366,9 +1450,11 @@ ${rsetCmd}\r
|
|
|
1366
1450
|
}
|
|
1367
1451
|
if (MULTILINE_RE.test(line)) {
|
|
1368
1452
|
const step = this.steps[this.currentStepIndex];
|
|
1369
|
-
|
|
1453
|
+
const isEhloLike = step === SMTPStep.ehlo || step === SMTPStep.helo || step === SMTPStep.startTls && this.postUpgradeReEhlo;
|
|
1454
|
+
if (isEhloLike && line.startsWith("250-")) {
|
|
1370
1455
|
const upper = line.toUpperCase();
|
|
1371
1456
|
if (upper.includes("PIPELINING")) this.supportsPipelining = true;
|
|
1457
|
+
if (upper.includes("STARTTLS")) this.supportsStartTls = true;
|
|
1372
1458
|
}
|
|
1373
1459
|
return;
|
|
1374
1460
|
}
|
|
@@ -1397,6 +1483,19 @@ ${rsetCmd}\r
|
|
|
1397
1483
|
if (code === 250) this.nextStep();
|
|
1398
1484
|
else this.finish(null, "helo_failed");
|
|
1399
1485
|
return;
|
|
1486
|
+
case SMTPStep.startTls:
|
|
1487
|
+
if (this.postUpgradeReEhlo) {
|
|
1488
|
+
this.postUpgradeReEhlo = false;
|
|
1489
|
+
if (code === 250) this.nextStep();
|
|
1490
|
+
else this.finish(null, "ehlo_failed");
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
if (code === 220) {
|
|
1494
|
+
this.upgradeToTls();
|
|
1495
|
+
} else {
|
|
1496
|
+
this.finish(null, "tls_upgrade_failed");
|
|
1497
|
+
}
|
|
1498
|
+
return;
|
|
1400
1499
|
case SMTPStep.mailFrom:
|
|
1401
1500
|
if (code === 250) this.nextStep();
|
|
1402
1501
|
else this.finish(null, "mail_from_rejected");
|