@lacspace/email-verify 1.0.3 → 1.0.5

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 CHANGED
@@ -85,4 +85,14 @@ This package is **free** under the **[Lacspace Free Licence](https://lacspace.co
85
85
 
86
86
  Not every Lacspace package is free. We also offer **Commercial** (paid), **Client-specific**, and **Private** (proprietary) packages under separate terms. See the full **[Lacspace Licence Centre](https://lacspace.com/licenses)**.
87
87
 
88
+ ---
89
+
90
+ <div align="center">
91
+
92
+ **Part of the Lacspace ecosystem — 35 zero-dependency, isomorphic TypeScript packages.**
93
+
94
+ [All packages ↗](https://lacspace.com/packages) · [npm org ↗](https://www.npmjs.com/org/lacspace) · [Licence Centre ↗](https://lacspace.com/licenses) · [GitHub ↗](https://github.com/lacspace/npm-packages)
95
+
96
+ </div>
97
+
88
98
  <div align="center"><sub>Built with care by <a href="https://lacspace.com">Lacspace</a> · Lacspace Free Licence · <a href="https://github.com/lacspace/npm-packages">source</a></sub></div>
package/dist/index.cjs CHANGED
@@ -21,6 +21,9 @@ async function resolveMx(domain) {
21
21
  function smtpCheck(email, mxHost, opts = {}) {
22
22
  const timeout = opts.timeout ?? 1e4;
23
23
  const from = opts.fromAddress ?? `verify@${os.hostname() || "localhost"}`;
24
+ if (/[\r\n]/.test(email) || /[\r\n]/.test(mxHost) || /[\r\n]/.test(from)) {
25
+ return Promise.resolve("undeliverable");
26
+ }
24
27
  return new Promise((resolve) => {
25
28
  const socket = net__default.default.connect({ host: mxHost, port: 25 });
26
29
  socket.setEncoding("utf8");
@@ -1 +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"]}
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 // Guard against SMTP command injection: a CR or LF in the email or MX host\n // would let a caller inject additional SMTP commands into the probe.\n if (/[\\r\\n]/.test(email) || /[\\r\\n]/.test(mxHost) || /[\\r\\n]/.test(from)) {\n return Promise.resolve(\"undeliverable\");\n }\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.js CHANGED
@@ -15,6 +15,9 @@ async function resolveMx(domain) {
15
15
  function smtpCheck(email, mxHost, opts = {}) {
16
16
  const timeout = opts.timeout ?? 1e4;
17
17
  const from = opts.fromAddress ?? `verify@${hostname() || "localhost"}`;
18
+ if (/[\r\n]/.test(email) || /[\r\n]/.test(mxHost) || /[\r\n]/.test(from)) {
19
+ return Promise.resolve("undeliverable");
20
+ }
18
21
  return new Promise((resolve) => {
19
22
  const socket = net.connect({ host: mxHost, port: 25 });
20
23
  socket.setEncoding("utf8");
package/dist/index.js.map CHANGED
@@ -1 +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"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":["dnsResolveMx"],"mappings":";;;;;;AAoDA,eAAsB,UAAU,MAAA,EAAqC;AACnE,EAAA,IAAI;AACF,IAAA,MAAM,OAAA,GAAU,MAAMA,WAAA,CAAa,MAAM,CAAA;AACzC,IAAA,OAAO,OAAA,CAAQ,KAAK,CAAC,CAAA,EAAG,MAAM,CAAA,CAAE,QAAA,GAAW,EAAE,QAAQ,CAAA;AAAA,EACvD,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,EAAC;AAAA,EACV;AACF;AAMO,SAAS,SAAA,CACd,KAAA,EACA,MAAA,EACA,IAAA,GAAmD,EAAC,EAC9B;AACtB,EAAA,MAAM,OAAA,GAAU,KAAK,OAAA,IAAW,GAAA;AAChC,EAAA,MAAM,OAAO,IAAA,CAAK,WAAA,IAAe,CAAA,OAAA,EAAU,QAAA,MAAc,WAAW,CAAA,CAAA;AAIpE,EAAA,IAAI,QAAA,CAAS,IAAA,CAAK,KAAK,CAAA,IAAK,QAAA,CAAS,IAAA,CAAK,MAAM,CAAA,IAAK,QAAA,CAAS,IAAA,CAAK,IAAI,CAAA,EAAG;AACxE,IAAA,OAAO,OAAA,CAAQ,QAAQ,eAAe,CAAA;AAAA,EACxC;AAEA,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,KAAY;AAC9B,IAAA,MAAM,MAAA,GAAS,IAAI,OAAA,CAAQ,EAAE,MAAM,MAAA,EAAQ,IAAA,EAAM,IAAI,CAAA;AACrD,IAAA,MAAA,CAAO,YAAY,MAAM,CAAA;AACzB,IAAA,MAAA,CAAO,WAAW,OAAO,CAAA;AAEzB,IAAA,IAAI,MAAA,GAAS,EAAA;AACb,IAAA,IAAI,IAAA,GAAO,CAAA;AACX,IAAA,IAAI,OAAA,GAAuB,SAAA;AAC3B,IAAA,MAAM,KAAA,GAAQ;AAAA,MACZ,CAAA,KAAA,EAAQ,QAAA,EAAS,IAAK,WAAW,CAAA,CAAA;AAAA,MACjC,cAAc,IAAI,CAAA,CAAA,CAAA;AAAA,MAClB,YAAY,KAAK,CAAA,CAAA,CAAA;AAAA,MACjB;AAAA,KACF;AAEA,IAAA,MAAM,MAAA,GAAS,CAAC,CAAA,KAAmB;AACjC,MAAA,OAAA,GAAU,CAAA;AACV,MAAA,IAAI;AACF,QAAA,MAAA,CAAO,MAAM,UAAU,CAAA;AAAA,MACzB,CAAA,CAAA,MAAQ;AAAA,MAER;AACA,MAAA,MAAA,CAAO,OAAA,EAAQ;AACf,MAAA,OAAA,CAAQ,OAAO,CAAA;AAAA,IACjB,CAAA;AAEA,IAAA,MAAM,OAAO,MAAM;AACjB,MAAA,IAAI,IAAA,GAAO,MAAM,MAAA,EAAQ,MAAA,CAAO,MAAM,KAAA,CAAM,IAAI,IAAK,MAAM,CAAA;AAAA,IAC7D,CAAA;AAEA,IAAA,MAAA,CAAO,EAAA,CAAG,WAAW,MAAM;AAAA,IAE3B,CAAC,CAAA;AAED,IAAA,MAAA,CAAO,EAAA,CAAG,MAAA,EAAQ,CAAC,KAAA,KAAkB;AACnC,MAAA,MAAA,IAAU,KAAA;AACV,MAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,KAAA,CAAM,MAAM,CAAA;AACjC,MAAA,MAAA,GAAS,KAAA,CAAM,KAAI,IAAK,EAAA;AACxB,MAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,QAAA,IAAI,CAAC,SAAA,CAAU,IAAA,CAAK,IAAI,CAAA,EAAG;AAC3B,QAAA,MAAM,OAAO,QAAA,CAAS,IAAA,CAAK,MAAM,CAAA,EAAG,CAAC,GAAG,EAAE,CAAA;AAE1C,QAAA,IAAI,SAAS,CAAA,EAAG;AACd,UAAA,IAAI,IAAA,KAAS,GAAA,EAAK,OAAO,MAAA,CAAO,SAAS,CAAA;AACzC,UAAA,IAAA,EAAK;AACL,UAAA,IAAA,GAAO,CAAA;AAAA,QACT,CAAA,MAAA,IAAW,SAAS,CAAA,EAAG;AACrB,UAAA,IAAA,EAAK;AACL,UAAA,IAAA,GAAO,CAAA;AAAA,QACT,CAAA,MAAA,IAAW,SAAS,CAAA,EAAG;AACrB,UAAA,IAAI,IAAA,IAAQ,GAAA,EAAK,OAAO,MAAA,CAAO,SAAS,CAAA;AACxC,UAAA,IAAA,EAAK;AACL,UAAA,IAAA,GAAO,CAAA;AAAA,QACT,CAAA,MAAA,IAAW,SAAS,CAAA,EAAG;AACrB,UAAA,IAAI,SAAS,GAAA,IAAO,IAAA,KAAS,GAAA,EAAK,OAAO,OAAO,aAAa,CAAA;AAC7D,UAAA,IAAI,IAAA,KAAS,OAAO,IAAA,KAAS,GAAA,IAAO,SAAS,GAAA,EAAK,OAAO,OAAO,eAAe,CAAA;AAC/E,UAAA,OAAO,OAAO,SAAS,CAAA;AAAA,QACzB;AAAA,MACF;AAAA,IACF,CAAC,CAAA;AAED,IAAA,MAAA,CAAO,EAAA,CAAG,SAAA,EAAW,MAAM,MAAA,CAAO,SAAS,CAAC,CAAA;AAC5C,IAAA,MAAA,CAAO,EAAA,CAAG,OAAA,EAAS,MAAM,MAAA,CAAO,SAAS,CAAC,CAAA;AAC1C,IAAA,MAAA,CAAO,EAAA,CAAG,OAAA,EAAS,MAAM,OAAA,CAAQ,OAAO,CAAC,CAAA;AAAA,EAC3C,CAAC,CAAA;AACH;AAGA,eAAsB,WAAA,CAAY,KAAA,EAAe,IAAA,GAAsB,EAAC,EAA0B;AAChG,EAAA,MAAM,CAAA,GAAI,cAAc,KAAA,EAAO,EAAE,iBAAiB,IAAA,CAAK,eAAA,EAAiB,WAAA,EAAa,KAAA,EAAO,CAAA;AAC5F,EAAA,MAAM,IAAA,GAAqB;AAAA,IACzB,KAAA;AAAA,IACA,KAAA,EAAO,KAAA;AAAA,IACP,QAAQ,CAAA,CAAE,KAAA;AAAA,IACV,YAAY,CAAA,CAAE,UAAA;AAAA,IACd,MAAM,CAAA,CAAE,IAAA;AAAA,IACR,OAAA,EAAS,KAAA;AAAA,IACT,WAAW,EAAC;AAAA,IACZ,IAAA,EAAM;AAAA,GACR;AAEA,EAAA,IAAI,CAAC,CAAA,CAAE,KAAA,IAAS,CAAC,CAAA,CAAE,MAAA,EAAQ,OAAO,EAAE,GAAG,IAAA,EAAM,MAAA,EAAQ,gBAAA,EAAiB;AAEtE,EAAA,MAAM,EAAA,GAAK,MAAM,SAAA,CAAU,CAAA,CAAE,MAAM,CAAA;AACnC,EAAA,IAAA,CAAK,SAAA,GAAY,EAAA;AACjB,EAAA,IAAA,CAAK,OAAA,GAAU,GAAG,MAAA,GAAS,CAAA;AAC3B,EAAA,IAAI,CAAC,KAAK,OAAA,EAAS,OAAO,EAAE,GAAG,IAAA,EAAM,QAAQ,0BAAA,EAA2B;AAExE,EAAA,IAAI,IAAA,CAAK,cAAc,KAAA,EAAO;AAC5B,IAAA,OAAO,EAAE,GAAG,IAAA,EAAM,KAAA,EAAO,CAAC,EAAE,UAAA,EAAW;AAAA,EACzC;AAEA,EAAA,MAAM,UAAU,MAAM,SAAA,CAAU,OAAO,EAAA,CAAG,CAAC,EAAG,QAAA,EAAU;AAAA,IACtD,aAAa,IAAA,CAAK,WAAA;AAAA,IAClB,SAAS,IAAA,CAAK;AAAA,GACf,CAAA;AACD,EAAA,IAAA,CAAK,IAAA,GAAO,OAAA;AAEZ,EAAA,OAAO;AAAA,IACL,GAAG,IAAA;AAAA,IACH,KAAA,EAAO,CAAC,CAAA,CAAE,UAAA,IAAc,OAAA,KAAY,eAAA;AAAA,IACpC,MAAA,EAAQ,OAAA,KAAY,eAAA,GAAkB,4BAAA,GAA+B;AAAA,GACvE;AACF","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 // Guard against SMTP command injection: a CR or LF in the email or MX host\n // would let a caller inject additional SMTP commands into the probe.\n if (/[\\r\\n]/.test(email) || /[\\r\\n]/.test(mxHost) || /[\\r\\n]/.test(from)) {\n return Promise.resolve(\"undeliverable\");\n }\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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lacspace/email-verify",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
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
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -34,7 +34,12 @@
34
34
  "deliverability",
35
35
  "mailbox-exists",
36
36
  "disposable-email",
37
- "typescript"
37
+ "typescript",
38
+ "verify-email",
39
+ "email-exists",
40
+ "smtp-verify",
41
+ "catch-all",
42
+ "mailbox-verification"
38
43
  ],
39
44
  "author": "Lacspace <contact@lacspace.com>",
40
45
  "license": "SEE LICENSE IN LICENSE",
@@ -52,5 +57,8 @@
52
57
  },
53
58
  "dependencies": {
54
59
  "@lacspace/email-validate": "^1.0.1"
60
+ },
61
+ "publishConfig": {
62
+ "access": "public"
55
63
  }
56
64
  }