@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 +35 -0
- package/dist/cli/index.js +103 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +130 -8
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +130 -7
- package/dist/index.js.map +1 -1
- package/dist/refine-reason.d.ts +1 -0
- package/dist/serverless/adapters/aws-lambda.cjs.js.map +1 -1
- package/dist/serverless/adapters/aws-lambda.esm.js.map +1 -1
- package/dist/serverless/index.cjs.js.map +1 -1
- package/dist/serverless/index.esm.js.map +1 -1
- package/dist/types.d.ts +28 -19
- 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
|
@@ -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.
|
|
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
|
-
|
|
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");
|