mxprobe-core 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/address.d.ts +17 -0
- package/dist/address.d.ts.map +1 -0
- package/dist/address.js +14 -0
- package/dist/address.js.map +1 -0
- package/dist/api.d.ts +39 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +2 -0
- package/dist/api.js.map +1 -0
- package/dist/dns.d.ts +23 -0
- package/dist/dns.d.ts.map +1 -0
- package/dist/dns.js +97 -0
- package/dist/dns.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/options.d.ts +29 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +24 -0
- package/dist/options.js.map +1 -0
- package/dist/smtp.d.ts +23 -0
- package/dist/smtp.d.ts.map +1 -0
- package/dist/smtp.js +143 -0
- package/dist/smtp.js.map +1 -0
- package/dist/types.d.ts +36 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +6 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +21 -0
- package/dist/util.js.map +1 -0
- package/dist/verifier.d.ts +26 -0
- package/dist/verifier.d.ts.map +1 -0
- package/dist/verifier.js +113 -0
- package/dist/verifier.js.map +1 -0
- package/package.json +17 -5
- package/src/address.ts +29 -0
- package/src/api.ts +45 -0
- package/src/dns.ts +118 -0
- package/src/index.ts +30 -0
- package/src/options.ts +51 -0
- package/src/smtp.ts +172 -0
- package/src/types.ts +48 -0
- package/src/util.ts +21 -0
- package/src/verifier.ts +138 -0
- package/src/index.mjs +0 -408
package/README.md
CHANGED
|
@@ -31,4 +31,10 @@ Options (all optional): `smtp`, `helo`, `from`, `port`, `hostOverride`,
|
|
|
31
31
|
`autoDisableSmtp`, `resolver` (an object with `resolveMx`, `resolve4`,
|
|
32
32
|
`resolve6`, `reverse`, for tests).
|
|
33
33
|
|
|
34
|
+
Written in TypeScript; the package ships its declarations. `VerifyResult`,
|
|
35
|
+
`Action`, `Verdict`, `SmtpCheck`, `Checks`, `Summary`, `VerifierOptions`,
|
|
36
|
+
`Resolver` and the hosted API's response types (`SignupResponse`,
|
|
37
|
+
`VerifyResponse`, `BalanceResponse`, `CheckoutResponse`, `ApiErrorBody`)
|
|
38
|
+
are exported as types.
|
|
39
|
+
|
|
34
40
|
MIT. Part of https://github.com/andrewchmr/mxprobe.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ParsedAddress {
|
|
2
|
+
/** The address with the domain lowercased and the trailing dot removed. */
|
|
3
|
+
email: string;
|
|
4
|
+
local: string;
|
|
5
|
+
domain: string;
|
|
6
|
+
error?: undefined;
|
|
7
|
+
}
|
|
8
|
+
export interface AddressError {
|
|
9
|
+
email: string;
|
|
10
|
+
error: string;
|
|
11
|
+
local?: undefined;
|
|
12
|
+
domain?: undefined;
|
|
13
|
+
}
|
|
14
|
+
export type ParseResult = ParsedAddress | AddressError;
|
|
15
|
+
/** Split an address into local part and domain, or return { email, error }. */
|
|
16
|
+
export declare function parseAddress(raw: unknown): ParseResult;
|
|
17
|
+
//# sourceMappingURL=address.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"address.d.ts","sourceRoot":"","sources":["../src/address.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B,2EAA2E;IAC3E,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB;AAED,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,YAAY,CAAC;AAEvD,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,WAAW,CAUtD"}
|
package/dist/address.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Split an address into local part and domain, or return { email, error }. */
|
|
2
|
+
export function parseAddress(raw) {
|
|
3
|
+
const email = String(raw ?? "").trim();
|
|
4
|
+
const m = email.match(/^([^\s@]+)@([^\s@]+\.[^\s@]+)$/);
|
|
5
|
+
if (!m || m[1] === undefined || m[2] === undefined)
|
|
6
|
+
return { email, error: "not an email address" };
|
|
7
|
+
const local = m[1];
|
|
8
|
+
const domain = m[2].toLowerCase().replace(/\.$/, "");
|
|
9
|
+
if (local.length > 64 || domain.length > 253 || /\.\./.test(domain) || /[^a-z0-9.-]/.test(domain)) {
|
|
10
|
+
return { email, error: "malformed address" };
|
|
11
|
+
}
|
|
12
|
+
return { email: `${local}@${domain}`, local, domain };
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=address.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"address.js","sourceRoot":"","sources":["../src/address.ts"],"names":[],"mappings":"AAiBA,+EAA+E;AAC/E,MAAM,UAAU,YAAY,CAAC,GAAY;IACvC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACvC,MAAM,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC;IACxD,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,SAAS,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,SAAS;QAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,sBAAsB,EAAE,CAAC;IACpG,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACnB,MAAM,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACrD,IAAI,KAAK,CAAC,MAAM,GAAG,EAAE,IAAI,MAAM,CAAC,MAAM,GAAG,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAClG,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAC/C,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,GAAG,KAAK,IAAI,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AACxD,CAAC"}
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Summary, VerifyResult } from "./types.ts";
|
|
2
|
+
/** Every error response: { error, message } plus route-specific extras. */
|
|
3
|
+
export interface ApiErrorBody {
|
|
4
|
+
error: string;
|
|
5
|
+
message: string;
|
|
6
|
+
retry_after_seconds?: number;
|
|
7
|
+
credits_left?: number;
|
|
8
|
+
needed?: number;
|
|
9
|
+
checkout_hint?: string;
|
|
10
|
+
max?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface SignupResponse {
|
|
13
|
+
api_key: string;
|
|
14
|
+
email: string;
|
|
15
|
+
credits: number;
|
|
16
|
+
mailed: boolean;
|
|
17
|
+
message: string;
|
|
18
|
+
}
|
|
19
|
+
export interface VerifyResponse {
|
|
20
|
+
results: VerifyResult[];
|
|
21
|
+
summary: Summary;
|
|
22
|
+
credits_used: number;
|
|
23
|
+
credits_left: number;
|
|
24
|
+
}
|
|
25
|
+
export interface BalanceResponse {
|
|
26
|
+
email: string;
|
|
27
|
+
credits: number;
|
|
28
|
+
checks_total: number;
|
|
29
|
+
created_at: string;
|
|
30
|
+
}
|
|
31
|
+
export interface CheckoutResponse {
|
|
32
|
+
url: string;
|
|
33
|
+
credits: number;
|
|
34
|
+
amount_usd: number;
|
|
35
|
+
packs: number;
|
|
36
|
+
session_id: string;
|
|
37
|
+
message: string;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAExD,2EAA2E;AAC3E,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB"}
|
package/dist/api.js
ADDED
package/dist/api.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":""}
|
package/dist/dns.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type VerifierOptions } from "./options.ts";
|
|
2
|
+
import type { Verdict } from "./types.ts";
|
|
3
|
+
export interface MxHost {
|
|
4
|
+
host: string;
|
|
5
|
+
/** Resolved for the primary; null for the secondary, which is resolved on demand. */
|
|
6
|
+
ip: string | null;
|
|
7
|
+
}
|
|
8
|
+
export interface DomainCheck {
|
|
9
|
+
verdict: Verdict;
|
|
10
|
+
reason: string;
|
|
11
|
+
/** The primary MX host name, or null when the domain is DEAD. */
|
|
12
|
+
mx: string | null;
|
|
13
|
+
/** The hosts to try in order. Empty when DEAD. */
|
|
14
|
+
mxHosts: MxHost[];
|
|
15
|
+
}
|
|
16
|
+
/** Name the web host behind an A record from its IP and reverse name. */
|
|
17
|
+
export declare function labelWebHost(ip: string, ptr: string | null): string;
|
|
18
|
+
/**
|
|
19
|
+
* The DNS tier for one domain. Returns the verdict, the primary MX host name
|
|
20
|
+
* (null when the domain is DEAD) and the sorted list of hosts to try.
|
|
21
|
+
*/
|
|
22
|
+
export declare function checkDomain(domain: string, opts?: VerifierOptions): Promise<DomainCheck>;
|
|
23
|
+
//# sourceMappingURL=dns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dns.d.ts","sourceRoot":"","sources":["../src/dns.ts"],"names":[],"mappings":"AAGA,OAAO,EAAkB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,KAAK,EAAsB,OAAO,EAAE,MAAM,YAAY,CAAC;AAkB9D,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,qFAAqF;IACrF,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,kDAAkD;IAClD,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAwBD,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CASnE;AAID;;;GAGG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,GAAE,eAAoB,GAAG,OAAO,CAAC,WAAW,CAAC,CAwClG"}
|
package/dist/dns.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Tier 1: DNS. Free and local. Finds domains that cannot receive mail: no MX
|
|
2
|
+
// and a web host behind the A record, a null MX, an MX that does not resolve,
|
|
3
|
+
// a domain that does not exist, a parking host as MX.
|
|
4
|
+
import { resolveOptions } from "./options.js";
|
|
5
|
+
import { errorCode, errorMessage, withTimeout } from "./util.js";
|
|
6
|
+
// MX hosts that forward rather than hold mail. They bounce when the forward
|
|
7
|
+
// target is dead, and some refuse relays outright.
|
|
8
|
+
const FORWARDER_MX = [
|
|
9
|
+
/registrar-servers\.com$/i, // Namecheap eforward1..5
|
|
10
|
+
/improvmx\.com$/i,
|
|
11
|
+
/forwardemail\.net$/i,
|
|
12
|
+
/mx\.cloudflare\.net$/i, // Cloudflare Email Routing route1..3
|
|
13
|
+
/fwd\d*\.porkbun\.com$/i,
|
|
14
|
+
/mailforward\./i,
|
|
15
|
+
/forwardmx\./i,
|
|
16
|
+
];
|
|
17
|
+
// Hosts that serve web pages, not mail. An MX pointing here times out.
|
|
18
|
+
const WEBHOST_MX = [/pixie\.porkbun\.com$/i, /parkingcrew\./i, /sedoparking\./i, /bodis\./i, /above\.com$/i];
|
|
19
|
+
const isNullMx = (mx) => mx.length === 1 && mx[0]?.exchange === "" && mx[0]?.priority === 0;
|
|
20
|
+
async function lookupAny(resolver, host) {
|
|
21
|
+
const [v4, v6] = await Promise.all([resolver.resolve4(host).catch(() => []), resolver.resolve6(host).catch(() => [])]);
|
|
22
|
+
return { v4, v6, all: [...v4, ...v6] };
|
|
23
|
+
}
|
|
24
|
+
async function reverseName(resolver, ip, ms) {
|
|
25
|
+
try {
|
|
26
|
+
const names = await withTimeout(resolver.reverse(ip), "reverse", ms);
|
|
27
|
+
return names[0] ?? null;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Name the web host behind an A record from its IP and reverse name. */
|
|
34
|
+
export function labelWebHost(ip, ptr) {
|
|
35
|
+
const p = (ptr ?? "").toLowerCase();
|
|
36
|
+
if (ip === "75.2.60.5" || ip === "99.83.190.102" || p.includes("netlify"))
|
|
37
|
+
return "Netlify";
|
|
38
|
+
if (ip.startsWith("76.76.21.") || ip.startsWith("216.198.79.") || p.includes("vercel"))
|
|
39
|
+
return "Vercel";
|
|
40
|
+
if (ip.startsWith("104.21.") || ip.startsWith("172.67.") || ip.startsWith("188.114.9") || p.includes("cloudflare"))
|
|
41
|
+
return "Cloudflare";
|
|
42
|
+
if (p.includes("amazonaws") || p.includes("awsglobalaccelerator"))
|
|
43
|
+
return "AWS";
|
|
44
|
+
if (p.includes("github"))
|
|
45
|
+
return "GitHub Pages";
|
|
46
|
+
if (p.includes("squarespace") || p.includes("wixdns") || p.includes("webflow"))
|
|
47
|
+
return "site builder";
|
|
48
|
+
return ptr ?? "unknown host";
|
|
49
|
+
}
|
|
50
|
+
const dead = (reason) => ({ verdict: "DEAD", reason, mx: null, mxHosts: [] });
|
|
51
|
+
/**
|
|
52
|
+
* The DNS tier for one domain. Returns the verdict, the primary MX host name
|
|
53
|
+
* (null when the domain is DEAD) and the sorted list of hosts to try.
|
|
54
|
+
*/
|
|
55
|
+
export async function checkDomain(domain, opts = {}) {
|
|
56
|
+
const o = resolveOptions(opts);
|
|
57
|
+
const { resolver } = o;
|
|
58
|
+
let mx;
|
|
59
|
+
try {
|
|
60
|
+
mx = await withTimeout(resolver.resolveMx(domain), "MX", o.dnsTimeoutMs);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
const code = errorCode(err) ?? "";
|
|
64
|
+
if (code === "ENOTFOUND")
|
|
65
|
+
return dead("domain does not exist (NXDOMAIN)");
|
|
66
|
+
if (code !== "ENODATA" && !/timed out/.test(errorMessage(err)))
|
|
67
|
+
return dead(`MX lookup failed (${code || errorMessage(err)})`);
|
|
68
|
+
mx = [];
|
|
69
|
+
}
|
|
70
|
+
if (mx.length === 0) {
|
|
71
|
+
const ips = await withTimeout(lookupAny(resolver, domain), "A/AAAA", o.dnsTimeoutMs);
|
|
72
|
+
const ip = ips.all[0];
|
|
73
|
+
if (ip === undefined)
|
|
74
|
+
return dead("no MX and no A/AAAA record");
|
|
75
|
+
const host = labelWebHost(ip, await reverseName(resolver, ip, o.dnsTimeoutMs));
|
|
76
|
+
return dead(`no MX record; mail falls back to the A record ${ip} (${host}), which does not take mail`);
|
|
77
|
+
}
|
|
78
|
+
if (isNullMx(mx))
|
|
79
|
+
return dead("null MX (RFC 7505): the domain accepts no mail");
|
|
80
|
+
const sorted = [...mx].sort((x, y) => x.priority - y.priority).map((r) => r.exchange.replace(/\.$/, "").toLowerCase());
|
|
81
|
+
const primary = sorted[0];
|
|
82
|
+
const secondary = sorted[1];
|
|
83
|
+
if (WEBHOST_MX.some((re) => re.test(primary)))
|
|
84
|
+
return dead(`MX ${primary} is a web or parking host, not a mail server`);
|
|
85
|
+
const primaryIps = await withTimeout(lookupAny(resolver, primary), "MX host", o.dnsTimeoutMs);
|
|
86
|
+
const primaryIp = primaryIps.v4[0] ?? primaryIps.v6[0];
|
|
87
|
+
if (primaryIp === undefined)
|
|
88
|
+
return dead(`MX host ${primary} does not resolve`);
|
|
89
|
+
const mxHosts = [{ host: primary, ip: primaryIp }];
|
|
90
|
+
if (secondary !== undefined && secondary !== primary)
|
|
91
|
+
mxHosts.push({ host: secondary, ip: null });
|
|
92
|
+
if (FORWARDER_MX.some((re) => re.test(primary))) {
|
|
93
|
+
return { verdict: "WEAK", reason: `MX ${primary} is a forwarder; it bounces when the forward target is dead`, mx: primary, mxHosts };
|
|
94
|
+
}
|
|
95
|
+
return { verdict: "OK", reason: `MX ${primary}`, mx: primary, mxHosts };
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=dns.js.map
|
package/dist/dns.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dns.js","sourceRoot":"","sources":["../src/dns.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** The package version, read from package.json so a bump is one edit there. */
|
|
2
|
+
export declare const VERSION: string;
|
|
3
|
+
export { parseAddress, type AddressError, type ParsedAddress, type ParseResult } from "./address.ts";
|
|
4
|
+
export { checkDomain, labelWebHost, type DomainCheck, type MxHost } from "./dns.ts";
|
|
5
|
+
export { DEFAULTS, resolveOptions, type ResolvedOptions, type VerifierOptions } from "./options.ts";
|
|
6
|
+
export { probeMailbox, PORT_BLOCKED, type ProbeOptions, type ProbeResult, type ProbeSmtp } from "./smtp.ts";
|
|
7
|
+
export { ACTIONS, VERDICTS, createVerifier, summarize, verify, verifyBatch, type Verifier, type VerifierState } from "./verifier.ts";
|
|
8
|
+
export { errorMessage } from "./util.ts";
|
|
9
|
+
export type { Action, Checks, MxRecord, Resolver, SmtpCheck, Summary, Verdict, VerifyResult } from "./types.ts";
|
|
10
|
+
export type { ApiErrorBody, BalanceResponse, CheckoutResponse, SignupResponse, VerifyResponse } from "./api.ts";
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAmBA,+EAA+E;AAC/E,eAAO,MAAM,OAAO,EAAE,MAA2F,CAAC;AAElH,OAAO,EAAE,YAAY,EAAE,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AACrG,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,KAAK,WAAW,EAAE,KAAK,MAAM,EAAE,MAAM,UAAU,CAAC;AACpF,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,KAAK,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACpG,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAC5G,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AACrI,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAChH,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// MX Probe engine. Zero dependencies, Node 20+.
|
|
2
|
+
//
|
|
3
|
+
// Two tiers, run in order:
|
|
4
|
+
// 1. DNS. Free and local. Finds domains that cannot receive mail: no MX and
|
|
5
|
+
// a web host behind the A record, a null MX, an MX that does not resolve,
|
|
6
|
+
// a domain that does not exist, a parking host as MX.
|
|
7
|
+
// 2. SMTP probe. Connects to the MX on port 25, says EHLO and MAIL FROM,
|
|
8
|
+
// asks RCPT TO for the address and for a random local part (the catch-all
|
|
9
|
+
// test), then QUITs. No message is ever sent. Needs outbound port 25.
|
|
10
|
+
//
|
|
11
|
+
// The verdict contract, the same in the CLI, the MCP server and the API:
|
|
12
|
+
// { email, action, verdict, reason, checks: { syntax, mx, smtp, catch_all } }
|
|
13
|
+
// action = send | hold | kill
|
|
14
|
+
// verdict = OK | WEAK | DEAD
|
|
15
|
+
// `hold` never becomes `kill` on a refusal, a greylist or a catch-all. Only
|
|
16
|
+
// a 5xx that names the mailbox kills.
|
|
17
|
+
import { createRequire } from "node:module";
|
|
18
|
+
/** The package version, read from package.json so a bump is one edit there. */
|
|
19
|
+
export const VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
20
|
+
export { parseAddress } from "./address.js";
|
|
21
|
+
export { checkDomain, labelWebHost } from "./dns.js";
|
|
22
|
+
export { DEFAULTS, resolveOptions } from "./options.js";
|
|
23
|
+
export { probeMailbox, PORT_BLOCKED } from "./smtp.js";
|
|
24
|
+
export { ACTIONS, VERDICTS, createVerifier, summarize, verify, verifyBatch } from "./verifier.js";
|
|
25
|
+
export { errorMessage } from "./util.js";
|
|
26
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,EAAE;AACF,2BAA2B;AAC3B,8EAA8E;AAC9E,+EAA+E;AAC/E,2DAA2D;AAC3D,2EAA2E;AAC3E,+EAA+E;AAC/E,2EAA2E;AAC3E,EAAE;AACF,yEAAyE;AACzE,gFAAgF;AAChF,iCAAiC;AACjC,iCAAiC;AACjC,8EAA8E;AAC9E,wCAAwC;AAExC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,+EAA+E;AAC/E,MAAM,CAAC,MAAM,OAAO,GAAY,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,iBAAiB,CAAyB,CAAC,OAAO,CAAC;AAElH,OAAO,EAAE,YAAY,EAA2D,MAAM,cAAc,CAAC;AACrG,OAAO,EAAE,WAAW,EAAE,YAAY,EAAiC,MAAM,UAAU,CAAC;AACpF,OAAO,EAAE,QAAQ,EAAE,cAAc,EAA8C,MAAM,cAAc,CAAC;AACpG,OAAO,EAAE,YAAY,EAAE,YAAY,EAAuD,MAAM,WAAW,CAAC;AAC5G,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAqC,MAAM,eAAe,CAAC;AACrI,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Resolver } from "./types.ts";
|
|
2
|
+
export interface VerifierOptions {
|
|
3
|
+
/** Run the SMTP probe after the DNS tier. Needs outbound port 25. */
|
|
4
|
+
smtp?: boolean;
|
|
5
|
+
dnsTimeoutMs?: number;
|
|
6
|
+
smtpTimeoutMs?: number;
|
|
7
|
+
smtpConcurrency?: number;
|
|
8
|
+
dnsConcurrency?: number;
|
|
9
|
+
/** The name given in EHLO. Its reverse DNS should match the probing IP. */
|
|
10
|
+
helo?: string;
|
|
11
|
+
/** The MAIL FROM address. */
|
|
12
|
+
from?: string;
|
|
13
|
+
port?: number;
|
|
14
|
+
/** Connect here instead of the MX host, for tests against a local server. */
|
|
15
|
+
hostOverride?: string | null;
|
|
16
|
+
/**
|
|
17
|
+
* When true, an ECONNREFUSED / EHOSTUNREACH / ENETUNREACH on a probe turns
|
|
18
|
+
* the SMTP tier off for the rest of the run: the network blocks port 25.
|
|
19
|
+
* The hosted API sets this to false because its port is proven by a health
|
|
20
|
+
* check, and one refusing MX must not switch the tier off for everyone.
|
|
21
|
+
*/
|
|
22
|
+
autoDisableSmtp?: boolean;
|
|
23
|
+
resolver?: Resolver;
|
|
24
|
+
}
|
|
25
|
+
export type ResolvedOptions = Required<VerifierOptions>;
|
|
26
|
+
export declare const DEFAULTS: Readonly<ResolvedOptions>;
|
|
27
|
+
/** Defaults under the caller's options. An explicit `undefined` keeps the default. */
|
|
28
|
+
export declare function resolveOptions(opts?: VerifierOptions): ResolvedOptions;
|
|
29
|
+
//# sourceMappingURL=options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6BAA6B;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;;;;OAKG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAED,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC,eAAe,CAAC,CAAC;AAExD,eAAO,MAAM,QAAQ,EAAE,QAAQ,CAAC,eAAe,CAY7C,CAAC;AAEH,sFAAsF;AACtF,wBAAgB,cAAc,CAAC,IAAI,GAAE,eAAoB,GAAG,eAAe,CAM1E"}
|
package/dist/options.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { promises as dnsPromises } from "node:dns";
|
|
2
|
+
export const DEFAULTS = Object.freeze({
|
|
3
|
+
smtp: false,
|
|
4
|
+
dnsTimeoutMs: 8000,
|
|
5
|
+
smtpTimeoutMs: 12000,
|
|
6
|
+
smtpConcurrency: 3,
|
|
7
|
+
dnsConcurrency: 20,
|
|
8
|
+
helo: "probe.mxprobe.dev",
|
|
9
|
+
from: "probe@mxprobe.dev",
|
|
10
|
+
port: 25,
|
|
11
|
+
hostOverride: null,
|
|
12
|
+
autoDisableSmtp: true,
|
|
13
|
+
resolver: dnsPromises,
|
|
14
|
+
});
|
|
15
|
+
/** Defaults under the caller's options. An explicit `undefined` keeps the default. */
|
|
16
|
+
export function resolveOptions(opts = {}) {
|
|
17
|
+
const out = { ...DEFAULTS };
|
|
18
|
+
for (const [key, value] of Object.entries(opts)) {
|
|
19
|
+
if (value !== undefined)
|
|
20
|
+
out[key] = value;
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=options.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,WAAW,EAAE,MAAM,UAAU,CAAC;AA6BnD,MAAM,CAAC,MAAM,QAAQ,GAA8B,MAAM,CAAC,MAAM,CAAC;IAC/D,IAAI,EAAE,KAAK;IACX,YAAY,EAAE,IAAI;IAClB,aAAa,EAAE,KAAK;IACpB,eAAe,EAAE,CAAC;IAClB,cAAc,EAAE,EAAE;IAClB,IAAI,EAAE,mBAAmB;IACzB,IAAI,EAAE,mBAAmB;IACzB,IAAI,EAAE,EAAE;IACR,YAAY,EAAE,IAAI;IAClB,eAAe,EAAE,IAAI;IACrB,QAAQ,EAAE,WAAW;CACtB,CAAC,CAAC;AAEH,sFAAsF;AACtF,MAAM,UAAU,cAAc,CAAC,IAAI,GAAoB,EAAE;IACvD,MAAM,GAAG,GAA4B,EAAE,GAAG,QAAQ,EAAE,CAAC;IACrD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAChD,IAAI,KAAK,KAAK,SAAS;YAAE,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IAC5C,CAAC;IACD,OAAO,GAAsB,CAAC;AAChC,CAAC"}
|
package/dist/smtp.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type VerifierOptions } from "./options.ts";
|
|
2
|
+
import type { SmtpCheck, Verdict } from "./types.ts";
|
|
3
|
+
/** Connect errors that mean the network blocks port 25, not that one MX is down. */
|
|
4
|
+
export declare const PORT_BLOCKED: ReadonlySet<string>;
|
|
5
|
+
export type ProbeSmtp = Exclude<SmtpCheck, "skipped">;
|
|
6
|
+
export interface ProbeResult {
|
|
7
|
+
verdict: Verdict;
|
|
8
|
+
reason: string;
|
|
9
|
+
smtp: ProbeSmtp;
|
|
10
|
+
catchAll: boolean | null;
|
|
11
|
+
/** The connect error code when smtp is `unreachable`. */
|
|
12
|
+
connectError?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ProbeOptions extends VerifierOptions {
|
|
15
|
+
/** Connect to this address (the resolved MX IP) while naming mxHost in the reason. */
|
|
16
|
+
connectHost?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Probe one mailbox on one MX. `smtp` is accepted | rejected | refused |
|
|
20
|
+
* deferred | unreachable | dropped. Never sends DATA.
|
|
21
|
+
*/
|
|
22
|
+
export declare function probeMailbox(email: string, domain: string, mxHost: string, opts?: ProbeOptions): Promise<ProbeResult>;
|
|
23
|
+
//# sourceMappingURL=smtp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"smtp.d.ts","sourceRoot":"","sources":["../src/smtp.ts"],"names":[],"mappings":"AAKA,OAAO,EAAkB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACpE,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAQrD,oFAAoF;AACpF,eAAO,MAAM,YAAY,EAAE,WAAW,CAAC,MAAM,CAA4D,CAAC;AAE1G,MAAM,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;AAEtD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC;IACzB,yDAAyD;IACzD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,YAAa,SAAQ,eAAe;IACnD,sFAAsF;IACtF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AA2FD;;;GAGG;AACH,wBAAsB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CA6C/H"}
|
package/dist/smtp.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Tier 2: the SMTP probe. Connects to the MX on port 25, says EHLO and MAIL
|
|
2
|
+
// FROM, asks RCPT TO for the address and for a random local part (the
|
|
3
|
+
// catch-all test), then QUITs. No message is ever sent. Needs outbound port 25.
|
|
4
|
+
import net from "node:net";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
import { resolveOptions } from "./options.js";
|
|
7
|
+
import { errorCode, errorMessage, withTimeout } from "./util.js";
|
|
8
|
+
// Reply text that means "this mailbox does not exist", as opposed to "we do
|
|
9
|
+
// not like you" (5.7.x) or "not now" (4xx).
|
|
10
|
+
const NO_SUCH_MAILBOX = /5\.1\.[0136]\b|5\.4\.1\b|user unknown|unknown user|does not exist|doesn't exist|no such (user|recipient|mailbox)|not found|not exist|no mailbox|invalid recipient|recipient rejected|recipient address rejected|unrouteable|unknown recipient|not our customer|mailbox unavailable|address rejected|invalid mailbox|unknown address/i;
|
|
11
|
+
/** Connect errors that mean the network blocks port 25, not that one MX is down. */
|
|
12
|
+
export const PORT_BLOCKED = new Set(["ECONNREFUSED", "EHOSTUNREACH", "ENETUNREACH"]);
|
|
13
|
+
const codedError = (message, code) => Object.assign(new Error(message), { code });
|
|
14
|
+
// A line-oriented SMTP client: connect, then read() a reply or send(cmd) and
|
|
15
|
+
// read its reply. Multi-line replies (250-... 250 ...) come back as one.
|
|
16
|
+
function connectSmtp(host, port, timeoutMs) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const socket = net.createConnection({ host, port });
|
|
19
|
+
socket.setEncoding("utf8");
|
|
20
|
+
socket.setTimeout(timeoutMs);
|
|
21
|
+
let buffer = "";
|
|
22
|
+
let lines = [];
|
|
23
|
+
const queued = [];
|
|
24
|
+
let waiter = null;
|
|
25
|
+
let dead = null;
|
|
26
|
+
let connected = false;
|
|
27
|
+
const fail = (err) => {
|
|
28
|
+
dead = dead ?? err;
|
|
29
|
+
if (waiter) {
|
|
30
|
+
const w = waiter;
|
|
31
|
+
waiter = null;
|
|
32
|
+
w.reject(err);
|
|
33
|
+
}
|
|
34
|
+
if (!connected)
|
|
35
|
+
reject(err);
|
|
36
|
+
socket.destroy();
|
|
37
|
+
};
|
|
38
|
+
socket.on("timeout", () => fail(codedError("SMTP timeout", "ETIMEDOUT")));
|
|
39
|
+
socket.on("error", fail);
|
|
40
|
+
socket.on("close", () => fail(codedError("connection closed", "ECLOSED")));
|
|
41
|
+
socket.on("data", (chunk) => {
|
|
42
|
+
buffer += chunk;
|
|
43
|
+
let idx;
|
|
44
|
+
while ((idx = buffer.indexOf("\n")) !== -1) {
|
|
45
|
+
const line = buffer.slice(0, idx).replace(/\r$/, "");
|
|
46
|
+
buffer = buffer.slice(idx + 1);
|
|
47
|
+
lines.push(line);
|
|
48
|
+
if (/^\d{3}( |$)/.test(line)) {
|
|
49
|
+
const reply = { code: Number(line.slice(0, 3)), text: lines.join(" | ") };
|
|
50
|
+
lines = [];
|
|
51
|
+
if (waiter) {
|
|
52
|
+
const w = waiter;
|
|
53
|
+
waiter = null;
|
|
54
|
+
w.resolve(reply);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
queued.push(reply);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
const read = () => new Promise((res, rej) => {
|
|
63
|
+
const next = queued.shift();
|
|
64
|
+
if (next)
|
|
65
|
+
return res(next);
|
|
66
|
+
if (dead)
|
|
67
|
+
return rej(dead);
|
|
68
|
+
waiter = { resolve: res, reject: rej };
|
|
69
|
+
});
|
|
70
|
+
const send = (cmd) => {
|
|
71
|
+
if (dead)
|
|
72
|
+
return Promise.reject(dead);
|
|
73
|
+
socket.write(`${cmd}\r\n`);
|
|
74
|
+
return read();
|
|
75
|
+
};
|
|
76
|
+
socket.once("connect", () => {
|
|
77
|
+
connected = true;
|
|
78
|
+
resolve({ read, send, close: () => socket.destroy() });
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const firstLine = (reply) => reply.text.split(" | ")[0] ?? "";
|
|
83
|
+
const refused = (reason) => ({ verdict: "WEAK", reason, smtp: "refused", catchAll: null });
|
|
84
|
+
const deferred = (reason) => ({ verdict: "WEAK", reason, smtp: "deferred", catchAll: null });
|
|
85
|
+
/**
|
|
86
|
+
* Probe one mailbox on one MX. `smtp` is accepted | rejected | refused |
|
|
87
|
+
* deferred | unreachable | dropped. Never sends DATA.
|
|
88
|
+
*/
|
|
89
|
+
export async function probeMailbox(email, domain, mxHost, opts = {}) {
|
|
90
|
+
const o = resolveOptions(opts);
|
|
91
|
+
const target = o.hostOverride ?? opts.connectHost ?? mxHost;
|
|
92
|
+
let s;
|
|
93
|
+
try {
|
|
94
|
+
s = await withTimeout(connectSmtp(target, o.port, o.smtpTimeoutMs), "connect", o.smtpTimeoutMs);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
const code = errorCode(err) ?? "EUNKNOWN";
|
|
98
|
+
return { verdict: "WEAK", reason: `cannot connect to ${mxHost}:${o.port} (${code})`, smtp: "unreachable", catchAll: null, connectError: code };
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const banner = await s.read();
|
|
102
|
+
if (banner.code !== 220)
|
|
103
|
+
return refused(`${mxHost} greeted with ${firstLine(banner)}`);
|
|
104
|
+
let r = await s.send(`EHLO ${o.helo}`);
|
|
105
|
+
if (r.code !== 250) {
|
|
106
|
+
r = await s.send(`HELO ${o.helo}`);
|
|
107
|
+
if (r.code !== 250)
|
|
108
|
+
return refused(`${mxHost} refused HELO (${firstLine(r)})`);
|
|
109
|
+
}
|
|
110
|
+
r = await s.send(`MAIL FROM:<${o.from}>`);
|
|
111
|
+
if (r.code !== 250) {
|
|
112
|
+
return r.code >= 500 ? refused(`${mxHost} refused the sender (${firstLine(r)})`) : deferred(`${mxHost} deferred the sender (${firstLine(r)})`);
|
|
113
|
+
}
|
|
114
|
+
r = await s.send(`RCPT TO:<${email}>`);
|
|
115
|
+
let result;
|
|
116
|
+
if (r.code === 250 || r.code === 251) {
|
|
117
|
+
const random = `${randomBytes(6).toString("hex")}-probe@${domain}`;
|
|
118
|
+
const c = await s.send(`RCPT TO:<${random}>`);
|
|
119
|
+
result =
|
|
120
|
+
c.code === 250 || c.code === 251
|
|
121
|
+
? { verdict: "WEAK", reason: `${domain} is catch-all: ${mxHost} accepts any local part, so the mailbox cannot be proven`, smtp: "accepted", catchAll: true }
|
|
122
|
+
: { verdict: "OK", reason: `mailbox accepted by ${mxHost}`, smtp: "accepted", catchAll: false };
|
|
123
|
+
}
|
|
124
|
+
else if (r.code >= 500 && NO_SUCH_MAILBOX.test(r.text)) {
|
|
125
|
+
result = { verdict: "DEAD", reason: `${mxHost} says the mailbox does not exist (${firstLine(r)})`, smtp: "rejected", catchAll: null };
|
|
126
|
+
}
|
|
127
|
+
else if (r.code >= 500) {
|
|
128
|
+
result = refused(`${mxHost} refused the probe, not the mailbox (${firstLine(r)})`);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
result = deferred(`${mxHost} deferred (${firstLine(r)})`);
|
|
132
|
+
}
|
|
133
|
+
await s.send("QUIT").catch(() => { });
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
return { verdict: "WEAK", reason: `${mxHost} dropped the session (${errorCode(err) ?? errorMessage(err)})`, smtp: "dropped", catchAll: null };
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
s.close();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=smtp.js.map
|
package/dist/smtp.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"smtp.js","sourceRoot":"","sources":["../src/smtp.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,sEAAsE;AACtE,gFAAgF;AAChF,OAAO,GAAG,MAAM,UAAU,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAwB,MAAM,cAAc,CAAC;AAEpE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAEjE,4EAA4E;AAC5E,4CAA4C;AAC5C,MAAM,eAAe,GACnB,sUAAsU,CAAC;AAEzU,oFAAoF;AACpF,MAAM,CAAC,MAAM,YAAY,GAAwB,IAAI,GAAG,CAAC,CAAC,cAAc,EAAE,cAAc,EAAE,aAAa,CAAC,CAAC,CAAC;AAmC1G,MAAM,UAAU,GAAG,CAAC,OAAe,EAAE,IAAY,EAAyB,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;AAEzH,6EAA6E;AAC7E,yEAAyE;AACzE,SAAS,WAAW,CAAC,IAAY,EAAE,IAAY,EAAE,SAAiB;IAChE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,MAAM,GAAG,GAAG,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACpD,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QAC3B,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;QAC7B,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,IAAI,KAAK,GAAa,EAAE,CAAC;QACzB,MAAM,MAAM,GAAY,EAAE,CAAC;QAC3B,IAAI,MAAM,GAAkB,IAAI,CAAC;QACjC,IAAI,IAAI,GAAiB,IAAI,CAAC;QAC9B,IAAI,SAAS,GAAG,KAAK,CAAC;QAEtB,MAAM,IAAI,GAAG,CAAC,GAAU,EAAQ,EAAE;YAChC,IAAI,GAAG,IAAI,IAAI,GAAG,CAAC;YACnB,IAAI,MAAM,EAAE,CAAC;gBACX,MAAM,CAAC,GAAG,MAAM,CAAC;gBACjB,MAAM,GAAG,IAAI,CAAC;gBACd,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAChB,CAAC;YACD,IAAI,CAAC,SAAS;gBAAE,MAAM,CAAC,GAAG,CAAC,CAAC;YAC5B,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,CAAC,CAAC;QACF,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC;QAC1E,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QACzB,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,mBAAmB,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;QAC3E,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YAClC,MAAM,IAAI,KAAK,CAAC;YAChB,IAAI,GAAW,CAAC;YAChB,OAAO,CAAC,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;gBAC3C,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;gBACrD,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;gBAC/B,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACjB,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;oBAC7B,MAAM,KAAK,GAAU,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;oBACjF,KAAK,GAAG,EAAE,CAAC;oBACX,IAAI,MAAM,EAAE,CAAC;wBACX,MAAM,CAAC,GAAG,MAAM,CAAC;wBACjB,MAAM,GAAG,IAAI,CAAC;wBACd,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;oBACnB,CAAC;yBAAM,CAAC;wBACN,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;oBACrB,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,GAAmB,EAAE,CAChC,IAAI,OAAO,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;YACvB,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC;YAC5B,IAAI,IAAI;gBAAE,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC;YAC3B,IAAI,IAAI;gBAAE,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC;YAC3B,MAAM,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;QACzC,CAAC,CAAC,CAAC;QACL,MAAM,IAAI,GAAG,CAAC,GAAW,EAAkB,EAAE;YAC3C,IAAI,IAAI;gBAAE,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YACtC,MAAM,CAAC,KAAK,CAAC,GAAG,GAAG,MAAM,CAAC,CAAC;YAC3B,OAAO,IAAI,EAAE,CAAC;QAChB,CAAC,CAAC;QACF,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE;YAC1B,SAAS,GAAG,IAAI,CAAC;YACjB,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QACzD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,SAAS,GAAG,CAAC,KAAY,EAAU,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AAC7E,MAAM,OAAO,GAAG,CAAC,MAAc,EAAe,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;AAChH,MAAM,QAAQ,GAAG,CAAC,MAAc,EAAe,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;AAElH;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,KAAa,EAAE,MAAc,EAAE,MAAc,EAAE,IAAI,GAAiB,EAAE;IACvG,MAAM,CAAC,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,MAAM,GAAG,CAAC,CAAC,YAAY,IAAI,IAAI,CAAC,WAAW,IAAI,MAAM,CAAC;IAC5D,IAAI,CAAc,CAAC;IACnB,IAAI,CAAC;QACH,CAAC,GAAG,MAAM,WAAW,CAAC,WAAW,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,aAAa,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC;IAClG,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,UAAU,CAAC;QAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,qBAAqB,MAAM,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI,GAAG,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;IACjJ,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QAC9B,IAAI,MAAM,CAAC,IAAI,KAAK,GAAG;YAAE,OAAO,OAAO,CAAC,GAAG,MAAM,iBAAiB,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACvF,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACvC,IAAI,CAAC,CAAC,IAAI,KAAK,GAAG,EAAE,CAAC;YACnB,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;YACnC,IAAI,CAAC,CAAC,IAAI,KAAK,GAAG;gBAAE,OAAO,OAAO,CAAC,GAAG,MAAM,kBAAkB,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QACjF,CAAC;QACD,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC;QAC1C,IAAI,CAAC,CAAC,IAAI,KAAK,GAAG,EAAE,CAAC;YACnB,OAAO,CAAC,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,MAAM,wBAAwB,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,MAAM,yBAAyB,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QACjJ,CAAC;QACD,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,YAAY,KAAK,GAAG,CAAC,CAAC;QACvC,IAAI,MAAmB,CAAC;QACxB,IAAI,CAAC,CAAC,IAAI,KAAK,GAAG,IAAI,CAAC,CAAC,IAAI,KAAK,GAAG,EAAE,CAAC;YACrC,MAAM,MAAM,GAAG,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,UAAU,MAAM,EAAE,CAAC;YACnE,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,YAAY,MAAM,GAAG,CAAC,CAAC;YAC9C,MAAM;gBACJ,CAAC,CAAC,IAAI,KAAK,GAAG,IAAI,CAAC,CAAC,IAAI,KAAK,GAAG;oBAC9B,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,kBAAkB,MAAM,0DAA0D,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,IAAI,EAAE;oBAC5J,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,uBAAuB,MAAM,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;QACtG,CAAC;aAAM,IAAI,CAAC,CAAC,IAAI,IAAI,GAAG,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;YACzD,MAAM,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,qCAAqC,SAAS,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;QACxI,CAAC;aAAM,IAAI,CAAC,CAAC,IAAI,IAAI,GAAG,EAAE,CAAC;YACzB,MAAM,GAAG,OAAO,CAAC,GAAG,MAAM,wCAAwC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QACrF,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,QAAQ,CAAC,GAAG,MAAM,cAAc,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAC5D,CAAC;QACD,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACrC,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,yBAAyB,SAAS,CAAC,GAAG,CAAC,IAAI,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAChJ,CAAC;YAAS,CAAC;QACT,CAAC,CAAC,KAAK,EAAE,CAAC;IACZ,CAAC;AACH,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type Action = "send" | "hold" | "kill";
|
|
2
|
+
export type Verdict = "OK" | "WEAK" | "DEAD";
|
|
3
|
+
/** What the SMTP tier said. `skipped` on the DNS tier. */
|
|
4
|
+
export type SmtpCheck = "skipped" | "accepted" | "rejected" | "refused" | "deferred" | "unreachable" | "dropped";
|
|
5
|
+
export interface Checks {
|
|
6
|
+
syntax: boolean;
|
|
7
|
+
mx: string | null;
|
|
8
|
+
smtp: SmtpCheck;
|
|
9
|
+
catch_all: boolean | null;
|
|
10
|
+
}
|
|
11
|
+
export interface VerifyResult {
|
|
12
|
+
email: string;
|
|
13
|
+
action: Action;
|
|
14
|
+
verdict: Verdict;
|
|
15
|
+
reason: string;
|
|
16
|
+
checks: Checks;
|
|
17
|
+
}
|
|
18
|
+
/** Summary counts for a batch. */
|
|
19
|
+
export interface Summary {
|
|
20
|
+
send: number;
|
|
21
|
+
hold: number;
|
|
22
|
+
kill: number;
|
|
23
|
+
total: number;
|
|
24
|
+
}
|
|
25
|
+
export interface MxRecord {
|
|
26
|
+
exchange: string;
|
|
27
|
+
priority: number;
|
|
28
|
+
}
|
|
29
|
+
/** The part of `node:dns` promises the engine uses. Tests pass a fake. */
|
|
30
|
+
export interface Resolver {
|
|
31
|
+
resolveMx(hostname: string): Promise<MxRecord[]>;
|
|
32
|
+
resolve4(hostname: string): Promise<string[]>;
|
|
33
|
+
resolve6(hostname: string): Promise<string[]>;
|
|
34
|
+
reverse(ip: string): Promise<string[]>;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAC9C,MAAM,MAAM,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AAE7C,0DAA0D;AAC1D,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,aAAa,GAAG,SAAS,CAAC;AAEjH,MAAM,WAAW,MAAM;IACrB,MAAM,EAAE,OAAO,CAAC;IAChB,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,IAAI,EAAE,SAAS,CAAC;IAChB,SAAS,EAAE,OAAO,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,kCAAkC;AAClC,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,0EAA0E;AAC1E,MAAM,WAAW,QAAQ;IACvB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IACjD,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC9C,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC9C,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CACxC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// The verdict contract, the same in the CLI, the MCP server and the API:
|
|
2
|
+
// { email, action, verdict, reason, checks: { syntax, mx, smtp, catch_all } }
|
|
3
|
+
// action = send | hold | kill
|
|
4
|
+
// verdict = OK | WEAK | DEAD
|
|
5
|
+
// `hold` never becomes `kill` on a refusal, a greylist or a catch-all. Only
|
|
6
|
+
// a 5xx that names the mailbox kills.
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,gFAAgF;AAChF,iCAAiC;AACjC,iCAAiC;AACjC,8EAA8E;AAC9E,wCAAwC"}
|
package/dist/util.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Reject after `ms` with a message that names the step. */
|
|
2
|
+
export declare function withTimeout<T>(promise: Promise<T>, label: string, ms: number): Promise<T>;
|
|
3
|
+
/** The `code` of a Node error (ENOTFOUND, ECONNREFUSED, ...), else undefined. */
|
|
4
|
+
export declare function errorCode(err: unknown): string | undefined;
|
|
5
|
+
export declare function errorMessage(err: unknown): string;
|
|
6
|
+
//# sourceMappingURL=util.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAC5D,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAMzF;AAED,iFAAiF;AACjF,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAM1D;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAEjD"}
|
package/dist/util.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Reject after `ms` with a message that names the step. */
|
|
2
|
+
export function withTimeout(promise, label, ms) {
|
|
3
|
+
let timer;
|
|
4
|
+
const timeout = new Promise((_, reject) => {
|
|
5
|
+
timer = setTimeout(() => reject(new Error(`${label}: timed out after ${ms} ms`)), ms);
|
|
6
|
+
});
|
|
7
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
8
|
+
}
|
|
9
|
+
/** The `code` of a Node error (ENOTFOUND, ECONNREFUSED, ...), else undefined. */
|
|
10
|
+
export function errorCode(err) {
|
|
11
|
+
if (typeof err === "object" && err !== null && "code" in err) {
|
|
12
|
+
const code = err.code;
|
|
13
|
+
if (typeof code === "string")
|
|
14
|
+
return code;
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
export function errorMessage(err) {
|
|
19
|
+
return err instanceof Error ? err.message : String(err);
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=util.js.map
|