@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.js
CHANGED
|
@@ -4,6 +4,7 @@ var tinyLru = require('tiny-lru');
|
|
|
4
4
|
var psl = require('psl');
|
|
5
5
|
var stringSimilarityJs = require('string-similarity-js');
|
|
6
6
|
var node_dns = require('node:dns');
|
|
7
|
+
var node_crypto = require('node:crypto');
|
|
7
8
|
var net$1 = require('node:net');
|
|
8
9
|
var tls = require('node:tls');
|
|
9
10
|
|
|
@@ -196,6 +197,7 @@ exports.SMTPStep = void 0;
|
|
|
196
197
|
SMTPStep2["greeting"] = "GREETING";
|
|
197
198
|
SMTPStep2["ehlo"] = "EHLO";
|
|
198
199
|
SMTPStep2["helo"] = "HELO";
|
|
200
|
+
SMTPStep2["startTls"] = "STARTTLS";
|
|
199
201
|
SMTPStep2["mailFrom"] = "MAIL_FROM";
|
|
200
202
|
SMTPStep2["rcptTo"] = "RCPT_TO";
|
|
201
203
|
})(exports.SMTPStep || (exports.SMTPStep = {}));
|
|
@@ -680,9 +682,9 @@ function parseCompositeNamePart(part) {
|
|
|
680
682
|
cleaned = pureAlpha;
|
|
681
683
|
confidence = 0.6;
|
|
682
684
|
} else {
|
|
683
|
-
const
|
|
684
|
-
if (
|
|
685
|
-
cleaned =
|
|
685
|
+
const baseMatch = part.match(/^([a-zA-Z]+)\d*$/);
|
|
686
|
+
if (baseMatch) {
|
|
687
|
+
cleaned = baseMatch[1];
|
|
686
688
|
confidence = 0.75;
|
|
687
689
|
} else {
|
|
688
690
|
cleaned = part;
|
|
@@ -696,9 +698,7 @@ function parseCompositeNamePart(part) {
|
|
|
696
698
|
confidence = Math.min(1, confidence + 0.2);
|
|
697
699
|
}
|
|
698
700
|
}
|
|
699
|
-
|
|
700
|
-
const base = baseMatch ? baseMatch[1] : part;
|
|
701
|
-
return { base, hasNumbers, cleaned, confidence };
|
|
701
|
+
return { hasNumbers, cleaned, confidence };
|
|
702
702
|
}
|
|
703
703
|
function isLikelyName(str, allowNumbers = false, allowSingleLetter = false) {
|
|
704
704
|
if (!str)
|
|
@@ -989,6 +989,9 @@ function parseDsn(reply) {
|
|
|
989
989
|
return null;
|
|
990
990
|
return { class: Number(match[1]), subject: Number(match[2]), detail: Number(match[3]) };
|
|
991
991
|
}
|
|
992
|
+
function dsnToString(dsn) {
|
|
993
|
+
return `${dsn.class}.${dsn.subject}.${dsn.detail}`;
|
|
994
|
+
}
|
|
992
995
|
function isPolicyBlock(reply) {
|
|
993
996
|
const dsn = parseDsn(reply);
|
|
994
997
|
return (dsn === null || dsn === void 0 ? void 0 : dsn.class) === 5 && (dsn === null || dsn === void 0 ? void 0 : dsn.subject) === 7;
|
|
@@ -1009,8 +1012,11 @@ function isInvalidMailboxError(reply) {
|
|
|
1009
1012
|
return false;
|
|
1010
1013
|
return true;
|
|
1011
1014
|
}
|
|
1015
|
+
function defaultProbeLocal() {
|
|
1016
|
+
return `${node_crypto.randomBytes(8).toString("hex")}-noexist`;
|
|
1017
|
+
}
|
|
1012
1018
|
async function verifyMailboxSMTP(params) {
|
|
1013
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
1019
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
1014
1020
|
const { local, domain, options = {} } = params;
|
|
1015
1021
|
const mxRecords = (_a = params.mxRecords) !== null && _a !== void 0 ? _a : [];
|
|
1016
1022
|
const ports = ((_b = options.ports) !== null && _b !== void 0 ? _b : DEFAULT_PORTS).filter((port) => Number.isInteger(port) && port > 0 && port < 65536);
|
|
@@ -1023,16 +1029,30 @@ async function verifyMailboxSMTP(params) {
|
|
|
1023
1029
|
const cache = options.cache;
|
|
1024
1030
|
const log = debug ? (...args) => console.log("[SMTP]", ...args) : () => {
|
|
1025
1031
|
};
|
|
1026
|
-
const
|
|
1027
|
-
|
|
1032
|
+
const startedAtMs = Date.now();
|
|
1033
|
+
const primaryMx = mxRecords[0];
|
|
1034
|
+
if (!primaryMx) {
|
|
1028
1035
|
log("No MX records found");
|
|
1029
|
-
|
|
1036
|
+
const metrics2 = makeMetrics([], 0, 0, void 0, startedAtMs);
|
|
1037
|
+
return { smtpResult: failureResult("no_mx_records", metrics2), cached: false, port: 0, portCached: false };
|
|
1030
1038
|
}
|
|
1031
|
-
log(`Verifying ${local}@${domain} via ${
|
|
1039
|
+
log(`Verifying ${local}@${domain} via ${primaryMx} (mx count=${mxRecords.length})`);
|
|
1032
1040
|
const transcript = [];
|
|
1033
1041
|
const commands = [];
|
|
1042
|
+
const probeOptions = {
|
|
1043
|
+
local,
|
|
1044
|
+
domain,
|
|
1045
|
+
timeout,
|
|
1046
|
+
tlsConfig,
|
|
1047
|
+
hostname,
|
|
1048
|
+
sequence,
|
|
1049
|
+
log,
|
|
1050
|
+
catchAllProbeLocal: options.catchAllProbeLocal,
|
|
1051
|
+
pipelining: (_h = options.pipelining) !== null && _h !== void 0 ? _h : "auto",
|
|
1052
|
+
startTls: (_j = options.startTls) !== null && _j !== void 0 ? _j : "auto"
|
|
1053
|
+
};
|
|
1034
1054
|
const verdictCache = cache ? getCacheStore(cache, "smtp") : null;
|
|
1035
|
-
const verdictKey = `${
|
|
1055
|
+
const verdictKey = `${primaryMx}:${local}@${domain}`;
|
|
1036
1056
|
if (verdictCache) {
|
|
1037
1057
|
const cachedResult = await safeCacheGet(verdictCache, verdictKey);
|
|
1038
1058
|
if (cachedResult) {
|
|
@@ -1041,81 +1061,99 @@ async function verifyMailboxSMTP(params) {
|
|
|
1041
1061
|
}
|
|
1042
1062
|
}
|
|
1043
1063
|
const portCache = cache ? getCacheStore(cache, "smtpPort") : null;
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
});
|
|
1059
|
-
collectTranscript(transcript, commands, probe,
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1064
|
+
const cachedPort = portCache ? await safeCacheGet(portCache, primaryMx) : null;
|
|
1065
|
+
const mxHostsTried = [];
|
|
1066
|
+
let mxAttempts = 0;
|
|
1067
|
+
let portAttempts = 0;
|
|
1068
|
+
let lastReason = "all_attempts_failed";
|
|
1069
|
+
let lastEnhancedStatus;
|
|
1070
|
+
let lastResponseCode;
|
|
1071
|
+
for (const mxHost of mxRecords) {
|
|
1072
|
+
mxHostsTried.push(mxHost);
|
|
1073
|
+
mxAttempts++;
|
|
1074
|
+
const portsForThisMx = mxHost === primaryMx && cachedPort ? [cachedPort, ...ports.filter((p) => p !== cachedPort)] : ports;
|
|
1075
|
+
for (const port of portsForThisMx) {
|
|
1076
|
+
portAttempts++;
|
|
1077
|
+
log(`Testing ${mxHost}:${port}`);
|
|
1078
|
+
const probe = await runProbe({ ...probeOptions, mxHost, port });
|
|
1079
|
+
collectTranscript(transcript, commands, probe, mxHost, port);
|
|
1080
|
+
lastReason = probe.reason;
|
|
1081
|
+
if (probe.enhancedStatus !== void 0)
|
|
1082
|
+
lastEnhancedStatus = probe.enhancedStatus;
|
|
1083
|
+
if (probe.responseCode !== void 0)
|
|
1084
|
+
lastResponseCode = probe.responseCode;
|
|
1085
|
+
if (probe.result !== null) {
|
|
1086
|
+
const metrics2 = makeMetrics(mxHostsTried, mxAttempts, portAttempts, mxHost, startedAtMs);
|
|
1087
|
+
const smtpResult2 = toSmtpVerificationResult(probe, {
|
|
1088
|
+
transcript: captureTranscript ? transcript : void 0,
|
|
1089
|
+
commands: captureTranscript ? commands : void 0,
|
|
1090
|
+
metrics: metrics2
|
|
1091
|
+
});
|
|
1092
|
+
await safeCacheSet(verdictCache, verdictKey, smtpResult2);
|
|
1093
|
+
if (mxHost === primaryMx)
|
|
1094
|
+
await safeCacheSet(portCache, primaryMx, port);
|
|
1095
|
+
return { smtpResult: smtpResult2, cached: false, port, portCached: cachedPort === port };
|
|
1096
|
+
}
|
|
1074
1097
|
}
|
|
1075
1098
|
}
|
|
1076
|
-
log(
|
|
1099
|
+
log(`All MX\xD7port attempts failed (mx=${mxAttempts}, port=${portAttempts})`);
|
|
1100
|
+
const metrics = makeMetrics(mxHostsTried, mxAttempts, portAttempts, void 0, startedAtMs);
|
|
1101
|
+
const smtpResult = {
|
|
1102
|
+
...failureResult(lastReason, metrics),
|
|
1103
|
+
...lastEnhancedStatus !== void 0 ? { enhancedStatus: lastEnhancedStatus } : {},
|
|
1104
|
+
...lastResponseCode !== void 0 ? { responseCode: lastResponseCode } : {},
|
|
1105
|
+
...captureTranscript ? { transcript: [...transcript], commands: [...commands] } : {}
|
|
1106
|
+
};
|
|
1107
|
+
return { smtpResult, cached: false, port: 0, portCached: false };
|
|
1108
|
+
}
|
|
1109
|
+
function makeMetrics(mxHostsTried, mxAttempts, portAttempts, mxHostUsed, startedAtMs) {
|
|
1077
1110
|
return {
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
},
|
|
1082
|
-
|
|
1083
|
-
port: 0,
|
|
1084
|
-
portCached: false
|
|
1111
|
+
mxAttempts,
|
|
1112
|
+
portAttempts,
|
|
1113
|
+
mxHostsTried: [...mxHostsTried],
|
|
1114
|
+
...mxHostUsed !== void 0 ? { mxHostUsed } : {},
|
|
1115
|
+
totalDurationMs: Date.now() - startedAtMs
|
|
1085
1116
|
};
|
|
1086
1117
|
}
|
|
1087
|
-
function collectTranscript(transcript, commands, probe, port) {
|
|
1118
|
+
function collectTranscript(transcript, commands, probe, mxHost, port) {
|
|
1119
|
+
const prefix = `${mxHost}:${port}`;
|
|
1088
1120
|
for (const line of probe.transcript)
|
|
1089
|
-
transcript.push(`${
|
|
1121
|
+
transcript.push(`${prefix}|s| ${line}`);
|
|
1090
1122
|
for (const cmd of probe.commands)
|
|
1091
|
-
commands.push(`${
|
|
1123
|
+
commands.push(`${prefix}|c| ${cmd}`);
|
|
1092
1124
|
}
|
|
1093
|
-
function failureResult(
|
|
1125
|
+
function failureResult(reason, metrics) {
|
|
1094
1126
|
return {
|
|
1095
1127
|
canConnectSmtp: false,
|
|
1096
1128
|
hasFullInbox: false,
|
|
1097
1129
|
isCatchAll: false,
|
|
1098
1130
|
isDeliverable: false,
|
|
1099
1131
|
isDisabled: false,
|
|
1100
|
-
error,
|
|
1101
|
-
|
|
1102
|
-
|
|
1132
|
+
error: reason,
|
|
1133
|
+
checkedAt: Date.now(),
|
|
1134
|
+
metrics
|
|
1103
1135
|
};
|
|
1104
1136
|
}
|
|
1105
|
-
function toSmtpVerificationResult(
|
|
1106
|
-
|
|
1137
|
+
function toSmtpVerificationResult(probe, extras) {
|
|
1138
|
+
var _a;
|
|
1139
|
+
const result = probe.result;
|
|
1140
|
+
const out = {
|
|
1107
1141
|
canConnectSmtp: result !== null,
|
|
1108
|
-
hasFullInbox:
|
|
1109
|
-
isCatchAll: false,
|
|
1142
|
+
hasFullInbox: probe.reason === "over_quota",
|
|
1143
|
+
isCatchAll: (_a = probe.isCatchAll) !== null && _a !== void 0 ? _a : false,
|
|
1110
1144
|
isDeliverable: result === true,
|
|
1111
1145
|
isDisabled: result === false,
|
|
1112
|
-
error: result === true ? void 0 :
|
|
1113
|
-
|
|
1114
|
-
|
|
1146
|
+
error: result === true ? void 0 : probe.reason,
|
|
1147
|
+
checkedAt: Date.now(),
|
|
1148
|
+
metrics: extras.metrics,
|
|
1149
|
+
...probe.enhancedStatus !== void 0 ? { enhancedStatus: probe.enhancedStatus } : {},
|
|
1150
|
+
...probe.responseCode !== void 0 ? { responseCode: probe.responseCode } : {}
|
|
1115
1151
|
};
|
|
1116
|
-
if (
|
|
1117
|
-
|
|
1118
|
-
|
|
1152
|
+
if (extras.transcript)
|
|
1153
|
+
out.transcript = [...extras.transcript];
|
|
1154
|
+
if (extras.commands)
|
|
1155
|
+
out.commands = [...extras.commands];
|
|
1156
|
+
return out;
|
|
1119
1157
|
}
|
|
1120
1158
|
async function safeCacheGet(store, key) {
|
|
1121
1159
|
if (!store)
|
|
@@ -1145,8 +1183,17 @@ class SMTPProbeConnection {
|
|
|
1145
1183
|
this.buffer = "";
|
|
1146
1184
|
this.resolved = false;
|
|
1147
1185
|
this.currentStepIndex = 0;
|
|
1186
|
+
this.tlsUpgrading = false;
|
|
1187
|
+
this.postUpgradeReEhlo = false;
|
|
1148
1188
|
this.transcript = [];
|
|
1149
1189
|
this.commands = [];
|
|
1190
|
+
this.supportsPipelining = false;
|
|
1191
|
+
this.supportsStartTls = false;
|
|
1192
|
+
this.dualPhase = "idle";
|
|
1193
|
+
this.realOutcome = "pending";
|
|
1194
|
+
this.probeOutcome = "pending";
|
|
1195
|
+
this.dualPipelined = false;
|
|
1196
|
+
this.pendingDecision = null;
|
|
1150
1197
|
this.onData = (data) => {
|
|
1151
1198
|
if (this.resolved)
|
|
1152
1199
|
return;
|
|
@@ -1159,7 +1206,7 @@ class SMTPProbeConnection {
|
|
|
1159
1206
|
this.processLine(line);
|
|
1160
1207
|
}
|
|
1161
1208
|
};
|
|
1162
|
-
const defaultSteps = [exports.SMTPStep.greeting, exports.SMTPStep.ehlo, exports.SMTPStep.mailFrom, exports.SMTPStep.rcptTo];
|
|
1209
|
+
const defaultSteps = [exports.SMTPStep.greeting, exports.SMTPStep.ehlo, exports.SMTPStep.startTls, exports.SMTPStep.mailFrom, exports.SMTPStep.rcptTo];
|
|
1163
1210
|
this.steps = [...(_b = (_a = p.sequence) === null || _a === void 0 ? void 0 : _a.steps) !== null && _b !== void 0 ? _b : defaultSteps];
|
|
1164
1211
|
this.isTLS = PORT_TLS[p.port] === true;
|
|
1165
1212
|
const servername = isIPAddress(p.mxHost) ? void 0 : p.mxHost;
|
|
@@ -1170,6 +1217,7 @@ class SMTPProbeConnection {
|
|
|
1170
1217
|
minVersion: "TLSv1.2",
|
|
1171
1218
|
...typeof p.tlsConfig === "object" ? p.tlsConfig : {}
|
|
1172
1219
|
};
|
|
1220
|
+
this.probeLocal = p.catchAllProbeLocal ? p.catchAllProbeLocal(p.local, p.domain) : defaultProbeLocal();
|
|
1173
1221
|
}
|
|
1174
1222
|
run() {
|
|
1175
1223
|
return new Promise((resolve) => {
|
|
@@ -1178,7 +1226,8 @@ class SMTPProbeConnection {
|
|
|
1178
1226
|
this.connect();
|
|
1179
1227
|
this.armConnectionTimer();
|
|
1180
1228
|
} catch (error) {
|
|
1181
|
-
this.
|
|
1229
|
+
this.p.log(`connect threw: ${error instanceof Error ? error.message : "unknown"}`);
|
|
1230
|
+
this.finish(null, "connection_error");
|
|
1182
1231
|
}
|
|
1183
1232
|
});
|
|
1184
1233
|
}
|
|
@@ -1230,51 +1279,173 @@ class SMTPProbeConnection {
|
|
|
1230
1279
|
switch (step) {
|
|
1231
1280
|
case exports.SMTPStep.greeting:
|
|
1232
1281
|
return;
|
|
1282
|
+
// server-driven; nothing to send
|
|
1233
1283
|
case exports.SMTPStep.ehlo:
|
|
1234
1284
|
this.send(`EHLO ${this.p.hostname}`);
|
|
1235
1285
|
return;
|
|
1236
1286
|
case exports.SMTPStep.helo:
|
|
1237
1287
|
this.send(`HELO ${this.p.hostname}`);
|
|
1238
1288
|
return;
|
|
1289
|
+
case exports.SMTPStep.startTls:
|
|
1290
|
+
this.executeStartTls();
|
|
1291
|
+
return;
|
|
1239
1292
|
case exports.SMTPStep.mailFrom: {
|
|
1240
1293
|
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}>`;
|
|
1241
1294
|
this.send(`MAIL FROM:${from}`);
|
|
1242
1295
|
return;
|
|
1243
1296
|
}
|
|
1244
1297
|
case exports.SMTPStep.rcptTo:
|
|
1245
|
-
this.
|
|
1298
|
+
this.executeEnvelope();
|
|
1246
1299
|
return;
|
|
1247
1300
|
}
|
|
1248
1301
|
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1302
|
+
/**
|
|
1303
|
+
* Conditional STARTTLS upgrade. Skipped (advances to next step) when:
|
|
1304
|
+
* - already TLS (implicit-TLS port like 465 or already-upgraded)
|
|
1305
|
+
* - `startTls === 'never'`
|
|
1306
|
+
* - `startTls === 'auto'` AND the MX didn't advertise STARTTLS in EHLO
|
|
1307
|
+
*
|
|
1308
|
+
* Sends `STARTTLS` and waits for 220 when:
|
|
1309
|
+
* - `startTls === 'force'` (regardless of advertisement)
|
|
1310
|
+
* - `startTls === 'auto'` AND the MX advertised it
|
|
1311
|
+
*
|
|
1312
|
+
* On 220, `tls.connect()` wraps the existing socket. After the handshake
|
|
1313
|
+
* we re-EHLO (mandatory per RFC 3207 §4.2 — pre-TLS state must be
|
|
1314
|
+
* discarded) before continuing to MAIL FROM.
|
|
1315
|
+
*/
|
|
1316
|
+
executeStartTls() {
|
|
1317
|
+
const mode = this.p.startTls;
|
|
1318
|
+
const wantsUpgrade = !this.isTLS && mode !== "never" && (mode === "force" || mode === "auto" && this.supportsStartTls);
|
|
1319
|
+
if (!wantsUpgrade) {
|
|
1320
|
+
this.nextStep();
|
|
1251
1321
|
return;
|
|
1252
|
-
|
|
1253
|
-
this.
|
|
1254
|
-
|
|
1255
|
-
|
|
1322
|
+
}
|
|
1323
|
+
this.send("STARTTLS");
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Wrap the plaintext socket with TLS in place. Called after the server
|
|
1327
|
+
* answers our STARTTLS with 220. Detaches the plaintext-socket listeners
|
|
1328
|
+
* (TLS owns the underlying transport now), re-installs them on the wrapped
|
|
1329
|
+
* socket, resets EHLO-derived capabilities, and re-issues EHLO once the
|
|
1330
|
+
* handshake completes (RFC 3207 §4.2 mandates re-EHLO after upgrade —
|
|
1331
|
+
* pre-TLS state must be discarded).
|
|
1332
|
+
*/
|
|
1333
|
+
upgradeToTls() {
|
|
1334
|
+
const plainSocket = this.socket;
|
|
1335
|
+
if (!plainSocket) {
|
|
1336
|
+
this.finish(null, "tls_upgrade_failed");
|
|
1256
1337
|
return;
|
|
1257
1338
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1339
|
+
const detach = plainSocket.removeAllListeners;
|
|
1340
|
+
if (typeof detach === "function") {
|
|
1341
|
+
detach.call(plainSocket, "data");
|
|
1342
|
+
detach.call(plainSocket, "error");
|
|
1343
|
+
detach.call(plainSocket, "close");
|
|
1344
|
+
detach.call(plainSocket, "timeout");
|
|
1345
|
+
}
|
|
1346
|
+
try {
|
|
1347
|
+
plainSocket.setTimeout(0);
|
|
1348
|
+
} catch {
|
|
1349
|
+
}
|
|
1350
|
+
this.tlsUpgrading = true;
|
|
1351
|
+
const servername = isIPAddress(this.p.mxHost) ? void 0 : this.p.mxHost;
|
|
1352
|
+
const tlsSocket = tls__namespace.connect({ ...this.tlsOptions, socket: plainSocket, servername }, () => {
|
|
1353
|
+
this.tlsUpgrading = false;
|
|
1354
|
+
this.isTLS = true;
|
|
1355
|
+
this.buffer = "";
|
|
1356
|
+
this.supportsStartTls = false;
|
|
1357
|
+
this.supportsPipelining = false;
|
|
1358
|
+
this.postUpgradeReEhlo = true;
|
|
1359
|
+
this.send(`EHLO ${this.p.hostname}`);
|
|
1360
|
+
});
|
|
1361
|
+
this.socket = tlsSocket;
|
|
1362
|
+
this.socket.on("data", this.onData);
|
|
1363
|
+
this.socket.on("error", () => {
|
|
1364
|
+
this.finish(null, this.tlsUpgrading ? "tls_handshake_failed" : "connection_error");
|
|
1365
|
+
});
|
|
1366
|
+
this.socket.on("close", () => this.finish(null, "connection_closed"));
|
|
1367
|
+
this.socket.setTimeout(this.p.timeout, () => this.finish(null, "socket_timeout"));
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Send the dual-probe envelope (real RCPT + probe RCPT + RSET). Pipelined
|
|
1371
|
+
* when the MX advertised PIPELINING (or `pipelining: 'force'`); sequential
|
|
1372
|
+
* otherwise.
|
|
1373
|
+
*/
|
|
1374
|
+
executeEnvelope() {
|
|
1375
|
+
var _a;
|
|
1376
|
+
const wantsPipelining = this.p.pipelining === "force" || this.p.pipelining === "auto" && this.supportsPipelining;
|
|
1377
|
+
const realCmd = `RCPT TO:<${this.p.local}@${this.p.domain}>`;
|
|
1378
|
+
if (wantsPipelining) {
|
|
1379
|
+
this.dualPipelined = true;
|
|
1380
|
+
const probeCmd = `RCPT TO:<${this.probeLocal}@${this.p.domain}>`;
|
|
1381
|
+
const rsetCmd = "RSET";
|
|
1382
|
+
this.commands.push(realCmd, probeCmd, rsetCmd);
|
|
1383
|
+
this.p.log(`\u2192 ${realCmd}`);
|
|
1384
|
+
this.p.log(`\u2192 ${probeCmd}`);
|
|
1385
|
+
this.p.log(`\u2192 ${rsetCmd}`);
|
|
1386
|
+
(_a = this.socket) === null || _a === void 0 ? void 0 : _a.write(`${realCmd}\r
|
|
1387
|
+
${probeCmd}\r
|
|
1388
|
+
${rsetCmd}\r
|
|
1389
|
+
`);
|
|
1390
|
+
this.dualPhase = "rcpt_real";
|
|
1260
1391
|
return;
|
|
1261
1392
|
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1393
|
+
this.dualPipelined = false;
|
|
1394
|
+
this.send(realCmd);
|
|
1395
|
+
this.dualPhase = "rcpt_real";
|
|
1396
|
+
}
|
|
1397
|
+
processLine(line) {
|
|
1398
|
+
if (this.resolved)
|
|
1264
1399
|
return;
|
|
1400
|
+
this.transcript.push(line);
|
|
1401
|
+
this.p.log(`\u2190 ${line}`);
|
|
1402
|
+
const codeStr = line.slice(0, 3);
|
|
1403
|
+
const numericCode = /^\d{3}$/.test(codeStr) ? parseInt(codeStr, 10) : null;
|
|
1404
|
+
if (numericCode !== null)
|
|
1405
|
+
this.lastResponseCode = numericCode;
|
|
1406
|
+
const dsn = parseDsn(line);
|
|
1407
|
+
if (dsn)
|
|
1408
|
+
this.lastEnhancedStatus = dsnToString(dsn);
|
|
1409
|
+
if (this.dualPhase === "idle" || this.dualPhase === "rcpt_real") {
|
|
1410
|
+
if (isHighVolume(line)) {
|
|
1411
|
+
this.finish(true, "high_volume");
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
if (isOverQuota(line)) {
|
|
1415
|
+
this.isCatchAllFlag = false;
|
|
1416
|
+
this.finish(false, "over_quota");
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
if (isInvalidMailboxError(line)) {
|
|
1420
|
+
this.isCatchAllFlag = false;
|
|
1421
|
+
this.finish(false, "not_found");
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1265
1424
|
}
|
|
1266
|
-
if (MULTILINE_RE.test(line))
|
|
1425
|
+
if (MULTILINE_RE.test(line)) {
|
|
1426
|
+
const step = this.steps[this.currentStepIndex];
|
|
1427
|
+
const isEhloLike = step === exports.SMTPStep.ehlo || step === exports.SMTPStep.helo || step === exports.SMTPStep.startTls && this.postUpgradeReEhlo;
|
|
1428
|
+
if (isEhloLike && line.startsWith("250-")) {
|
|
1429
|
+
const upper = line.toUpperCase();
|
|
1430
|
+
if (upper.includes("PIPELINING"))
|
|
1431
|
+
this.supportsPipelining = true;
|
|
1432
|
+
if (upper.includes("STARTTLS"))
|
|
1433
|
+
this.supportsStartTls = true;
|
|
1434
|
+
}
|
|
1267
1435
|
return;
|
|
1268
|
-
|
|
1269
|
-
const numericCode = /^\d{3}$/.test(code) ? parseInt(code, 10) : null;
|
|
1436
|
+
}
|
|
1270
1437
|
if (numericCode === null) {
|
|
1271
1438
|
this.finish(null, "unrecognized_response");
|
|
1272
1439
|
return;
|
|
1273
1440
|
}
|
|
1274
|
-
this.dispatch(numericCode);
|
|
1441
|
+
this.dispatch(numericCode, line);
|
|
1275
1442
|
}
|
|
1276
|
-
dispatch(code) {
|
|
1443
|
+
dispatch(code, line) {
|
|
1277
1444
|
const step = this.steps[this.currentStepIndex];
|
|
1445
|
+
if (this.dualPhase !== "idle" && step === exports.SMTPStep.rcptTo) {
|
|
1446
|
+
this.handleEnvelopeReply(code, line);
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1278
1449
|
switch (step) {
|
|
1279
1450
|
case exports.SMTPStep.greeting:
|
|
1280
1451
|
if (code === 220)
|
|
@@ -1294,6 +1465,21 @@ class SMTPProbeConnection {
|
|
|
1294
1465
|
else
|
|
1295
1466
|
this.finish(null, "helo_failed");
|
|
1296
1467
|
return;
|
|
1468
|
+
case exports.SMTPStep.startTls:
|
|
1469
|
+
if (this.postUpgradeReEhlo) {
|
|
1470
|
+
this.postUpgradeReEhlo = false;
|
|
1471
|
+
if (code === 250)
|
|
1472
|
+
this.nextStep();
|
|
1473
|
+
else
|
|
1474
|
+
this.finish(null, "ehlo_failed");
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
if (code === 220) {
|
|
1478
|
+
this.upgradeToTls();
|
|
1479
|
+
} else {
|
|
1480
|
+
this.finish(null, "tls_upgrade_failed");
|
|
1481
|
+
}
|
|
1482
|
+
return;
|
|
1297
1483
|
case exports.SMTPStep.mailFrom:
|
|
1298
1484
|
if (code === 250)
|
|
1299
1485
|
this.nextStep();
|
|
@@ -1301,15 +1487,91 @@ class SMTPProbeConnection {
|
|
|
1301
1487
|
this.finish(null, "mail_from_rejected");
|
|
1302
1488
|
return;
|
|
1303
1489
|
case exports.SMTPStep.rcptTo:
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1490
|
+
this.handleEnvelopeReply(code, line);
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Dual-probe / pipelined-envelope reply router. Demuxes server replies for
|
|
1496
|
+
* the three queued commands (real RCPT, probe RCPT, RSET) and resolves
|
|
1497
|
+
* with the catch-all-aware verdict.
|
|
1498
|
+
*/
|
|
1499
|
+
handleEnvelopeReply(code, line) {
|
|
1500
|
+
if (this.dualPhase === "rcpt_real") {
|
|
1501
|
+
this.realOutcome = classifyRcpt(code);
|
|
1502
|
+
if (code === 552 || code === 452 || isOverQuota(line)) {
|
|
1503
|
+
this.isCatchAllFlag = false;
|
|
1504
|
+
if (this.dualPipelined) {
|
|
1505
|
+
this.pendingDecision = { result: false, reason: "over_quota" };
|
|
1506
|
+
this.dualPhase = "rcpt_probe";
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
this.finish(false, "over_quota");
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
if (this.realOutcome === "soft_reject") {
|
|
1513
|
+
if (this.dualPipelined) {
|
|
1514
|
+
this.pendingDecision = { result: null, reason: "temporary_failure" };
|
|
1515
|
+
this.dualPhase = "rcpt_probe";
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
this.finish(null, "temporary_failure");
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
if (this.realOutcome === "hard_reject") {
|
|
1522
|
+
const reason = isInvalidMailboxError(line) ? "not_found" : "ambiguous";
|
|
1523
|
+
const result = reason === "not_found" ? false : null;
|
|
1524
|
+
this.isCatchAllFlag = false;
|
|
1525
|
+
if (this.dualPipelined) {
|
|
1526
|
+
this.pendingDecision = { result, reason };
|
|
1527
|
+
this.dualPhase = "rcpt_probe";
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
this.finish(result, reason);
|
|
1312
1531
|
return;
|
|
1532
|
+
}
|
|
1533
|
+
if (this.dualPipelined) {
|
|
1534
|
+
this.dualPhase = "rcpt_probe";
|
|
1535
|
+
} else {
|
|
1536
|
+
this.send(`RCPT TO:<${this.probeLocal}@${this.p.domain}>`);
|
|
1537
|
+
this.dualPhase = "rcpt_probe";
|
|
1538
|
+
}
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
if (this.dualPhase === "rcpt_probe") {
|
|
1542
|
+
this.probeOutcome = classifyRcpt(code);
|
|
1543
|
+
if (this.dualPipelined) {
|
|
1544
|
+
this.dualPhase = "rset";
|
|
1545
|
+
} else {
|
|
1546
|
+
this.send("RSET");
|
|
1547
|
+
this.dualPhase = "rset";
|
|
1548
|
+
}
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
if (this.dualPhase === "rset") {
|
|
1552
|
+
if (this.pendingDecision) {
|
|
1553
|
+
this.finish(this.pendingDecision.result, this.pendingDecision.reason);
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
this.decideDualProbe();
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
/** Final decision after both RCPT outcomes are known. Catch-all only when both 250. */
|
|
1561
|
+
decideDualProbe() {
|
|
1562
|
+
if (this.realOutcome === "accept" && this.probeOutcome === "accept") {
|
|
1563
|
+
this.isCatchAllFlag = true;
|
|
1564
|
+
this.finish(true, "valid");
|
|
1565
|
+
} else if (this.realOutcome === "accept") {
|
|
1566
|
+
this.isCatchAllFlag = false;
|
|
1567
|
+
this.finish(true, "valid");
|
|
1568
|
+
} else if (this.realOutcome === "hard_reject") {
|
|
1569
|
+
this.isCatchAllFlag = false;
|
|
1570
|
+
this.finish(false, "not_found");
|
|
1571
|
+
} else if (this.realOutcome === "soft_reject") {
|
|
1572
|
+
this.finish(null, "temporary_failure");
|
|
1573
|
+
} else {
|
|
1574
|
+
this.finish(null, "ambiguous");
|
|
1313
1575
|
}
|
|
1314
1576
|
}
|
|
1315
1577
|
finish(result, reason) {
|
|
@@ -1335,9 +1597,24 @@ class SMTPProbeConnection {
|
|
|
1335
1597
|
return (_a2 = this.socket) === null || _a2 === void 0 ? void 0 : _a2.destroy();
|
|
1336
1598
|
}, QUIT_DRAIN_MS);
|
|
1337
1599
|
(_c = drain.unref) === null || _c === void 0 ? void 0 : _c.call(drain);
|
|
1338
|
-
this.resolveFn({
|
|
1600
|
+
this.resolveFn({
|
|
1601
|
+
result,
|
|
1602
|
+
reason,
|
|
1603
|
+
...this.lastEnhancedStatus !== void 0 ? { enhancedStatus: this.lastEnhancedStatus } : {},
|
|
1604
|
+
...this.lastResponseCode !== void 0 ? { responseCode: this.lastResponseCode } : {},
|
|
1605
|
+
...this.isCatchAllFlag !== void 0 ? { isCatchAll: this.isCatchAllFlag } : {},
|
|
1606
|
+
transcript: this.transcript,
|
|
1607
|
+
commands: this.commands
|
|
1608
|
+
});
|
|
1339
1609
|
}
|
|
1340
1610
|
}
|
|
1611
|
+
function classifyRcpt(code) {
|
|
1612
|
+
if (code === 250 || code === 251)
|
|
1613
|
+
return "accept";
|
|
1614
|
+
if (code >= 400 && code < 500)
|
|
1615
|
+
return "soft_reject";
|
|
1616
|
+
return "hard_reject";
|
|
1617
|
+
}
|
|
1341
1618
|
|
|
1342
1619
|
class ArrayTranscriptCollector {
|
|
1343
1620
|
constructor() {
|
|
@@ -2010,12 +2287,11 @@ async function verifyEmail(params) {
|
|
|
2010
2287
|
const skipMx = ((_f = params.skipMxForDisposable) !== null && _f !== void 0 ? _f : false) && result.isDisposable;
|
|
2011
2288
|
const skipWhois = ((_g = params.skipDomainWhoisForDisposable) !== null && _g !== void 0 ? _g : false) && result.isDisposable;
|
|
2012
2289
|
await runWhoisChecks(domain, params, result, skipWhois, log, collector);
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
}
|
|
2290
|
+
const wantsMxOrSmtp = ((_h = params.verifyMx) !== null && _h !== void 0 ? _h : true) || ((_j = params.verifySmtp) !== null && _j !== void 0 ? _j : false);
|
|
2291
|
+
if (wantsMxOrSmtp && skipMx) {
|
|
2292
|
+
log(`[verifyEmail] skipping MX/SMTP for disposable: ${params.emailAddress}`);
|
|
2293
|
+
} else if (wantsMxOrSmtp) {
|
|
2294
|
+
await runMxAndSmtp(local, domain, params, result, log, collector);
|
|
2019
2295
|
}
|
|
2020
2296
|
result.metadata.verificationTime = Date.now() - startTime;
|
|
2021
2297
|
if (captureTranscript)
|
|
@@ -2365,6 +2641,36 @@ function isSpamName(name) {
|
|
|
2365
2641
|
return true;
|
|
2366
2642
|
}
|
|
2367
2643
|
|
|
2644
|
+
const REFINEMENT_TABLE = {
|
|
2645
|
+
// X.1.x — addressing
|
|
2646
|
+
"5.1.1": "mailbox_does_not_exist",
|
|
2647
|
+
"5.1.2": "bad_destination_system",
|
|
2648
|
+
"5.1.3": "bad_destination_address",
|
|
2649
|
+
"5.1.6": "mailbox_moved",
|
|
2650
|
+
"5.1.10": "recipient_address_has_null_mx",
|
|
2651
|
+
// X.2.x — mailbox status
|
|
2652
|
+
"5.2.0": "mailbox_status_other",
|
|
2653
|
+
"5.2.1": "mailbox_disabled",
|
|
2654
|
+
"5.2.2": "mailbox_full",
|
|
2655
|
+
"5.2.3": "message_too_long",
|
|
2656
|
+
"5.2.4": "mailing_list_expansion_problem",
|
|
2657
|
+
// X.4.x — network / routing
|
|
2658
|
+
"4.4.1": "no_answer_from_host",
|
|
2659
|
+
"4.4.2": "bad_connection",
|
|
2660
|
+
// X.7.x — security / policy
|
|
2661
|
+
"5.7.0": "security_other",
|
|
2662
|
+
"5.7.1": "delivery_not_authorized",
|
|
2663
|
+
"5.7.25": "no_reverse_dns",
|
|
2664
|
+
"5.7.26": "multiple_authentication_failures"
|
|
2665
|
+
};
|
|
2666
|
+
function refineReasonByEnhancedStatus(reason, enhancedStatus) {
|
|
2667
|
+
var _a;
|
|
2668
|
+
const base = reason !== null && reason !== void 0 ? reason : "unknown";
|
|
2669
|
+
if (!enhancedStatus)
|
|
2670
|
+
return base;
|
|
2671
|
+
return (_a = REFINEMENT_TABLE[enhancedStatus]) !== null && _a !== void 0 ? _a : base;
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2368
2674
|
const NETWORK_ERROR_PATTERNS = [
|
|
2369
2675
|
"etimedout",
|
|
2370
2676
|
"econnrefused",
|
|
@@ -2495,6 +2801,7 @@ exports.isSpamName = isSpamName;
|
|
|
2495
2801
|
exports.isValidEmail = isValidEmail;
|
|
2496
2802
|
exports.isValidEmailDomain = isValidEmailDomain;
|
|
2497
2803
|
exports.parseSmtpError = parseSmtpError;
|
|
2804
|
+
exports.refineReasonByEnhancedStatus = refineReasonByEnhancedStatus;
|
|
2498
2805
|
exports.suggestDomain = suggestDomain;
|
|
2499
2806
|
exports.suggestEmailDomain = suggestEmailDomain;
|
|
2500
2807
|
exports.verifyEmail = verifyEmail;
|