@emailcheck/email-validator-js 3.4.4 → 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 +38 -3
- package/dist/cache-interface.d.ts +4 -2
- package/dist/cli/index.js +374 -107
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +407 -101
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +407 -100
- 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/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 +103 -29
- package/package.json +12 -2
package/dist/index.esm.js
CHANGED
|
@@ -2,6 +2,7 @@ import { lru } from 'tiny-lru';
|
|
|
2
2
|
import { isValid, parse } from 'psl';
|
|
3
3
|
import { stringSimilarity } from 'string-similarity-js';
|
|
4
4
|
import { promises } from 'node:dns';
|
|
5
|
+
import { randomBytes } from 'node:crypto';
|
|
5
6
|
import * as net$1 from 'node:net';
|
|
6
7
|
import * as tls from 'node:tls';
|
|
7
8
|
|
|
@@ -174,6 +175,7 @@ var SMTPStep;
|
|
|
174
175
|
SMTPStep2["greeting"] = "GREETING";
|
|
175
176
|
SMTPStep2["ehlo"] = "EHLO";
|
|
176
177
|
SMTPStep2["helo"] = "HELO";
|
|
178
|
+
SMTPStep2["startTls"] = "STARTTLS";
|
|
177
179
|
SMTPStep2["mailFrom"] = "MAIL_FROM";
|
|
178
180
|
SMTPStep2["rcptTo"] = "RCPT_TO";
|
|
179
181
|
})(SMTPStep || (SMTPStep = {}));
|
|
@@ -658,9 +660,9 @@ function parseCompositeNamePart(part) {
|
|
|
658
660
|
cleaned = pureAlpha;
|
|
659
661
|
confidence = 0.6;
|
|
660
662
|
} else {
|
|
661
|
-
const
|
|
662
|
-
if (
|
|
663
|
-
cleaned =
|
|
663
|
+
const baseMatch = part.match(/^([a-zA-Z]+)\d*$/);
|
|
664
|
+
if (baseMatch) {
|
|
665
|
+
cleaned = baseMatch[1];
|
|
664
666
|
confidence = 0.75;
|
|
665
667
|
} else {
|
|
666
668
|
cleaned = part;
|
|
@@ -674,9 +676,7 @@ function parseCompositeNamePart(part) {
|
|
|
674
676
|
confidence = Math.min(1, confidence + 0.2);
|
|
675
677
|
}
|
|
676
678
|
}
|
|
677
|
-
|
|
678
|
-
const base = baseMatch ? baseMatch[1] : part;
|
|
679
|
-
return { base, hasNumbers, cleaned, confidence };
|
|
679
|
+
return { hasNumbers, cleaned, confidence };
|
|
680
680
|
}
|
|
681
681
|
function isLikelyName(str, allowNumbers = false, allowSingleLetter = false) {
|
|
682
682
|
if (!str)
|
|
@@ -967,6 +967,9 @@ function parseDsn(reply) {
|
|
|
967
967
|
return null;
|
|
968
968
|
return { class: Number(match[1]), subject: Number(match[2]), detail: Number(match[3]) };
|
|
969
969
|
}
|
|
970
|
+
function dsnToString(dsn) {
|
|
971
|
+
return `${dsn.class}.${dsn.subject}.${dsn.detail}`;
|
|
972
|
+
}
|
|
970
973
|
function isPolicyBlock(reply) {
|
|
971
974
|
const dsn = parseDsn(reply);
|
|
972
975
|
return (dsn === null || dsn === void 0 ? void 0 : dsn.class) === 5 && (dsn === null || dsn === void 0 ? void 0 : dsn.subject) === 7;
|
|
@@ -987,8 +990,11 @@ function isInvalidMailboxError(reply) {
|
|
|
987
990
|
return false;
|
|
988
991
|
return true;
|
|
989
992
|
}
|
|
993
|
+
function defaultProbeLocal() {
|
|
994
|
+
return `${randomBytes(8).toString("hex")}-noexist`;
|
|
995
|
+
}
|
|
990
996
|
async function verifyMailboxSMTP(params) {
|
|
991
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
997
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
992
998
|
const { local, domain, options = {} } = params;
|
|
993
999
|
const mxRecords = (_a = params.mxRecords) !== null && _a !== void 0 ? _a : [];
|
|
994
1000
|
const ports = ((_b = options.ports) !== null && _b !== void 0 ? _b : DEFAULT_PORTS).filter((port) => Number.isInteger(port) && port > 0 && port < 65536);
|
|
@@ -1001,16 +1007,30 @@ async function verifyMailboxSMTP(params) {
|
|
|
1001
1007
|
const cache = options.cache;
|
|
1002
1008
|
const log = debug ? (...args) => console.log("[SMTP]", ...args) : () => {
|
|
1003
1009
|
};
|
|
1004
|
-
const
|
|
1005
|
-
|
|
1010
|
+
const startedAtMs = Date.now();
|
|
1011
|
+
const primaryMx = mxRecords[0];
|
|
1012
|
+
if (!primaryMx) {
|
|
1006
1013
|
log("No MX records found");
|
|
1007
|
-
|
|
1014
|
+
const metrics2 = makeMetrics([], 0, 0, void 0, startedAtMs);
|
|
1015
|
+
return { smtpResult: failureResult("no_mx_records", metrics2), cached: false, port: 0, portCached: false };
|
|
1008
1016
|
}
|
|
1009
|
-
log(`Verifying ${local}@${domain} via ${
|
|
1017
|
+
log(`Verifying ${local}@${domain} via ${primaryMx} (mx count=${mxRecords.length})`);
|
|
1010
1018
|
const transcript = [];
|
|
1011
1019
|
const commands = [];
|
|
1020
|
+
const probeOptions = {
|
|
1021
|
+
local,
|
|
1022
|
+
domain,
|
|
1023
|
+
timeout,
|
|
1024
|
+
tlsConfig,
|
|
1025
|
+
hostname,
|
|
1026
|
+
sequence,
|
|
1027
|
+
log,
|
|
1028
|
+
catchAllProbeLocal: options.catchAllProbeLocal,
|
|
1029
|
+
pipelining: (_h = options.pipelining) !== null && _h !== void 0 ? _h : "auto",
|
|
1030
|
+
startTls: (_j = options.startTls) !== null && _j !== void 0 ? _j : "auto"
|
|
1031
|
+
};
|
|
1012
1032
|
const verdictCache = cache ? getCacheStore(cache, "smtp") : null;
|
|
1013
|
-
const verdictKey = `${
|
|
1033
|
+
const verdictKey = `${primaryMx}:${local}@${domain}`;
|
|
1014
1034
|
if (verdictCache) {
|
|
1015
1035
|
const cachedResult = await safeCacheGet(verdictCache, verdictKey);
|
|
1016
1036
|
if (cachedResult) {
|
|
@@ -1019,81 +1039,99 @@ async function verifyMailboxSMTP(params) {
|
|
|
1019
1039
|
}
|
|
1020
1040
|
}
|
|
1021
1041
|
const portCache = cache ? getCacheStore(cache, "smtpPort") : null;
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
});
|
|
1037
|
-
collectTranscript(transcript, commands, probe,
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1042
|
+
const cachedPort = portCache ? await safeCacheGet(portCache, primaryMx) : null;
|
|
1043
|
+
const mxHostsTried = [];
|
|
1044
|
+
let mxAttempts = 0;
|
|
1045
|
+
let portAttempts = 0;
|
|
1046
|
+
let lastReason = "all_attempts_failed";
|
|
1047
|
+
let lastEnhancedStatus;
|
|
1048
|
+
let lastResponseCode;
|
|
1049
|
+
for (const mxHost of mxRecords) {
|
|
1050
|
+
mxHostsTried.push(mxHost);
|
|
1051
|
+
mxAttempts++;
|
|
1052
|
+
const portsForThisMx = mxHost === primaryMx && cachedPort ? [cachedPort, ...ports.filter((p) => p !== cachedPort)] : ports;
|
|
1053
|
+
for (const port of portsForThisMx) {
|
|
1054
|
+
portAttempts++;
|
|
1055
|
+
log(`Testing ${mxHost}:${port}`);
|
|
1056
|
+
const probe = await runProbe({ ...probeOptions, mxHost, port });
|
|
1057
|
+
collectTranscript(transcript, commands, probe, mxHost, port);
|
|
1058
|
+
lastReason = probe.reason;
|
|
1059
|
+
if (probe.enhancedStatus !== void 0)
|
|
1060
|
+
lastEnhancedStatus = probe.enhancedStatus;
|
|
1061
|
+
if (probe.responseCode !== void 0)
|
|
1062
|
+
lastResponseCode = probe.responseCode;
|
|
1063
|
+
if (probe.result !== null) {
|
|
1064
|
+
const metrics2 = makeMetrics(mxHostsTried, mxAttempts, portAttempts, mxHost, startedAtMs);
|
|
1065
|
+
const smtpResult2 = toSmtpVerificationResult(probe, {
|
|
1066
|
+
transcript: captureTranscript ? transcript : void 0,
|
|
1067
|
+
commands: captureTranscript ? commands : void 0,
|
|
1068
|
+
metrics: metrics2
|
|
1069
|
+
});
|
|
1070
|
+
await safeCacheSet(verdictCache, verdictKey, smtpResult2);
|
|
1071
|
+
if (mxHost === primaryMx)
|
|
1072
|
+
await safeCacheSet(portCache, primaryMx, port);
|
|
1073
|
+
return { smtpResult: smtpResult2, cached: false, port, portCached: cachedPort === port };
|
|
1074
|
+
}
|
|
1052
1075
|
}
|
|
1053
1076
|
}
|
|
1054
|
-
log(
|
|
1077
|
+
log(`All MX\xD7port attempts failed (mx=${mxAttempts}, port=${portAttempts})`);
|
|
1078
|
+
const metrics = makeMetrics(mxHostsTried, mxAttempts, portAttempts, void 0, startedAtMs);
|
|
1079
|
+
const smtpResult = {
|
|
1080
|
+
...failureResult(lastReason, metrics),
|
|
1081
|
+
...lastEnhancedStatus !== void 0 ? { enhancedStatus: lastEnhancedStatus } : {},
|
|
1082
|
+
...lastResponseCode !== void 0 ? { responseCode: lastResponseCode } : {},
|
|
1083
|
+
...captureTranscript ? { transcript: [...transcript], commands: [...commands] } : {}
|
|
1084
|
+
};
|
|
1085
|
+
return { smtpResult, cached: false, port: 0, portCached: false };
|
|
1086
|
+
}
|
|
1087
|
+
function makeMetrics(mxHostsTried, mxAttempts, portAttempts, mxHostUsed, startedAtMs) {
|
|
1055
1088
|
return {
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
},
|
|
1060
|
-
|
|
1061
|
-
port: 0,
|
|
1062
|
-
portCached: false
|
|
1089
|
+
mxAttempts,
|
|
1090
|
+
portAttempts,
|
|
1091
|
+
mxHostsTried: [...mxHostsTried],
|
|
1092
|
+
...mxHostUsed !== void 0 ? { mxHostUsed } : {},
|
|
1093
|
+
totalDurationMs: Date.now() - startedAtMs
|
|
1063
1094
|
};
|
|
1064
1095
|
}
|
|
1065
|
-
function collectTranscript(transcript, commands, probe, port) {
|
|
1096
|
+
function collectTranscript(transcript, commands, probe, mxHost, port) {
|
|
1097
|
+
const prefix = `${mxHost}:${port}`;
|
|
1066
1098
|
for (const line of probe.transcript)
|
|
1067
|
-
transcript.push(`${
|
|
1099
|
+
transcript.push(`${prefix}|s| ${line}`);
|
|
1068
1100
|
for (const cmd of probe.commands)
|
|
1069
|
-
commands.push(`${
|
|
1101
|
+
commands.push(`${prefix}|c| ${cmd}`);
|
|
1070
1102
|
}
|
|
1071
|
-
function failureResult(
|
|
1103
|
+
function failureResult(reason, metrics) {
|
|
1072
1104
|
return {
|
|
1073
1105
|
canConnectSmtp: false,
|
|
1074
1106
|
hasFullInbox: false,
|
|
1075
1107
|
isCatchAll: false,
|
|
1076
1108
|
isDeliverable: false,
|
|
1077
1109
|
isDisabled: false,
|
|
1078
|
-
error,
|
|
1079
|
-
|
|
1080
|
-
|
|
1110
|
+
error: reason,
|
|
1111
|
+
checkedAt: Date.now(),
|
|
1112
|
+
metrics
|
|
1081
1113
|
};
|
|
1082
1114
|
}
|
|
1083
|
-
function toSmtpVerificationResult(
|
|
1084
|
-
|
|
1115
|
+
function toSmtpVerificationResult(probe, extras) {
|
|
1116
|
+
var _a;
|
|
1117
|
+
const result = probe.result;
|
|
1118
|
+
const out = {
|
|
1085
1119
|
canConnectSmtp: result !== null,
|
|
1086
|
-
hasFullInbox:
|
|
1087
|
-
isCatchAll: false,
|
|
1120
|
+
hasFullInbox: probe.reason === "over_quota",
|
|
1121
|
+
isCatchAll: (_a = probe.isCatchAll) !== null && _a !== void 0 ? _a : false,
|
|
1088
1122
|
isDeliverable: result === true,
|
|
1089
1123
|
isDisabled: result === false,
|
|
1090
|
-
error: result === true ? void 0 :
|
|
1091
|
-
|
|
1092
|
-
|
|
1124
|
+
error: result === true ? void 0 : probe.reason,
|
|
1125
|
+
checkedAt: Date.now(),
|
|
1126
|
+
metrics: extras.metrics,
|
|
1127
|
+
...probe.enhancedStatus !== void 0 ? { enhancedStatus: probe.enhancedStatus } : {},
|
|
1128
|
+
...probe.responseCode !== void 0 ? { responseCode: probe.responseCode } : {}
|
|
1093
1129
|
};
|
|
1094
|
-
if (
|
|
1095
|
-
|
|
1096
|
-
|
|
1130
|
+
if (extras.transcript)
|
|
1131
|
+
out.transcript = [...extras.transcript];
|
|
1132
|
+
if (extras.commands)
|
|
1133
|
+
out.commands = [...extras.commands];
|
|
1134
|
+
return out;
|
|
1097
1135
|
}
|
|
1098
1136
|
async function safeCacheGet(store, key) {
|
|
1099
1137
|
if (!store)
|
|
@@ -1123,8 +1161,17 @@ class SMTPProbeConnection {
|
|
|
1123
1161
|
this.buffer = "";
|
|
1124
1162
|
this.resolved = false;
|
|
1125
1163
|
this.currentStepIndex = 0;
|
|
1164
|
+
this.tlsUpgrading = false;
|
|
1165
|
+
this.postUpgradeReEhlo = false;
|
|
1126
1166
|
this.transcript = [];
|
|
1127
1167
|
this.commands = [];
|
|
1168
|
+
this.supportsPipelining = false;
|
|
1169
|
+
this.supportsStartTls = false;
|
|
1170
|
+
this.dualPhase = "idle";
|
|
1171
|
+
this.realOutcome = "pending";
|
|
1172
|
+
this.probeOutcome = "pending";
|
|
1173
|
+
this.dualPipelined = false;
|
|
1174
|
+
this.pendingDecision = null;
|
|
1128
1175
|
this.onData = (data) => {
|
|
1129
1176
|
if (this.resolved)
|
|
1130
1177
|
return;
|
|
@@ -1137,7 +1184,7 @@ class SMTPProbeConnection {
|
|
|
1137
1184
|
this.processLine(line);
|
|
1138
1185
|
}
|
|
1139
1186
|
};
|
|
1140
|
-
const defaultSteps = [SMTPStep.greeting, SMTPStep.ehlo, SMTPStep.mailFrom, SMTPStep.rcptTo];
|
|
1187
|
+
const defaultSteps = [SMTPStep.greeting, SMTPStep.ehlo, SMTPStep.startTls, SMTPStep.mailFrom, SMTPStep.rcptTo];
|
|
1141
1188
|
this.steps = [...(_b = (_a = p.sequence) === null || _a === void 0 ? void 0 : _a.steps) !== null && _b !== void 0 ? _b : defaultSteps];
|
|
1142
1189
|
this.isTLS = PORT_TLS[p.port] === true;
|
|
1143
1190
|
const servername = isIPAddress(p.mxHost) ? void 0 : p.mxHost;
|
|
@@ -1148,6 +1195,7 @@ class SMTPProbeConnection {
|
|
|
1148
1195
|
minVersion: "TLSv1.2",
|
|
1149
1196
|
...typeof p.tlsConfig === "object" ? p.tlsConfig : {}
|
|
1150
1197
|
};
|
|
1198
|
+
this.probeLocal = p.catchAllProbeLocal ? p.catchAllProbeLocal(p.local, p.domain) : defaultProbeLocal();
|
|
1151
1199
|
}
|
|
1152
1200
|
run() {
|
|
1153
1201
|
return new Promise((resolve) => {
|
|
@@ -1156,7 +1204,8 @@ class SMTPProbeConnection {
|
|
|
1156
1204
|
this.connect();
|
|
1157
1205
|
this.armConnectionTimer();
|
|
1158
1206
|
} catch (error) {
|
|
1159
|
-
this.
|
|
1207
|
+
this.p.log(`connect threw: ${error instanceof Error ? error.message : "unknown"}`);
|
|
1208
|
+
this.finish(null, "connection_error");
|
|
1160
1209
|
}
|
|
1161
1210
|
});
|
|
1162
1211
|
}
|
|
@@ -1208,51 +1257,173 @@ class SMTPProbeConnection {
|
|
|
1208
1257
|
switch (step) {
|
|
1209
1258
|
case SMTPStep.greeting:
|
|
1210
1259
|
return;
|
|
1260
|
+
// server-driven; nothing to send
|
|
1211
1261
|
case SMTPStep.ehlo:
|
|
1212
1262
|
this.send(`EHLO ${this.p.hostname}`);
|
|
1213
1263
|
return;
|
|
1214
1264
|
case SMTPStep.helo:
|
|
1215
1265
|
this.send(`HELO ${this.p.hostname}`);
|
|
1216
1266
|
return;
|
|
1267
|
+
case SMTPStep.startTls:
|
|
1268
|
+
this.executeStartTls();
|
|
1269
|
+
return;
|
|
1217
1270
|
case SMTPStep.mailFrom: {
|
|
1218
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}>`;
|
|
1219
1272
|
this.send(`MAIL FROM:${from}`);
|
|
1220
1273
|
return;
|
|
1221
1274
|
}
|
|
1222
1275
|
case SMTPStep.rcptTo:
|
|
1223
|
-
this.
|
|
1276
|
+
this.executeEnvelope();
|
|
1224
1277
|
return;
|
|
1225
1278
|
}
|
|
1226
1279
|
}
|
|
1227
|
-
|
|
1228
|
-
|
|
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();
|
|
1229
1299
|
return;
|
|
1230
|
-
|
|
1231
|
-
this.
|
|
1232
|
-
|
|
1233
|
-
|
|
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");
|
|
1234
1315
|
return;
|
|
1235
1316
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
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
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Send the dual-probe envelope (real RCPT + probe RCPT + RSET). Pipelined
|
|
1349
|
+
* when the MX advertised PIPELINING (or `pipelining: 'force'`); sequential
|
|
1350
|
+
* otherwise.
|
|
1351
|
+
*/
|
|
1352
|
+
executeEnvelope() {
|
|
1353
|
+
var _a;
|
|
1354
|
+
const wantsPipelining = this.p.pipelining === "force" || this.p.pipelining === "auto" && this.supportsPipelining;
|
|
1355
|
+
const realCmd = `RCPT TO:<${this.p.local}@${this.p.domain}>`;
|
|
1356
|
+
if (wantsPipelining) {
|
|
1357
|
+
this.dualPipelined = true;
|
|
1358
|
+
const probeCmd = `RCPT TO:<${this.probeLocal}@${this.p.domain}>`;
|
|
1359
|
+
const rsetCmd = "RSET";
|
|
1360
|
+
this.commands.push(realCmd, probeCmd, rsetCmd);
|
|
1361
|
+
this.p.log(`\u2192 ${realCmd}`);
|
|
1362
|
+
this.p.log(`\u2192 ${probeCmd}`);
|
|
1363
|
+
this.p.log(`\u2192 ${rsetCmd}`);
|
|
1364
|
+
(_a = this.socket) === null || _a === void 0 ? void 0 : _a.write(`${realCmd}\r
|
|
1365
|
+
${probeCmd}\r
|
|
1366
|
+
${rsetCmd}\r
|
|
1367
|
+
`);
|
|
1368
|
+
this.dualPhase = "rcpt_real";
|
|
1238
1369
|
return;
|
|
1239
1370
|
}
|
|
1240
|
-
|
|
1241
|
-
|
|
1371
|
+
this.dualPipelined = false;
|
|
1372
|
+
this.send(realCmd);
|
|
1373
|
+
this.dualPhase = "rcpt_real";
|
|
1374
|
+
}
|
|
1375
|
+
processLine(line) {
|
|
1376
|
+
if (this.resolved)
|
|
1242
1377
|
return;
|
|
1378
|
+
this.transcript.push(line);
|
|
1379
|
+
this.p.log(`\u2190 ${line}`);
|
|
1380
|
+
const codeStr = line.slice(0, 3);
|
|
1381
|
+
const numericCode = /^\d{3}$/.test(codeStr) ? parseInt(codeStr, 10) : null;
|
|
1382
|
+
if (numericCode !== null)
|
|
1383
|
+
this.lastResponseCode = numericCode;
|
|
1384
|
+
const dsn = parseDsn(line);
|
|
1385
|
+
if (dsn)
|
|
1386
|
+
this.lastEnhancedStatus = dsnToString(dsn);
|
|
1387
|
+
if (this.dualPhase === "idle" || this.dualPhase === "rcpt_real") {
|
|
1388
|
+
if (isHighVolume(line)) {
|
|
1389
|
+
this.finish(true, "high_volume");
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
if (isOverQuota(line)) {
|
|
1393
|
+
this.isCatchAllFlag = false;
|
|
1394
|
+
this.finish(false, "over_quota");
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
if (isInvalidMailboxError(line)) {
|
|
1398
|
+
this.isCatchAllFlag = false;
|
|
1399
|
+
this.finish(false, "not_found");
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1243
1402
|
}
|
|
1244
|
-
if (MULTILINE_RE.test(line))
|
|
1403
|
+
if (MULTILINE_RE.test(line)) {
|
|
1404
|
+
const step = this.steps[this.currentStepIndex];
|
|
1405
|
+
const isEhloLike = step === SMTPStep.ehlo || step === SMTPStep.helo || step === SMTPStep.startTls && this.postUpgradeReEhlo;
|
|
1406
|
+
if (isEhloLike && line.startsWith("250-")) {
|
|
1407
|
+
const upper = line.toUpperCase();
|
|
1408
|
+
if (upper.includes("PIPELINING"))
|
|
1409
|
+
this.supportsPipelining = true;
|
|
1410
|
+
if (upper.includes("STARTTLS"))
|
|
1411
|
+
this.supportsStartTls = true;
|
|
1412
|
+
}
|
|
1245
1413
|
return;
|
|
1246
|
-
|
|
1247
|
-
const numericCode = /^\d{3}$/.test(code) ? parseInt(code, 10) : null;
|
|
1414
|
+
}
|
|
1248
1415
|
if (numericCode === null) {
|
|
1249
1416
|
this.finish(null, "unrecognized_response");
|
|
1250
1417
|
return;
|
|
1251
1418
|
}
|
|
1252
|
-
this.dispatch(numericCode);
|
|
1419
|
+
this.dispatch(numericCode, line);
|
|
1253
1420
|
}
|
|
1254
|
-
dispatch(code) {
|
|
1421
|
+
dispatch(code, line) {
|
|
1255
1422
|
const step = this.steps[this.currentStepIndex];
|
|
1423
|
+
if (this.dualPhase !== "idle" && step === SMTPStep.rcptTo) {
|
|
1424
|
+
this.handleEnvelopeReply(code, line);
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1256
1427
|
switch (step) {
|
|
1257
1428
|
case SMTPStep.greeting:
|
|
1258
1429
|
if (code === 220)
|
|
@@ -1272,6 +1443,21 @@ class SMTPProbeConnection {
|
|
|
1272
1443
|
else
|
|
1273
1444
|
this.finish(null, "helo_failed");
|
|
1274
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;
|
|
1275
1461
|
case SMTPStep.mailFrom:
|
|
1276
1462
|
if (code === 250)
|
|
1277
1463
|
this.nextStep();
|
|
@@ -1279,15 +1465,91 @@ class SMTPProbeConnection {
|
|
|
1279
1465
|
this.finish(null, "mail_from_rejected");
|
|
1280
1466
|
return;
|
|
1281
1467
|
case SMTPStep.rcptTo:
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1468
|
+
this.handleEnvelopeReply(code, line);
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* Dual-probe / pipelined-envelope reply router. Demuxes server replies for
|
|
1474
|
+
* the three queued commands (real RCPT, probe RCPT, RSET) and resolves
|
|
1475
|
+
* with the catch-all-aware verdict.
|
|
1476
|
+
*/
|
|
1477
|
+
handleEnvelopeReply(code, line) {
|
|
1478
|
+
if (this.dualPhase === "rcpt_real") {
|
|
1479
|
+
this.realOutcome = classifyRcpt(code);
|
|
1480
|
+
if (code === 552 || code === 452 || isOverQuota(line)) {
|
|
1481
|
+
this.isCatchAllFlag = false;
|
|
1482
|
+
if (this.dualPipelined) {
|
|
1483
|
+
this.pendingDecision = { result: false, reason: "over_quota" };
|
|
1484
|
+
this.dualPhase = "rcpt_probe";
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
this.finish(false, "over_quota");
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
if (this.realOutcome === "soft_reject") {
|
|
1491
|
+
if (this.dualPipelined) {
|
|
1492
|
+
this.pendingDecision = { result: null, reason: "temporary_failure" };
|
|
1493
|
+
this.dualPhase = "rcpt_probe";
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
this.finish(null, "temporary_failure");
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
if (this.realOutcome === "hard_reject") {
|
|
1500
|
+
const reason = isInvalidMailboxError(line) ? "not_found" : "ambiguous";
|
|
1501
|
+
const result = reason === "not_found" ? false : null;
|
|
1502
|
+
this.isCatchAllFlag = false;
|
|
1503
|
+
if (this.dualPipelined) {
|
|
1504
|
+
this.pendingDecision = { result, reason };
|
|
1505
|
+
this.dualPhase = "rcpt_probe";
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
this.finish(result, reason);
|
|
1290
1509
|
return;
|
|
1510
|
+
}
|
|
1511
|
+
if (this.dualPipelined) {
|
|
1512
|
+
this.dualPhase = "rcpt_probe";
|
|
1513
|
+
} else {
|
|
1514
|
+
this.send(`RCPT TO:<${this.probeLocal}@${this.p.domain}>`);
|
|
1515
|
+
this.dualPhase = "rcpt_probe";
|
|
1516
|
+
}
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
if (this.dualPhase === "rcpt_probe") {
|
|
1520
|
+
this.probeOutcome = classifyRcpt(code);
|
|
1521
|
+
if (this.dualPipelined) {
|
|
1522
|
+
this.dualPhase = "rset";
|
|
1523
|
+
} else {
|
|
1524
|
+
this.send("RSET");
|
|
1525
|
+
this.dualPhase = "rset";
|
|
1526
|
+
}
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
if (this.dualPhase === "rset") {
|
|
1530
|
+
if (this.pendingDecision) {
|
|
1531
|
+
this.finish(this.pendingDecision.result, this.pendingDecision.reason);
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
this.decideDualProbe();
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
/** Final decision after both RCPT outcomes are known. Catch-all only when both 250. */
|
|
1539
|
+
decideDualProbe() {
|
|
1540
|
+
if (this.realOutcome === "accept" && this.probeOutcome === "accept") {
|
|
1541
|
+
this.isCatchAllFlag = true;
|
|
1542
|
+
this.finish(true, "valid");
|
|
1543
|
+
} else if (this.realOutcome === "accept") {
|
|
1544
|
+
this.isCatchAllFlag = false;
|
|
1545
|
+
this.finish(true, "valid");
|
|
1546
|
+
} else if (this.realOutcome === "hard_reject") {
|
|
1547
|
+
this.isCatchAllFlag = false;
|
|
1548
|
+
this.finish(false, "not_found");
|
|
1549
|
+
} else if (this.realOutcome === "soft_reject") {
|
|
1550
|
+
this.finish(null, "temporary_failure");
|
|
1551
|
+
} else {
|
|
1552
|
+
this.finish(null, "ambiguous");
|
|
1291
1553
|
}
|
|
1292
1554
|
}
|
|
1293
1555
|
finish(result, reason) {
|
|
@@ -1313,9 +1575,24 @@ class SMTPProbeConnection {
|
|
|
1313
1575
|
return (_a2 = this.socket) === null || _a2 === void 0 ? void 0 : _a2.destroy();
|
|
1314
1576
|
}, QUIT_DRAIN_MS);
|
|
1315
1577
|
(_c = drain.unref) === null || _c === void 0 ? void 0 : _c.call(drain);
|
|
1316
|
-
this.resolveFn({
|
|
1578
|
+
this.resolveFn({
|
|
1579
|
+
result,
|
|
1580
|
+
reason,
|
|
1581
|
+
...this.lastEnhancedStatus !== void 0 ? { enhancedStatus: this.lastEnhancedStatus } : {},
|
|
1582
|
+
...this.lastResponseCode !== void 0 ? { responseCode: this.lastResponseCode } : {},
|
|
1583
|
+
...this.isCatchAllFlag !== void 0 ? { isCatchAll: this.isCatchAllFlag } : {},
|
|
1584
|
+
transcript: this.transcript,
|
|
1585
|
+
commands: this.commands
|
|
1586
|
+
});
|
|
1317
1587
|
}
|
|
1318
1588
|
}
|
|
1589
|
+
function classifyRcpt(code) {
|
|
1590
|
+
if (code === 250 || code === 251)
|
|
1591
|
+
return "accept";
|
|
1592
|
+
if (code >= 400 && code < 500)
|
|
1593
|
+
return "soft_reject";
|
|
1594
|
+
return "hard_reject";
|
|
1595
|
+
}
|
|
1319
1596
|
|
|
1320
1597
|
class ArrayTranscriptCollector {
|
|
1321
1598
|
constructor() {
|
|
@@ -1988,12 +2265,11 @@ async function verifyEmail(params) {
|
|
|
1988
2265
|
const skipMx = ((_f = params.skipMxForDisposable) !== null && _f !== void 0 ? _f : false) && result.isDisposable;
|
|
1989
2266
|
const skipWhois = ((_g = params.skipDomainWhoisForDisposable) !== null && _g !== void 0 ? _g : false) && result.isDisposable;
|
|
1990
2267
|
await runWhoisChecks(domain, params, result, skipWhois, log, collector);
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
}
|
|
2268
|
+
const wantsMxOrSmtp = ((_h = params.verifyMx) !== null && _h !== void 0 ? _h : true) || ((_j = params.verifySmtp) !== null && _j !== void 0 ? _j : false);
|
|
2269
|
+
if (wantsMxOrSmtp && skipMx) {
|
|
2270
|
+
log(`[verifyEmail] skipping MX/SMTP for disposable: ${params.emailAddress}`);
|
|
2271
|
+
} else if (wantsMxOrSmtp) {
|
|
2272
|
+
await runMxAndSmtp(local, domain, params, result, log, collector);
|
|
1997
2273
|
}
|
|
1998
2274
|
result.metadata.verificationTime = Date.now() - startTime;
|
|
1999
2275
|
if (captureTranscript)
|
|
@@ -2343,6 +2619,36 @@ function isSpamName(name) {
|
|
|
2343
2619
|
return true;
|
|
2344
2620
|
}
|
|
2345
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
|
+
|
|
2346
2652
|
const NETWORK_ERROR_PATTERNS = [
|
|
2347
2653
|
"etimedout",
|
|
2348
2654
|
"econnrefused",
|
|
@@ -2446,5 +2752,5 @@ function parseSmtpError(errorMessage) {
|
|
|
2446
2752
|
return { isDisabled, hasFullInbox, isCatchAll, isInvalid };
|
|
2447
2753
|
}
|
|
2448
2754
|
|
|
2449
|
-
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 };
|
|
2450
2756
|
//# sourceMappingURL=index.esm.js.map
|