@emailcheck/email-validator-js 3.4.4 → 4.0.0-beta.1
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 +3 -3
- package/dist/cache-interface.d.ts +4 -2
- package/dist/cli/index.js +273 -105
- package/dist/cli/index.js.map +1 -1
- package/dist/index.esm.js +280 -98
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +280 -98
- package/dist/index.js.map +1 -1
- 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/adapters/vercel.cjs.js.map +1 -1
- package/dist/serverless/adapters/vercel.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/smtp-verifier.d.ts +31 -14
- package/dist/types.d.ts +81 -26
- package/package.json +12 -2
package/README.md
CHANGED
|
@@ -91,7 +91,7 @@ pnpm add @emailcheck/email-validator-js
|
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
### Requirements (consumers)
|
|
94
|
-
- Node.js >=
|
|
94
|
+
- Node.js >= 22 (currently-supported LTS lines: 22 Maintenance, 24 Active)
|
|
95
95
|
- TypeScript >= 4.0 (for TypeScript users)
|
|
96
96
|
|
|
97
97
|
### Requirements (contributing)
|
|
@@ -864,7 +864,7 @@ bun run examples/high-level/advanced-usage.ts
|
|
|
864
864
|
bun run examples/integrations/algolia.ts
|
|
865
865
|
```
|
|
866
866
|
|
|
867
|
-
**After installation in your own project (Node
|
|
867
|
+
**After installation in your own project (Node 22+):**
|
|
868
868
|
|
|
869
869
|
```bash
|
|
870
870
|
node --experimental-strip-types examples/smtp/usage.ts
|
|
@@ -1386,7 +1386,7 @@ app.http('validateEmail', {
|
|
|
1386
1386
|
|
|
1387
1387
|
### Edge MX support via the built-in DoH resolver
|
|
1388
1388
|
|
|
1389
|
-
A built-in `DoHResolver` ships with the package — works in any runtime with `fetch` (Cloudflare Workers, Vercel Edge, Deno, browsers, Node
|
|
1389
|
+
A built-in `DoHResolver` ships with the package — works in any runtime with `fetch` (Cloudflare Workers, Vercel Edge, Deno, browsers, Node 22+):
|
|
1390
1390
|
|
|
1391
1391
|
```typescript
|
|
1392
1392
|
import {
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
import type { DisposableEmailResult, DomainValidResult, FreeEmailResult, SmtpVerificationResult } from './types';
|
|
6
6
|
import type { ParsedWhoisResult } from './whois-parser';
|
|
7
7
|
/**
|
|
8
|
-
* Generic cache interface that can be implemented by any cache store
|
|
8
|
+
* Generic cache interface that can be implemented by any cache store.
|
|
9
|
+
* Default type parameter is `unknown` — callers should narrow it
|
|
10
|
+
* (`CacheStore<DisposableEmailResult>`) rather than relying on the default.
|
|
9
11
|
*/
|
|
10
|
-
export interface CacheStore<T =
|
|
12
|
+
export interface CacheStore<T = unknown> {
|
|
11
13
|
/**
|
|
12
14
|
* Get a value from cache
|
|
13
15
|
* @param key - The cache key
|
package/dist/cli/index.js
CHANGED
|
@@ -7,6 +7,7 @@ var tinyLru = require('tiny-lru');
|
|
|
7
7
|
var psl = require('psl');
|
|
8
8
|
var stringSimilarityJs = require('string-similarity-js');
|
|
9
9
|
var node_dns = require('node:dns');
|
|
10
|
+
var node_crypto = require('node:crypto');
|
|
10
11
|
var net$1 = require('node:net');
|
|
11
12
|
var tls = require('node:tls');
|
|
12
13
|
|
|
@@ -313,16 +314,6 @@ var VerificationErrorCode = /* @__PURE__ */ ((VerificationErrorCode2) => {
|
|
|
313
314
|
VerificationErrorCode2["freeEmailProvider"] = "FREE_EMAIL_PROVIDER";
|
|
314
315
|
return VerificationErrorCode2;
|
|
315
316
|
})(VerificationErrorCode || {});
|
|
316
|
-
var EmailProvider = /* @__PURE__ */ ((EmailProvider2) => {
|
|
317
|
-
EmailProvider2["gmail"] = "gmail";
|
|
318
|
-
EmailProvider2["hotmailB2b"] = "hotmail_b2b";
|
|
319
|
-
EmailProvider2["hotmailB2c"] = "hotmail_b2c";
|
|
320
|
-
EmailProvider2["proofpoint"] = "proofpoint";
|
|
321
|
-
EmailProvider2["mimecast"] = "mimecast";
|
|
322
|
-
EmailProvider2["yahoo"] = "yahoo";
|
|
323
|
-
EmailProvider2["everythingElse"] = "everything_else";
|
|
324
|
-
return EmailProvider2;
|
|
325
|
-
})(EmailProvider || {});
|
|
326
317
|
var SMTPStep = /* @__PURE__ */ ((SMTPStep2) => {
|
|
327
318
|
SMTPStep2["greeting"] = "GREETING";
|
|
328
319
|
SMTPStep2["ehlo"] = "EHLO";
|
|
@@ -765,9 +756,9 @@ function parseCompositeNamePart(part) {
|
|
|
765
756
|
cleaned = pureAlpha;
|
|
766
757
|
confidence = 0.6;
|
|
767
758
|
} else {
|
|
768
|
-
const
|
|
769
|
-
if (
|
|
770
|
-
cleaned =
|
|
759
|
+
const baseMatch = part.match(/^([a-zA-Z]+)\d*$/);
|
|
760
|
+
if (baseMatch) {
|
|
761
|
+
cleaned = baseMatch[1];
|
|
771
762
|
confidence = 0.75;
|
|
772
763
|
} else {
|
|
773
764
|
cleaned = part;
|
|
@@ -781,9 +772,7 @@ function parseCompositeNamePart(part) {
|
|
|
781
772
|
confidence = Math.min(1, confidence + 0.2);
|
|
782
773
|
}
|
|
783
774
|
}
|
|
784
|
-
|
|
785
|
-
const base = baseMatch ? baseMatch[1] : part;
|
|
786
|
-
return { base, hasNumbers, cleaned, confidence };
|
|
775
|
+
return { hasNumbers, cleaned, confidence };
|
|
787
776
|
}
|
|
788
777
|
function isLikelyName(str, allowNumbers = false, allowSingleLetter = false) {
|
|
789
778
|
if (!str) return false;
|
|
@@ -1037,6 +1026,9 @@ function parseDsn(reply) {
|
|
|
1037
1026
|
if (!match) return null;
|
|
1038
1027
|
return { class: Number(match[1]), subject: Number(match[2]), detail: Number(match[3]) };
|
|
1039
1028
|
}
|
|
1029
|
+
function dsnToString(dsn) {
|
|
1030
|
+
return `${dsn.class}.${dsn.subject}.${dsn.detail}`;
|
|
1031
|
+
}
|
|
1040
1032
|
function isPolicyBlock(reply) {
|
|
1041
1033
|
const dsn = parseDsn(reply);
|
|
1042
1034
|
return dsn?.class === 5 && dsn?.subject === 7;
|
|
@@ -1054,6 +1046,9 @@ function isInvalidMailboxError(reply) {
|
|
|
1054
1046
|
if (isPolicyBlock(reply)) return false;
|
|
1055
1047
|
return true;
|
|
1056
1048
|
}
|
|
1049
|
+
function defaultProbeLocal() {
|
|
1050
|
+
return `${node_crypto.randomBytes(8).toString("hex")}-noexist`;
|
|
1051
|
+
}
|
|
1057
1052
|
async function verifyMailboxSMTP(params) {
|
|
1058
1053
|
const { local, domain, options = {} } = params;
|
|
1059
1054
|
const mxRecords = params.mxRecords ?? [];
|
|
@@ -1067,16 +1062,29 @@ async function verifyMailboxSMTP(params) {
|
|
|
1067
1062
|
const cache = options.cache;
|
|
1068
1063
|
const log = debug ? (...args) => console.log("[SMTP]", ...args) : () => {
|
|
1069
1064
|
};
|
|
1070
|
-
const
|
|
1071
|
-
|
|
1065
|
+
const startedAtMs = Date.now();
|
|
1066
|
+
const primaryMx = mxRecords[0];
|
|
1067
|
+
if (!primaryMx) {
|
|
1072
1068
|
log("No MX records found");
|
|
1073
|
-
|
|
1069
|
+
const metrics2 = makeMetrics([], 0, 0, void 0, startedAtMs);
|
|
1070
|
+
return { smtpResult: failureResult("no_mx_records", metrics2), cached: false, port: 0, portCached: false };
|
|
1074
1071
|
}
|
|
1075
|
-
log(`Verifying ${local}@${domain} via ${
|
|
1072
|
+
log(`Verifying ${local}@${domain} via ${primaryMx} (mx count=${mxRecords.length})`);
|
|
1076
1073
|
const transcript = [];
|
|
1077
1074
|
const commands = [];
|
|
1075
|
+
const probeOptions = {
|
|
1076
|
+
local,
|
|
1077
|
+
domain,
|
|
1078
|
+
timeout,
|
|
1079
|
+
tlsConfig,
|
|
1080
|
+
hostname,
|
|
1081
|
+
sequence,
|
|
1082
|
+
log,
|
|
1083
|
+
catchAllProbeLocal: options.catchAllProbeLocal,
|
|
1084
|
+
pipelining: options.pipelining ?? "auto"
|
|
1085
|
+
};
|
|
1078
1086
|
const verdictCache = cache ? getCacheStore(cache, "smtp") : null;
|
|
1079
|
-
const verdictKey = `${
|
|
1087
|
+
const verdictKey = `${primaryMx}:${local}@${domain}`;
|
|
1080
1088
|
if (verdictCache) {
|
|
1081
1089
|
const cachedResult = await safeCacheGet(verdictCache, verdictKey);
|
|
1082
1090
|
if (cachedResult) {
|
|
@@ -1085,81 +1093,91 @@ async function verifyMailboxSMTP(params) {
|
|
|
1085
1093
|
}
|
|
1086
1094
|
}
|
|
1087
1095
|
const portCache = cache ? getCacheStore(cache, "smtpPort") : null;
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
});
|
|
1103
|
-
collectTranscript(transcript, commands, probe,
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
)
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
if (probe.result !== null) {
|
|
1119
|
-
await safeCacheSet(portCache, mxHost, port);
|
|
1120
|
-
return { smtpResult, cached: false, port, portCached: false };
|
|
1096
|
+
const cachedPort = portCache ? await safeCacheGet(portCache, primaryMx) : null;
|
|
1097
|
+
const mxHostsTried = [];
|
|
1098
|
+
let mxAttempts = 0;
|
|
1099
|
+
let portAttempts = 0;
|
|
1100
|
+
let lastReason = "all_attempts_failed";
|
|
1101
|
+
let lastEnhancedStatus;
|
|
1102
|
+
let lastResponseCode;
|
|
1103
|
+
for (const mxHost of mxRecords) {
|
|
1104
|
+
mxHostsTried.push(mxHost);
|
|
1105
|
+
mxAttempts++;
|
|
1106
|
+
const portsForThisMx = mxHost === primaryMx && cachedPort ? [cachedPort, ...ports.filter((p) => p !== cachedPort)] : ports;
|
|
1107
|
+
for (const port of portsForThisMx) {
|
|
1108
|
+
portAttempts++;
|
|
1109
|
+
log(`Testing ${mxHost}:${port}`);
|
|
1110
|
+
const probe = await runProbe({ ...probeOptions, mxHost, port });
|
|
1111
|
+
collectTranscript(transcript, commands, probe, mxHost, port);
|
|
1112
|
+
lastReason = probe.reason;
|
|
1113
|
+
if (probe.enhancedStatus !== void 0) lastEnhancedStatus = probe.enhancedStatus;
|
|
1114
|
+
if (probe.responseCode !== void 0) lastResponseCode = probe.responseCode;
|
|
1115
|
+
if (probe.result !== null) {
|
|
1116
|
+
const metrics2 = makeMetrics(mxHostsTried, mxAttempts, portAttempts, mxHost, startedAtMs);
|
|
1117
|
+
const smtpResult2 = toSmtpVerificationResult(probe, {
|
|
1118
|
+
transcript: captureTranscript ? transcript : void 0,
|
|
1119
|
+
commands: captureTranscript ? commands : void 0,
|
|
1120
|
+
metrics: metrics2
|
|
1121
|
+
});
|
|
1122
|
+
await safeCacheSet(verdictCache, verdictKey, smtpResult2);
|
|
1123
|
+
if (mxHost === primaryMx) await safeCacheSet(portCache, primaryMx, port);
|
|
1124
|
+
return { smtpResult: smtpResult2, cached: false, port, portCached: cachedPort === port };
|
|
1125
|
+
}
|
|
1121
1126
|
}
|
|
1122
1127
|
}
|
|
1123
|
-
log(
|
|
1128
|
+
log(`All MX\xD7port attempts failed (mx=${mxAttempts}, port=${portAttempts})`);
|
|
1129
|
+
const metrics = makeMetrics(mxHostsTried, mxAttempts, portAttempts, void 0, startedAtMs);
|
|
1130
|
+
const smtpResult = {
|
|
1131
|
+
...failureResult(lastReason, metrics),
|
|
1132
|
+
...lastEnhancedStatus !== void 0 ? { enhancedStatus: lastEnhancedStatus } : {},
|
|
1133
|
+
...lastResponseCode !== void 0 ? { responseCode: lastResponseCode } : {},
|
|
1134
|
+
...captureTranscript ? { transcript: [...transcript], commands: [...commands] } : {}
|
|
1135
|
+
};
|
|
1136
|
+
return { smtpResult, cached: false, port: 0, portCached: false };
|
|
1137
|
+
}
|
|
1138
|
+
function makeMetrics(mxHostsTried, mxAttempts, portAttempts, mxHostUsed, startedAtMs) {
|
|
1124
1139
|
return {
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
},
|
|
1129
|
-
|
|
1130
|
-
port: 0,
|
|
1131
|
-
portCached: false
|
|
1140
|
+
mxAttempts,
|
|
1141
|
+
portAttempts,
|
|
1142
|
+
mxHostsTried: [...mxHostsTried],
|
|
1143
|
+
...mxHostUsed !== void 0 ? { mxHostUsed } : {},
|
|
1144
|
+
totalDurationMs: Date.now() - startedAtMs
|
|
1132
1145
|
};
|
|
1133
1146
|
}
|
|
1134
|
-
function collectTranscript(transcript, commands, probe, port) {
|
|
1135
|
-
|
|
1136
|
-
for (const
|
|
1147
|
+
function collectTranscript(transcript, commands, probe, mxHost, port) {
|
|
1148
|
+
const prefix = `${mxHost}:${port}`;
|
|
1149
|
+
for (const line of probe.transcript) transcript.push(`${prefix}|s| ${line}`);
|
|
1150
|
+
for (const cmd of probe.commands) commands.push(`${prefix}|c| ${cmd}`);
|
|
1137
1151
|
}
|
|
1138
|
-
function failureResult(
|
|
1152
|
+
function failureResult(reason, metrics) {
|
|
1139
1153
|
return {
|
|
1140
1154
|
canConnectSmtp: false,
|
|
1141
1155
|
hasFullInbox: false,
|
|
1142
1156
|
isCatchAll: false,
|
|
1143
1157
|
isDeliverable: false,
|
|
1144
1158
|
isDisabled: false,
|
|
1145
|
-
error,
|
|
1146
|
-
|
|
1147
|
-
|
|
1159
|
+
error: reason,
|
|
1160
|
+
checkedAt: Date.now(),
|
|
1161
|
+
metrics
|
|
1148
1162
|
};
|
|
1149
1163
|
}
|
|
1150
|
-
function toSmtpVerificationResult(
|
|
1151
|
-
const
|
|
1164
|
+
function toSmtpVerificationResult(probe, extras) {
|
|
1165
|
+
const result = probe.result;
|
|
1166
|
+
const out = {
|
|
1152
1167
|
canConnectSmtp: result !== null,
|
|
1153
|
-
hasFullInbox:
|
|
1154
|
-
isCatchAll: false,
|
|
1168
|
+
hasFullInbox: probe.reason === "over_quota",
|
|
1169
|
+
isCatchAll: probe.isCatchAll ?? false,
|
|
1155
1170
|
isDeliverable: result === true,
|
|
1156
1171
|
isDisabled: result === false,
|
|
1157
|
-
error: result === true ? void 0 :
|
|
1158
|
-
|
|
1159
|
-
|
|
1172
|
+
error: result === true ? void 0 : probe.reason,
|
|
1173
|
+
checkedAt: Date.now(),
|
|
1174
|
+
metrics: extras.metrics,
|
|
1175
|
+
...probe.enhancedStatus !== void 0 ? { enhancedStatus: probe.enhancedStatus } : {},
|
|
1176
|
+
...probe.responseCode !== void 0 ? { responseCode: probe.responseCode } : {}
|
|
1160
1177
|
};
|
|
1161
|
-
if (
|
|
1162
|
-
|
|
1178
|
+
if (extras.transcript) out.transcript = [...extras.transcript];
|
|
1179
|
+
if (extras.commands) out.commands = [...extras.commands];
|
|
1180
|
+
return out;
|
|
1163
1181
|
}
|
|
1164
1182
|
async function safeCacheGet(store, key) {
|
|
1165
1183
|
if (!store) return null;
|
|
@@ -1186,9 +1204,20 @@ class SMTPProbeConnection {
|
|
|
1186
1204
|
this.buffer = "";
|
|
1187
1205
|
this.resolved = false;
|
|
1188
1206
|
this.currentStepIndex = 0;
|
|
1189
|
-
/** Server lines + client commands captured unconditionally (cost is trivial). */
|
|
1190
1207
|
this.transcript = [];
|
|
1191
1208
|
this.commands = [];
|
|
1209
|
+
// ── EHLO capability advertisement ────────────────────────────────────────
|
|
1210
|
+
this.supportsPipelining = false;
|
|
1211
|
+
this.dualPhase = "idle";
|
|
1212
|
+
this.realOutcome = "pending";
|
|
1213
|
+
this.probeOutcome = "pending";
|
|
1214
|
+
this.dualPipelined = false;
|
|
1215
|
+
/**
|
|
1216
|
+
* Pipelined-only escape hatch. When the real RCPT is rejected mid-batched-
|
|
1217
|
+
* envelope, the probe + RSET are already on the wire; we stash the verdict
|
|
1218
|
+
* and commit it after the response loop drains.
|
|
1219
|
+
*/
|
|
1220
|
+
this.pendingDecision = null;
|
|
1192
1221
|
this.onData = (data) => {
|
|
1193
1222
|
if (this.resolved) return;
|
|
1194
1223
|
this.resetStepTimer();
|
|
@@ -1211,6 +1240,7 @@ class SMTPProbeConnection {
|
|
|
1211
1240
|
minVersion: "TLSv1.2",
|
|
1212
1241
|
...typeof p.tlsConfig === "object" ? p.tlsConfig : {}
|
|
1213
1242
|
};
|
|
1243
|
+
this.probeLocal = p.catchAllProbeLocal ? p.catchAllProbeLocal(p.local, p.domain) : defaultProbeLocal();
|
|
1214
1244
|
}
|
|
1215
1245
|
run() {
|
|
1216
1246
|
return new Promise((resolve) => {
|
|
@@ -1265,6 +1295,7 @@ class SMTPProbeConnection {
|
|
|
1265
1295
|
switch (step) {
|
|
1266
1296
|
case SMTPStep.greeting:
|
|
1267
1297
|
return;
|
|
1298
|
+
// server-driven; nothing to send
|
|
1268
1299
|
case SMTPStep.ehlo:
|
|
1269
1300
|
this.send(`EHLO ${this.p.hostname}`);
|
|
1270
1301
|
return;
|
|
@@ -1277,37 +1308,82 @@ class SMTPProbeConnection {
|
|
|
1277
1308
|
return;
|
|
1278
1309
|
}
|
|
1279
1310
|
case SMTPStep.rcptTo:
|
|
1280
|
-
this.
|
|
1311
|
+
this.executeEnvelope();
|
|
1281
1312
|
return;
|
|
1282
1313
|
}
|
|
1283
1314
|
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Send the dual-probe envelope (real RCPT + probe RCPT + RSET). Pipelined
|
|
1317
|
+
* when the MX advertised PIPELINING (or `pipelining: 'force'`); sequential
|
|
1318
|
+
* otherwise.
|
|
1319
|
+
*/
|
|
1320
|
+
executeEnvelope() {
|
|
1321
|
+
const wantsPipelining = this.p.pipelining === "force" || this.p.pipelining === "auto" && this.supportsPipelining;
|
|
1322
|
+
const realCmd = `RCPT TO:<${this.p.local}@${this.p.domain}>`;
|
|
1323
|
+
if (wantsPipelining) {
|
|
1324
|
+
this.dualPipelined = true;
|
|
1325
|
+
const probeCmd = `RCPT TO:<${this.probeLocal}@${this.p.domain}>`;
|
|
1326
|
+
const rsetCmd = "RSET";
|
|
1327
|
+
this.commands.push(realCmd, probeCmd, rsetCmd);
|
|
1328
|
+
this.p.log(`\u2192 ${realCmd}`);
|
|
1329
|
+
this.p.log(`\u2192 ${probeCmd}`);
|
|
1330
|
+
this.p.log(`\u2192 ${rsetCmd}`);
|
|
1331
|
+
this.socket?.write(`${realCmd}\r
|
|
1332
|
+
${probeCmd}\r
|
|
1333
|
+
${rsetCmd}\r
|
|
1334
|
+
`);
|
|
1335
|
+
this.dualPhase = "rcpt_real";
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
this.dualPipelined = false;
|
|
1339
|
+
this.send(realCmd);
|
|
1340
|
+
this.dualPhase = "rcpt_real";
|
|
1341
|
+
}
|
|
1284
1342
|
processLine(line) {
|
|
1285
1343
|
if (this.resolved) return;
|
|
1286
1344
|
this.transcript.push(line);
|
|
1287
1345
|
this.p.log(`\u2190 ${line}`);
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
if (
|
|
1293
|
-
|
|
1294
|
-
|
|
1346
|
+
const codeStr = line.slice(0, 3);
|
|
1347
|
+
const numericCode = /^\d{3}$/.test(codeStr) ? parseInt(codeStr, 10) : null;
|
|
1348
|
+
if (numericCode !== null) this.lastResponseCode = numericCode;
|
|
1349
|
+
const dsn = parseDsn(line);
|
|
1350
|
+
if (dsn) this.lastEnhancedStatus = dsnToString(dsn);
|
|
1351
|
+
if (this.dualPhase === "idle" || this.dualPhase === "rcpt_real") {
|
|
1352
|
+
if (isHighVolume(line)) {
|
|
1353
|
+
this.finish(true, "high_volume");
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
if (isOverQuota(line)) {
|
|
1357
|
+
this.isCatchAllFlag = false;
|
|
1358
|
+
this.finish(false, "over_quota");
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
if (isInvalidMailboxError(line)) {
|
|
1362
|
+
this.isCatchAllFlag = false;
|
|
1363
|
+
this.finish(false, "not_found");
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1295
1366
|
}
|
|
1296
|
-
if (
|
|
1297
|
-
this.
|
|
1367
|
+
if (MULTILINE_RE.test(line)) {
|
|
1368
|
+
const step = this.steps[this.currentStepIndex];
|
|
1369
|
+
if ((step === SMTPStep.ehlo || step === SMTPStep.helo) && line.startsWith("250-")) {
|
|
1370
|
+
const upper = line.toUpperCase();
|
|
1371
|
+
if (upper.includes("PIPELINING")) this.supportsPipelining = true;
|
|
1372
|
+
}
|
|
1298
1373
|
return;
|
|
1299
1374
|
}
|
|
1300
|
-
if (MULTILINE_RE.test(line)) return;
|
|
1301
|
-
const code = line.slice(0, 3);
|
|
1302
|
-
const numericCode = /^\d{3}$/.test(code) ? parseInt(code, 10) : null;
|
|
1303
1375
|
if (numericCode === null) {
|
|
1304
1376
|
this.finish(null, "unrecognized_response");
|
|
1305
1377
|
return;
|
|
1306
1378
|
}
|
|
1307
|
-
this.dispatch(numericCode);
|
|
1379
|
+
this.dispatch(numericCode, line);
|
|
1308
1380
|
}
|
|
1309
|
-
dispatch(code) {
|
|
1381
|
+
dispatch(code, line) {
|
|
1310
1382
|
const step = this.steps[this.currentStepIndex];
|
|
1383
|
+
if (this.dualPhase !== "idle" && step === SMTPStep.rcptTo) {
|
|
1384
|
+
this.handleEnvelopeReply(code, line);
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1311
1387
|
switch (step) {
|
|
1312
1388
|
case SMTPStep.greeting:
|
|
1313
1389
|
if (code === 220) this.nextStep();
|
|
@@ -1326,11 +1402,91 @@ class SMTPProbeConnection {
|
|
|
1326
1402
|
else this.finish(null, "mail_from_rejected");
|
|
1327
1403
|
return;
|
|
1328
1404
|
case SMTPStep.rcptTo:
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1405
|
+
this.handleEnvelopeReply(code, line);
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Dual-probe / pipelined-envelope reply router. Demuxes server replies for
|
|
1411
|
+
* the three queued commands (real RCPT, probe RCPT, RSET) and resolves
|
|
1412
|
+
* with the catch-all-aware verdict.
|
|
1413
|
+
*/
|
|
1414
|
+
handleEnvelopeReply(code, line) {
|
|
1415
|
+
if (this.dualPhase === "rcpt_real") {
|
|
1416
|
+
this.realOutcome = classifyRcpt(code);
|
|
1417
|
+
if (code === 552 || code === 452 || isOverQuota(line)) {
|
|
1418
|
+
this.isCatchAllFlag = false;
|
|
1419
|
+
if (this.dualPipelined) {
|
|
1420
|
+
this.pendingDecision = { result: false, reason: "over_quota" };
|
|
1421
|
+
this.dualPhase = "rcpt_probe";
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
this.finish(false, "over_quota");
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
if (this.realOutcome === "soft_reject") {
|
|
1428
|
+
if (this.dualPipelined) {
|
|
1429
|
+
this.pendingDecision = { result: null, reason: "temporary_failure" };
|
|
1430
|
+
this.dualPhase = "rcpt_probe";
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
this.finish(null, "temporary_failure");
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
if (this.realOutcome === "hard_reject") {
|
|
1437
|
+
const reason = isInvalidMailboxError(line) ? "not_found" : "ambiguous";
|
|
1438
|
+
const result = reason === "not_found" ? false : null;
|
|
1439
|
+
this.isCatchAllFlag = false;
|
|
1440
|
+
if (this.dualPipelined) {
|
|
1441
|
+
this.pendingDecision = { result, reason };
|
|
1442
|
+
this.dualPhase = "rcpt_probe";
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
this.finish(result, reason);
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
if (this.dualPipelined) {
|
|
1449
|
+
this.dualPhase = "rcpt_probe";
|
|
1450
|
+
} else {
|
|
1451
|
+
this.send(`RCPT TO:<${this.probeLocal}@${this.p.domain}>`);
|
|
1452
|
+
this.dualPhase = "rcpt_probe";
|
|
1453
|
+
}
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
if (this.dualPhase === "rcpt_probe") {
|
|
1457
|
+
this.probeOutcome = classifyRcpt(code);
|
|
1458
|
+
if (this.dualPipelined) {
|
|
1459
|
+
this.dualPhase = "rset";
|
|
1460
|
+
} else {
|
|
1461
|
+
this.send("RSET");
|
|
1462
|
+
this.dualPhase = "rset";
|
|
1463
|
+
}
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
if (this.dualPhase === "rset") {
|
|
1467
|
+
if (this.pendingDecision) {
|
|
1468
|
+
this.finish(this.pendingDecision.result, this.pendingDecision.reason);
|
|
1333
1469
|
return;
|
|
1470
|
+
}
|
|
1471
|
+
this.decideDualProbe();
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
/** Final decision after both RCPT outcomes are known. Catch-all only when both 250. */
|
|
1476
|
+
decideDualProbe() {
|
|
1477
|
+
if (this.realOutcome === "accept" && this.probeOutcome === "accept") {
|
|
1478
|
+
this.isCatchAllFlag = true;
|
|
1479
|
+
this.finish(true, "valid");
|
|
1480
|
+
} else if (this.realOutcome === "accept") {
|
|
1481
|
+
this.isCatchAllFlag = false;
|
|
1482
|
+
this.finish(true, "valid");
|
|
1483
|
+
} else if (this.realOutcome === "hard_reject") {
|
|
1484
|
+
this.isCatchAllFlag = false;
|
|
1485
|
+
this.finish(false, "not_found");
|
|
1486
|
+
} else if (this.realOutcome === "soft_reject") {
|
|
1487
|
+
this.finish(null, "temporary_failure");
|
|
1488
|
+
} else {
|
|
1489
|
+
this.finish(null, "ambiguous");
|
|
1334
1490
|
}
|
|
1335
1491
|
}
|
|
1336
1492
|
finish(result, reason) {
|
|
@@ -1349,9 +1505,22 @@ class SMTPProbeConnection {
|
|
|
1349
1505
|
}
|
|
1350
1506
|
const drain = setTimeout(() => this.socket?.destroy(), QUIT_DRAIN_MS);
|
|
1351
1507
|
drain.unref?.();
|
|
1352
|
-
this.resolveFn({
|
|
1508
|
+
this.resolveFn({
|
|
1509
|
+
result,
|
|
1510
|
+
reason,
|
|
1511
|
+
...this.lastEnhancedStatus !== void 0 ? { enhancedStatus: this.lastEnhancedStatus } : {},
|
|
1512
|
+
...this.lastResponseCode !== void 0 ? { responseCode: this.lastResponseCode } : {},
|
|
1513
|
+
...this.isCatchAllFlag !== void 0 ? { isCatchAll: this.isCatchAllFlag } : {},
|
|
1514
|
+
transcript: this.transcript,
|
|
1515
|
+
commands: this.commands
|
|
1516
|
+
});
|
|
1353
1517
|
}
|
|
1354
1518
|
}
|
|
1519
|
+
function classifyRcpt(code) {
|
|
1520
|
+
if (code === 250 || code === 251) return "accept";
|
|
1521
|
+
if (code >= 400 && code < 500) return "soft_reject";
|
|
1522
|
+
return "hard_reject";
|
|
1523
|
+
}
|
|
1355
1524
|
|
|
1356
1525
|
class ArrayTranscriptCollector {
|
|
1357
1526
|
constructor() {
|
|
@@ -2021,12 +2190,11 @@ async function verifyEmail(params) {
|
|
|
2021
2190
|
const skipMx = (params.skipMxForDisposable ?? false) && result.isDisposable;
|
|
2022
2191
|
const skipWhois = (params.skipDomainWhoisForDisposable ?? false) && result.isDisposable;
|
|
2023
2192
|
await runWhoisChecks(domain, params, result, skipWhois, log, collector);
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
}
|
|
2193
|
+
const wantsMxOrSmtp = (params.verifyMx ?? true) || (params.verifySmtp ?? false);
|
|
2194
|
+
if (wantsMxOrSmtp && skipMx) {
|
|
2195
|
+
log(`[verifyEmail] skipping MX/SMTP for disposable: ${params.emailAddress}`);
|
|
2196
|
+
} else if (wantsMxOrSmtp) {
|
|
2197
|
+
await runMxAndSmtp(local, domain, params, result, log, collector);
|
|
2030
2198
|
}
|
|
2031
2199
|
result.metadata.verificationTime = Date.now() - startTime;
|
|
2032
2200
|
if (captureTranscript) result.transcript = collector.steps;
|