@lacspace/email-verify 1.0.0
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/LICENSE +21 -0
- package/dist/index.cjs +117 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +55 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +109 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lacspace
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var net = require('net');
|
|
4
|
+
var promises = require('dns/promises');
|
|
5
|
+
var os = require('os');
|
|
6
|
+
var emailValidate = require('@lacspace/email-validate');
|
|
7
|
+
|
|
8
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
+
|
|
10
|
+
var net__default = /*#__PURE__*/_interopDefault(net);
|
|
11
|
+
|
|
12
|
+
// src/index.ts
|
|
13
|
+
async function resolveMx(domain) {
|
|
14
|
+
try {
|
|
15
|
+
const records = await promises.resolveMx(domain);
|
|
16
|
+
return records.sort((a, b) => a.priority - b.priority);
|
|
17
|
+
} catch {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function smtpCheck(email, mxHost, opts = {}) {
|
|
22
|
+
const timeout = opts.timeout ?? 1e4;
|
|
23
|
+
const from = opts.fromAddress ?? `verify@${os.hostname() || "localhost"}`;
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const socket = net__default.default.connect({ host: mxHost, port: 25 });
|
|
26
|
+
socket.setEncoding("utf8");
|
|
27
|
+
socket.setTimeout(timeout);
|
|
28
|
+
let buffer = "";
|
|
29
|
+
let step = 0;
|
|
30
|
+
let verdict = "unknown";
|
|
31
|
+
const steps = [
|
|
32
|
+
`EHLO ${os.hostname() || "localhost"}`,
|
|
33
|
+
`MAIL FROM:<${from}>`,
|
|
34
|
+
`RCPT TO:<${email}>`,
|
|
35
|
+
"QUIT"
|
|
36
|
+
];
|
|
37
|
+
const finish = (v) => {
|
|
38
|
+
verdict = v;
|
|
39
|
+
try {
|
|
40
|
+
socket.write("QUIT\r\n");
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
socket.destroy();
|
|
44
|
+
resolve(verdict);
|
|
45
|
+
};
|
|
46
|
+
const send = () => {
|
|
47
|
+
if (step < steps.length) socket.write(steps[step] + "\r\n");
|
|
48
|
+
};
|
|
49
|
+
socket.on("connect", () => {
|
|
50
|
+
});
|
|
51
|
+
socket.on("data", (chunk) => {
|
|
52
|
+
buffer += chunk;
|
|
53
|
+
const lines = buffer.split("\r\n");
|
|
54
|
+
buffer = lines.pop() ?? "";
|
|
55
|
+
for (const line of lines) {
|
|
56
|
+
if (!/^\d{3} /.test(line)) continue;
|
|
57
|
+
const code = parseInt(line.slice(0, 3), 10);
|
|
58
|
+
if (step === 0) {
|
|
59
|
+
if (code !== 220) return finish("unknown");
|
|
60
|
+
send();
|
|
61
|
+
step = 1;
|
|
62
|
+
} else if (step === 1) {
|
|
63
|
+
send();
|
|
64
|
+
step = 2;
|
|
65
|
+
} else if (step === 2) {
|
|
66
|
+
if (code >= 400) return finish("unknown");
|
|
67
|
+
send();
|
|
68
|
+
step = 3;
|
|
69
|
+
} else if (step === 3) {
|
|
70
|
+
if (code === 250 || code === 251) return finish("deliverable");
|
|
71
|
+
if (code === 550 || code === 551 || code === 553) return finish("undeliverable");
|
|
72
|
+
return finish("unknown");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
socket.on("timeout", () => finish("unknown"));
|
|
77
|
+
socket.on("error", () => finish("unknown"));
|
|
78
|
+
socket.on("close", () => resolve(verdict));
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async function verifyEmail(email, opts = {}) {
|
|
82
|
+
const v = emailValidate.validateEmail(email, { extraDisposable: opts.extraDisposable, suggestions: false });
|
|
83
|
+
const base = {
|
|
84
|
+
email,
|
|
85
|
+
valid: false,
|
|
86
|
+
syntax: v.valid,
|
|
87
|
+
disposable: v.disposable,
|
|
88
|
+
role: v.role,
|
|
89
|
+
mxFound: false,
|
|
90
|
+
mxRecords: [],
|
|
91
|
+
smtp: "unknown"
|
|
92
|
+
};
|
|
93
|
+
if (!v.valid || !v.domain) return { ...base, reason: "invalid syntax" };
|
|
94
|
+
const mx = await resolveMx(v.domain);
|
|
95
|
+
base.mxRecords = mx;
|
|
96
|
+
base.mxFound = mx.length > 0;
|
|
97
|
+
if (!base.mxFound) return { ...base, reason: "no MX records for domain" };
|
|
98
|
+
if (opts.checkSmtp === false) {
|
|
99
|
+
return { ...base, valid: !v.disposable };
|
|
100
|
+
}
|
|
101
|
+
const verdict = await smtpCheck(email, mx[0].exchange, {
|
|
102
|
+
fromAddress: opts.fromAddress,
|
|
103
|
+
timeout: opts.timeout
|
|
104
|
+
});
|
|
105
|
+
base.smtp = verdict;
|
|
106
|
+
return {
|
|
107
|
+
...base,
|
|
108
|
+
valid: !v.disposable && verdict !== "undeliverable",
|
|
109
|
+
reason: verdict === "undeliverable" ? "mailbox rejected by server" : void 0
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
exports.resolveMx = resolveMx;
|
|
114
|
+
exports.smtpCheck = smtpCheck;
|
|
115
|
+
exports.verifyEmail = verifyEmail;
|
|
116
|
+
//# sourceMappingURL=index.cjs.map
|
|
117
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["dnsResolveMx","hostname","net","validateEmail"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @lacspace/email-verify\n * Does this mailbox actually exist? — best-effort deliverability checks.\n *\n * Combines syntax + disposable checks (@lacspace/email-validate) with a real MX\n * record lookup and an optional SMTP RCPT probe, over Node's built-in `dns` and\n * `net`. No message is ever sent.\n *\n * ⚠️ Live SMTP verification is inherently unreliable: many servers greylist,\n * accept-all (catch-all), or block probes — expect \"unknown\" often. Treat a\n * positive as \"likely deliverable\", never as a guarantee.\n *\n * Node.js only · zero npm dependencies · fully typed.\n */\n\nimport net from \"node:net\";\nimport { resolveMx as dnsResolveMx } from \"node:dns/promises\";\nimport { hostname } from \"node:os\";\nimport { validateEmail } from \"@lacspace/email-validate\";\n\nexport interface MxRecord {\n exchange: string;\n priority: number;\n}\n\nexport type SmtpVerdict = \"deliverable\" | \"undeliverable\" | \"unknown\";\n\nexport interface VerifyOptions {\n /** Also run the live SMTP RCPT probe (default true). */\n checkSmtp?: boolean;\n /** MAIL FROM address used in the probe. Default `verify@<your hostname>`. */\n fromAddress?: string;\n /** Per-connection timeout in ms (default 10000). */\n timeout?: number;\n /** Extra disposable domains. */\n extraDisposable?: string[];\n}\n\nexport interface VerifyResult {\n email: string;\n /** Overall best-effort verdict: syntax ok, MX present, and SMTP not undeliverable. */\n valid: boolean;\n syntax: boolean;\n disposable: boolean;\n role: boolean;\n mxFound: boolean;\n mxRecords: MxRecord[];\n smtp: SmtpVerdict;\n reason?: string;\n}\n\n/** Look up and sort a domain's MX records (best priority first). */\nexport async function resolveMx(domain: string): Promise<MxRecord[]> {\n try {\n const records = await dnsResolveMx(domain);\n return records.sort((a, b) => a.priority - b.priority);\n } catch {\n return [];\n }\n}\n\n/**\n * Probe a mailbox with SMTP MAIL FROM / RCPT TO (no DATA, nothing sent).\n * Returns \"deliverable\" (250), \"undeliverable\" (550-class), or \"unknown\".\n */\nexport function smtpCheck(\n email: string,\n mxHost: string,\n opts: { fromAddress?: string; timeout?: number } = {},\n): Promise<SmtpVerdict> {\n const timeout = opts.timeout ?? 10000;\n const from = opts.fromAddress ?? `verify@${hostname() || \"localhost\"}`;\n\n return new Promise((resolve) => {\n const socket = net.connect({ host: mxHost, port: 25 });\n socket.setEncoding(\"utf8\");\n socket.setTimeout(timeout);\n\n let buffer = \"\";\n let step = 0;\n let verdict: SmtpVerdict = \"unknown\";\n const steps = [\n `EHLO ${hostname() || \"localhost\"}`,\n `MAIL FROM:<${from}>`,\n `RCPT TO:<${email}>`,\n \"QUIT\",\n ];\n\n const finish = (v: SmtpVerdict) => {\n verdict = v;\n try {\n socket.write(\"QUIT\\r\\n\");\n } catch {\n /* ignore */\n }\n socket.destroy();\n resolve(verdict);\n };\n\n const send = () => {\n if (step < steps.length) socket.write(steps[step]! + \"\\r\\n\");\n };\n\n socket.on(\"connect\", () => {\n /* wait for 220 greeting before first command */\n });\n\n socket.on(\"data\", (chunk: string) => {\n buffer += chunk;\n const lines = buffer.split(\"\\r\\n\");\n buffer = lines.pop() ?? \"\";\n for (const line of lines) {\n if (!/^\\d{3} /.test(line)) continue; // wait for final line of a reply\n const code = parseInt(line.slice(0, 3), 10);\n\n if (step === 0) {\n if (code !== 220) return finish(\"unknown\");\n send(); // EHLO\n step = 1;\n } else if (step === 1) {\n send(); // MAIL FROM\n step = 2;\n } else if (step === 2) {\n if (code >= 400) return finish(\"unknown\");\n send(); // RCPT TO\n step = 3;\n } else if (step === 3) {\n if (code === 250 || code === 251) return finish(\"deliverable\");\n if (code === 550 || code === 551 || code === 553) return finish(\"undeliverable\");\n return finish(\"unknown\");\n }\n }\n });\n\n socket.on(\"timeout\", () => finish(\"unknown\"));\n socket.on(\"error\", () => finish(\"unknown\"));\n socket.on(\"close\", () => resolve(verdict));\n });\n}\n\n/** Full verification: syntax → disposable → MX → (optional) SMTP probe. */\nexport async function verifyEmail(email: string, opts: VerifyOptions = {}): Promise<VerifyResult> {\n const v = validateEmail(email, { extraDisposable: opts.extraDisposable, suggestions: false });\n const base: VerifyResult = {\n email,\n valid: false,\n syntax: v.valid,\n disposable: v.disposable,\n role: v.role,\n mxFound: false,\n mxRecords: [],\n smtp: \"unknown\",\n };\n\n if (!v.valid || !v.domain) return { ...base, reason: \"invalid syntax\" };\n\n const mx = await resolveMx(v.domain);\n base.mxRecords = mx;\n base.mxFound = mx.length > 0;\n if (!base.mxFound) return { ...base, reason: \"no MX records for domain\" };\n\n if (opts.checkSmtp === false) {\n return { ...base, valid: !v.disposable };\n }\n\n const verdict = await smtpCheck(email, mx[0]!.exchange, {\n fromAddress: opts.fromAddress,\n timeout: opts.timeout,\n });\n base.smtp = verdict;\n\n return {\n ...base,\n valid: !v.disposable && verdict !== \"undeliverable\",\n reason: verdict === \"undeliverable\" ? \"mailbox rejected by server\" : undefined,\n };\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lacspace/email-verify
|
|
3
|
+
* Does this mailbox actually exist? — best-effort deliverability checks.
|
|
4
|
+
*
|
|
5
|
+
* Combines syntax + disposable checks (@lacspace/email-validate) with a real MX
|
|
6
|
+
* record lookup and an optional SMTP RCPT probe, over Node's built-in `dns` and
|
|
7
|
+
* `net`. No message is ever sent.
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ Live SMTP verification is inherently unreliable: many servers greylist,
|
|
10
|
+
* accept-all (catch-all), or block probes — expect "unknown" often. Treat a
|
|
11
|
+
* positive as "likely deliverable", never as a guarantee.
|
|
12
|
+
*
|
|
13
|
+
* Node.js only · zero npm dependencies · fully typed.
|
|
14
|
+
*/
|
|
15
|
+
interface MxRecord {
|
|
16
|
+
exchange: string;
|
|
17
|
+
priority: number;
|
|
18
|
+
}
|
|
19
|
+
type SmtpVerdict = "deliverable" | "undeliverable" | "unknown";
|
|
20
|
+
interface VerifyOptions {
|
|
21
|
+
/** Also run the live SMTP RCPT probe (default true). */
|
|
22
|
+
checkSmtp?: boolean;
|
|
23
|
+
/** MAIL FROM address used in the probe. Default `verify@<your hostname>`. */
|
|
24
|
+
fromAddress?: string;
|
|
25
|
+
/** Per-connection timeout in ms (default 10000). */
|
|
26
|
+
timeout?: number;
|
|
27
|
+
/** Extra disposable domains. */
|
|
28
|
+
extraDisposable?: string[];
|
|
29
|
+
}
|
|
30
|
+
interface VerifyResult {
|
|
31
|
+
email: string;
|
|
32
|
+
/** Overall best-effort verdict: syntax ok, MX present, and SMTP not undeliverable. */
|
|
33
|
+
valid: boolean;
|
|
34
|
+
syntax: boolean;
|
|
35
|
+
disposable: boolean;
|
|
36
|
+
role: boolean;
|
|
37
|
+
mxFound: boolean;
|
|
38
|
+
mxRecords: MxRecord[];
|
|
39
|
+
smtp: SmtpVerdict;
|
|
40
|
+
reason?: string;
|
|
41
|
+
}
|
|
42
|
+
/** Look up and sort a domain's MX records (best priority first). */
|
|
43
|
+
declare function resolveMx(domain: string): Promise<MxRecord[]>;
|
|
44
|
+
/**
|
|
45
|
+
* Probe a mailbox with SMTP MAIL FROM / RCPT TO (no DATA, nothing sent).
|
|
46
|
+
* Returns "deliverable" (250), "undeliverable" (550-class), or "unknown".
|
|
47
|
+
*/
|
|
48
|
+
declare function smtpCheck(email: string, mxHost: string, opts?: {
|
|
49
|
+
fromAddress?: string;
|
|
50
|
+
timeout?: number;
|
|
51
|
+
}): Promise<SmtpVerdict>;
|
|
52
|
+
/** Full verification: syntax → disposable → MX → (optional) SMTP probe. */
|
|
53
|
+
declare function verifyEmail(email: string, opts?: VerifyOptions): Promise<VerifyResult>;
|
|
54
|
+
|
|
55
|
+
export { type MxRecord, type SmtpVerdict, type VerifyOptions, type VerifyResult, resolveMx, smtpCheck, verifyEmail };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @lacspace/email-verify
|
|
3
|
+
* Does this mailbox actually exist? — best-effort deliverability checks.
|
|
4
|
+
*
|
|
5
|
+
* Combines syntax + disposable checks (@lacspace/email-validate) with a real MX
|
|
6
|
+
* record lookup and an optional SMTP RCPT probe, over Node's built-in `dns` and
|
|
7
|
+
* `net`. No message is ever sent.
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ Live SMTP verification is inherently unreliable: many servers greylist,
|
|
10
|
+
* accept-all (catch-all), or block probes — expect "unknown" often. Treat a
|
|
11
|
+
* positive as "likely deliverable", never as a guarantee.
|
|
12
|
+
*
|
|
13
|
+
* Node.js only · zero npm dependencies · fully typed.
|
|
14
|
+
*/
|
|
15
|
+
interface MxRecord {
|
|
16
|
+
exchange: string;
|
|
17
|
+
priority: number;
|
|
18
|
+
}
|
|
19
|
+
type SmtpVerdict = "deliverable" | "undeliverable" | "unknown";
|
|
20
|
+
interface VerifyOptions {
|
|
21
|
+
/** Also run the live SMTP RCPT probe (default true). */
|
|
22
|
+
checkSmtp?: boolean;
|
|
23
|
+
/** MAIL FROM address used in the probe. Default `verify@<your hostname>`. */
|
|
24
|
+
fromAddress?: string;
|
|
25
|
+
/** Per-connection timeout in ms (default 10000). */
|
|
26
|
+
timeout?: number;
|
|
27
|
+
/** Extra disposable domains. */
|
|
28
|
+
extraDisposable?: string[];
|
|
29
|
+
}
|
|
30
|
+
interface VerifyResult {
|
|
31
|
+
email: string;
|
|
32
|
+
/** Overall best-effort verdict: syntax ok, MX present, and SMTP not undeliverable. */
|
|
33
|
+
valid: boolean;
|
|
34
|
+
syntax: boolean;
|
|
35
|
+
disposable: boolean;
|
|
36
|
+
role: boolean;
|
|
37
|
+
mxFound: boolean;
|
|
38
|
+
mxRecords: MxRecord[];
|
|
39
|
+
smtp: SmtpVerdict;
|
|
40
|
+
reason?: string;
|
|
41
|
+
}
|
|
42
|
+
/** Look up and sort a domain's MX records (best priority first). */
|
|
43
|
+
declare function resolveMx(domain: string): Promise<MxRecord[]>;
|
|
44
|
+
/**
|
|
45
|
+
* Probe a mailbox with SMTP MAIL FROM / RCPT TO (no DATA, nothing sent).
|
|
46
|
+
* Returns "deliverable" (250), "undeliverable" (550-class), or "unknown".
|
|
47
|
+
*/
|
|
48
|
+
declare function smtpCheck(email: string, mxHost: string, opts?: {
|
|
49
|
+
fromAddress?: string;
|
|
50
|
+
timeout?: number;
|
|
51
|
+
}): Promise<SmtpVerdict>;
|
|
52
|
+
/** Full verification: syntax → disposable → MX → (optional) SMTP probe. */
|
|
53
|
+
declare function verifyEmail(email: string, opts?: VerifyOptions): Promise<VerifyResult>;
|
|
54
|
+
|
|
55
|
+
export { type MxRecord, type SmtpVerdict, type VerifyOptions, type VerifyResult, resolveMx, smtpCheck, verifyEmail };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import net from 'net';
|
|
2
|
+
import { resolveMx as resolveMx$1 } from 'dns/promises';
|
|
3
|
+
import { hostname } from 'os';
|
|
4
|
+
import { validateEmail } from '@lacspace/email-validate';
|
|
5
|
+
|
|
6
|
+
// src/index.ts
|
|
7
|
+
async function resolveMx(domain) {
|
|
8
|
+
try {
|
|
9
|
+
const records = await resolveMx$1(domain);
|
|
10
|
+
return records.sort((a, b) => a.priority - b.priority);
|
|
11
|
+
} catch {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function smtpCheck(email, mxHost, opts = {}) {
|
|
16
|
+
const timeout = opts.timeout ?? 1e4;
|
|
17
|
+
const from = opts.fromAddress ?? `verify@${hostname() || "localhost"}`;
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
const socket = net.connect({ host: mxHost, port: 25 });
|
|
20
|
+
socket.setEncoding("utf8");
|
|
21
|
+
socket.setTimeout(timeout);
|
|
22
|
+
let buffer = "";
|
|
23
|
+
let step = 0;
|
|
24
|
+
let verdict = "unknown";
|
|
25
|
+
const steps = [
|
|
26
|
+
`EHLO ${hostname() || "localhost"}`,
|
|
27
|
+
`MAIL FROM:<${from}>`,
|
|
28
|
+
`RCPT TO:<${email}>`,
|
|
29
|
+
"QUIT"
|
|
30
|
+
];
|
|
31
|
+
const finish = (v) => {
|
|
32
|
+
verdict = v;
|
|
33
|
+
try {
|
|
34
|
+
socket.write("QUIT\r\n");
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
socket.destroy();
|
|
38
|
+
resolve(verdict);
|
|
39
|
+
};
|
|
40
|
+
const send = () => {
|
|
41
|
+
if (step < steps.length) socket.write(steps[step] + "\r\n");
|
|
42
|
+
};
|
|
43
|
+
socket.on("connect", () => {
|
|
44
|
+
});
|
|
45
|
+
socket.on("data", (chunk) => {
|
|
46
|
+
buffer += chunk;
|
|
47
|
+
const lines = buffer.split("\r\n");
|
|
48
|
+
buffer = lines.pop() ?? "";
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
if (!/^\d{3} /.test(line)) continue;
|
|
51
|
+
const code = parseInt(line.slice(0, 3), 10);
|
|
52
|
+
if (step === 0) {
|
|
53
|
+
if (code !== 220) return finish("unknown");
|
|
54
|
+
send();
|
|
55
|
+
step = 1;
|
|
56
|
+
} else if (step === 1) {
|
|
57
|
+
send();
|
|
58
|
+
step = 2;
|
|
59
|
+
} else if (step === 2) {
|
|
60
|
+
if (code >= 400) return finish("unknown");
|
|
61
|
+
send();
|
|
62
|
+
step = 3;
|
|
63
|
+
} else if (step === 3) {
|
|
64
|
+
if (code === 250 || code === 251) return finish("deliverable");
|
|
65
|
+
if (code === 550 || code === 551 || code === 553) return finish("undeliverable");
|
|
66
|
+
return finish("unknown");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
socket.on("timeout", () => finish("unknown"));
|
|
71
|
+
socket.on("error", () => finish("unknown"));
|
|
72
|
+
socket.on("close", () => resolve(verdict));
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async function verifyEmail(email, opts = {}) {
|
|
76
|
+
const v = validateEmail(email, { extraDisposable: opts.extraDisposable, suggestions: false });
|
|
77
|
+
const base = {
|
|
78
|
+
email,
|
|
79
|
+
valid: false,
|
|
80
|
+
syntax: v.valid,
|
|
81
|
+
disposable: v.disposable,
|
|
82
|
+
role: v.role,
|
|
83
|
+
mxFound: false,
|
|
84
|
+
mxRecords: [],
|
|
85
|
+
smtp: "unknown"
|
|
86
|
+
};
|
|
87
|
+
if (!v.valid || !v.domain) return { ...base, reason: "invalid syntax" };
|
|
88
|
+
const mx = await resolveMx(v.domain);
|
|
89
|
+
base.mxRecords = mx;
|
|
90
|
+
base.mxFound = mx.length > 0;
|
|
91
|
+
if (!base.mxFound) return { ...base, reason: "no MX records for domain" };
|
|
92
|
+
if (opts.checkSmtp === false) {
|
|
93
|
+
return { ...base, valid: !v.disposable };
|
|
94
|
+
}
|
|
95
|
+
const verdict = await smtpCheck(email, mx[0].exchange, {
|
|
96
|
+
fromAddress: opts.fromAddress,
|
|
97
|
+
timeout: opts.timeout
|
|
98
|
+
});
|
|
99
|
+
base.smtp = verdict;
|
|
100
|
+
return {
|
|
101
|
+
...base,
|
|
102
|
+
valid: !v.disposable && verdict !== "undeliverable",
|
|
103
|
+
reason: verdict === "undeliverable" ? "mailbox rejected by server" : void 0
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { resolveMx, smtpCheck, verifyEmail };
|
|
108
|
+
//# sourceMappingURL=index.js.map
|
|
109
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["dnsResolveMx"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @lacspace/email-verify\n * Does this mailbox actually exist? — best-effort deliverability checks.\n *\n * Combines syntax + disposable checks (@lacspace/email-validate) with a real MX\n * record lookup and an optional SMTP RCPT probe, over Node's built-in `dns` and\n * `net`. No message is ever sent.\n *\n * ⚠️ Live SMTP verification is inherently unreliable: many servers greylist,\n * accept-all (catch-all), or block probes — expect \"unknown\" often. Treat a\n * positive as \"likely deliverable\", never as a guarantee.\n *\n * Node.js only · zero npm dependencies · fully typed.\n */\n\nimport net from \"node:net\";\nimport { resolveMx as dnsResolveMx } from \"node:dns/promises\";\nimport { hostname } from \"node:os\";\nimport { validateEmail } from \"@lacspace/email-validate\";\n\nexport interface MxRecord {\n exchange: string;\n priority: number;\n}\n\nexport type SmtpVerdict = \"deliverable\" | \"undeliverable\" | \"unknown\";\n\nexport interface VerifyOptions {\n /** Also run the live SMTP RCPT probe (default true). */\n checkSmtp?: boolean;\n /** MAIL FROM address used in the probe. Default `verify@<your hostname>`. */\n fromAddress?: string;\n /** Per-connection timeout in ms (default 10000). */\n timeout?: number;\n /** Extra disposable domains. */\n extraDisposable?: string[];\n}\n\nexport interface VerifyResult {\n email: string;\n /** Overall best-effort verdict: syntax ok, MX present, and SMTP not undeliverable. */\n valid: boolean;\n syntax: boolean;\n disposable: boolean;\n role: boolean;\n mxFound: boolean;\n mxRecords: MxRecord[];\n smtp: SmtpVerdict;\n reason?: string;\n}\n\n/** Look up and sort a domain's MX records (best priority first). */\nexport async function resolveMx(domain: string): Promise<MxRecord[]> {\n try {\n const records = await dnsResolveMx(domain);\n return records.sort((a, b) => a.priority - b.priority);\n } catch {\n return [];\n }\n}\n\n/**\n * Probe a mailbox with SMTP MAIL FROM / RCPT TO (no DATA, nothing sent).\n * Returns \"deliverable\" (250), \"undeliverable\" (550-class), or \"unknown\".\n */\nexport function smtpCheck(\n email: string,\n mxHost: string,\n opts: { fromAddress?: string; timeout?: number } = {},\n): Promise<SmtpVerdict> {\n const timeout = opts.timeout ?? 10000;\n const from = opts.fromAddress ?? `verify@${hostname() || \"localhost\"}`;\n\n return new Promise((resolve) => {\n const socket = net.connect({ host: mxHost, port: 25 });\n socket.setEncoding(\"utf8\");\n socket.setTimeout(timeout);\n\n let buffer = \"\";\n let step = 0;\n let verdict: SmtpVerdict = \"unknown\";\n const steps = [\n `EHLO ${hostname() || \"localhost\"}`,\n `MAIL FROM:<${from}>`,\n `RCPT TO:<${email}>`,\n \"QUIT\",\n ];\n\n const finish = (v: SmtpVerdict) => {\n verdict = v;\n try {\n socket.write(\"QUIT\\r\\n\");\n } catch {\n /* ignore */\n }\n socket.destroy();\n resolve(verdict);\n };\n\n const send = () => {\n if (step < steps.length) socket.write(steps[step]! + \"\\r\\n\");\n };\n\n socket.on(\"connect\", () => {\n /* wait for 220 greeting before first command */\n });\n\n socket.on(\"data\", (chunk: string) => {\n buffer += chunk;\n const lines = buffer.split(\"\\r\\n\");\n buffer = lines.pop() ?? \"\";\n for (const line of lines) {\n if (!/^\\d{3} /.test(line)) continue; // wait for final line of a reply\n const code = parseInt(line.slice(0, 3), 10);\n\n if (step === 0) {\n if (code !== 220) return finish(\"unknown\");\n send(); // EHLO\n step = 1;\n } else if (step === 1) {\n send(); // MAIL FROM\n step = 2;\n } else if (step === 2) {\n if (code >= 400) return finish(\"unknown\");\n send(); // RCPT TO\n step = 3;\n } else if (step === 3) {\n if (code === 250 || code === 251) return finish(\"deliverable\");\n if (code === 550 || code === 551 || code === 553) return finish(\"undeliverable\");\n return finish(\"unknown\");\n }\n }\n });\n\n socket.on(\"timeout\", () => finish(\"unknown\"));\n socket.on(\"error\", () => finish(\"unknown\"));\n socket.on(\"close\", () => resolve(verdict));\n });\n}\n\n/** Full verification: syntax → disposable → MX → (optional) SMTP probe. */\nexport async function verifyEmail(email: string, opts: VerifyOptions = {}): Promise<VerifyResult> {\n const v = validateEmail(email, { extraDisposable: opts.extraDisposable, suggestions: false });\n const base: VerifyResult = {\n email,\n valid: false,\n syntax: v.valid,\n disposable: v.disposable,\n role: v.role,\n mxFound: false,\n mxRecords: [],\n smtp: \"unknown\",\n };\n\n if (!v.valid || !v.domain) return { ...base, reason: \"invalid syntax\" };\n\n const mx = await resolveMx(v.domain);\n base.mxRecords = mx;\n base.mxFound = mx.length > 0;\n if (!base.mxFound) return { ...base, reason: \"no MX records for domain\" };\n\n if (opts.checkSmtp === false) {\n return { ...base, valid: !v.disposable };\n }\n\n const verdict = await smtpCheck(email, mx[0]!.exchange, {\n fromAddress: opts.fromAddress,\n timeout: opts.timeout,\n });\n base.smtp = verdict;\n\n return {\n ...base,\n valid: !v.disposable && verdict !== \"undeliverable\",\n reason: verdict === \"undeliverable\" ? \"mailbox rejected by server\" : undefined,\n };\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lacspace/email-verify",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Best-effort email deliverability checks for Node — syntax + disposable, MX record lookup and an optional SMTP RCPT probe (no mail sent). Zero npm dependencies.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"prepublishOnly": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"email-verification",
|
|
31
|
+
"email-verify",
|
|
32
|
+
"mx-lookup",
|
|
33
|
+
"smtp-check",
|
|
34
|
+
"deliverability",
|
|
35
|
+
"mailbox-exists",
|
|
36
|
+
"disposable-email",
|
|
37
|
+
"typescript"
|
|
38
|
+
],
|
|
39
|
+
"author": "Lacspace <contact@lacspace.com>",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"homepage": "https://lacspace.com/packages",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/lacspace/npm-packages.git",
|
|
45
|
+
"directory": "email-verify"
|
|
46
|
+
},
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/lacspace/npm-packages/issues"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=18"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@lacspace/email-validate": "^1.0.0"
|
|
55
|
+
}
|
|
56
|
+
}
|