mailifier 0.1.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/README.md +105 -0
- package/dist/address.d.ts +17 -0
- package/dist/address.js +144 -0
- package/dist/address.js.map +1 -0
- package/dist/client.d.ts +19 -0
- package/dist/client.js +53 -0
- package/dist/client.js.map +1 -0
- package/dist/dns.d.ts +23 -0
- package/dist/dns.js +52 -0
- package/dist/dns.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/local.d.ts +29 -0
- package/dist/local.js +117 -0
- package/dist/local.js.map +1 -0
- package/dist/mailifier.mjs +384 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +55 -0
- package/dist/main.js.map +1 -0
- package/dist/server.d.ts +22 -0
- package/dist/server.js +89 -0
- package/dist/server.js.map +1 -0
- package/dist/smtp.d.ts +68 -0
- package/dist/smtp.js +251 -0
- package/dist/smtp.js.map +1 -0
- package/dist/verdict.d.ts +29 -0
- package/dist/verdict.js +14 -0
- package/dist/verdict.js.map +1 -0
- package/package.json +64 -0
package/dist/smtp.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SMTP handshake a paid verification service runs, from a host of yours.
|
|
3
|
+
*
|
|
4
|
+
* MX lookup → connect on 25 → EHLO → MAIL FROM → RCPT TO <the address> → QUIT. The
|
|
5
|
+
* server's answer to RCPT is the verdict: 250 accepted, 5xx no such user, 4xx "ask
|
|
6
|
+
* later" (greylisting). A second RCPT to a random local part tells catch-all domains
|
|
7
|
+
* apart from real acceptance. No DATA, so nothing is ever delivered.
|
|
8
|
+
*
|
|
9
|
+
* Honest limits: a catch-all domain (most Microsoft 365 tenants) says yes to anything,
|
|
10
|
+
* so the verdict is `catch_all`, not `valid`; an MX that will not talk to us is `risky`,
|
|
11
|
+
* never `invalid`. Both are what the paid services return too. Etiquette: one
|
|
12
|
+
* connection per MX at a time, a gap between probes, a HELO name whose forward and
|
|
13
|
+
* reverse DNS match, and MAIL FROM at that same name so a curious postmaster can look
|
|
14
|
+
* us up. Most clouds close outbound port 25 by default, so this usually runs on one host
|
|
15
|
+
* that can, with everything else asking that host over HTTP (`client.ts`, `server.ts`).
|
|
16
|
+
*/
|
|
17
|
+
import { randomBytes } from "node:crypto";
|
|
18
|
+
import { createConnection } from "node:net";
|
|
19
|
+
import { resolve as dohResolve } from "./dns.js";
|
|
20
|
+
export const SMTP_PORT = 25;
|
|
21
|
+
const CRLF = "\r\n";
|
|
22
|
+
const DEFAULT_TIMEOUT_MS = 12_000;
|
|
23
|
+
const MAX_MX_TRIED = 3;
|
|
24
|
+
class SmtpReply extends Error {
|
|
25
|
+
code;
|
|
26
|
+
text;
|
|
27
|
+
constructor(code, text) {
|
|
28
|
+
super(`${code} ${text}`);
|
|
29
|
+
this.code = code;
|
|
30
|
+
this.text = text;
|
|
31
|
+
this.name = "SmtpReply";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const replyCode = (reply) => Number.parseInt(reply.slice(0, 3), 10);
|
|
35
|
+
/**
|
|
36
|
+
* "No such user" in the words the big hosts use when the enhanced code is not 5.1.x:
|
|
37
|
+
* Microsoft 365 says "5.4.1 Recipient address rejected: Access denied", consumer
|
|
38
|
+
* Outlook "5.5.0 mailbox unavailable".
|
|
39
|
+
*/
|
|
40
|
+
const REJECTED_USER = /user unknown|no such user|does not exist|unknown user|recipient rejected|recipient address rejected|invalid recipient|no mailbox|mailbox not found|mailbox unavailable|address rejected/i;
|
|
41
|
+
const replyClass = (code) => Math.floor(code / 100);
|
|
42
|
+
/** RFC 3463 enhanced code carried in the text, e.g. "5.1.1". */
|
|
43
|
+
const enhanced = (reply) => /\b([245])\.(\d{1,3})\.(\d{1,3})\b/.exec(reply);
|
|
44
|
+
/**
|
|
45
|
+
* MX hosts in priority order (lowest number first), falling back to the domain's own A
|
|
46
|
+
* record when there is none, as mail does. Empty = nothing to connect to.
|
|
47
|
+
*/
|
|
48
|
+
export async function mailHosts(domain, resolver) {
|
|
49
|
+
const mx = (await resolver(domain, "MX"))
|
|
50
|
+
.map((rr) => {
|
|
51
|
+
const [priority, host] = rr.trim().split(/\s+/);
|
|
52
|
+
return { priority: Number(priority), host: (host ?? "").replace(/\.$/, "").toLowerCase() };
|
|
53
|
+
})
|
|
54
|
+
.filter((r) => r.host !== "" && r.host !== ".")
|
|
55
|
+
.sort((a, b) => a.priority - b.priority);
|
|
56
|
+
if (mx.length > 0)
|
|
57
|
+
return [...new Set(mx.map((r) => r.host))];
|
|
58
|
+
const a = await resolver(domain, "A");
|
|
59
|
+
return a.length > 0 ? [domain] : [];
|
|
60
|
+
}
|
|
61
|
+
/** Talk to one MX about one address. Throws SmtpReply for a refusal before RCPT, or a socket error. */
|
|
62
|
+
async function converse(conv, email, opts, transcript) {
|
|
63
|
+
const step = async (line) => {
|
|
64
|
+
if (line !== null)
|
|
65
|
+
await conv.write(line);
|
|
66
|
+
const reply = await conv.read();
|
|
67
|
+
const code = replyCode(reply);
|
|
68
|
+
transcript.push({ sent: line, code, reply: reply.slice(0, 200) });
|
|
69
|
+
return { code, reply };
|
|
70
|
+
};
|
|
71
|
+
const expect = async (line, ok) => {
|
|
72
|
+
const r = await step(line);
|
|
73
|
+
if (r.code !== ok)
|
|
74
|
+
throw new SmtpReply(r.code, r.reply);
|
|
75
|
+
return r;
|
|
76
|
+
};
|
|
77
|
+
await expect(null, 220);
|
|
78
|
+
await expect(`EHLO ${opts.helo}`, 250);
|
|
79
|
+
await expect(`MAIL FROM:<${opts.mailFrom}>`, 250);
|
|
80
|
+
const rcpt = await step(`RCPT TO:<${email}>`);
|
|
81
|
+
let randomAccepted = null;
|
|
82
|
+
if (replyClass(rcpt.code) === 2) {
|
|
83
|
+
const domain = email.slice(email.lastIndexOf("@") + 1);
|
|
84
|
+
const probe = await step(`RCPT TO:<${opts.random()}@${domain}>`);
|
|
85
|
+
randomAccepted = replyClass(probe.code) === 2;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
await conv.write("QUIT");
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Already said what we needed.
|
|
92
|
+
}
|
|
93
|
+
conv.close();
|
|
94
|
+
return { code: rcpt.code, reply: rcpt.reply, randomAccepted };
|
|
95
|
+
}
|
|
96
|
+
/** Read the RCPT answer as a verdict. */
|
|
97
|
+
function readRcpt(code, reply, randomAccepted) {
|
|
98
|
+
const klass = replyClass(code);
|
|
99
|
+
if (klass === 2) {
|
|
100
|
+
return randomAccepted
|
|
101
|
+
? { result: "catch_all", reason: "catch_all" }
|
|
102
|
+
: { result: "valid", reason: "accepted" };
|
|
103
|
+
}
|
|
104
|
+
const enh = enhanced(reply);
|
|
105
|
+
if (klass === 5) {
|
|
106
|
+
// 5.1.x = bad mailbox/address: the definitive "no such user". Other 5xx (5.7.x
|
|
107
|
+
// policy, 554 blocked, 552 quota) say something about us or the box, not the address.
|
|
108
|
+
if (enh?.[2] === "1" || REJECTED_USER.test(reply))
|
|
109
|
+
return { result: "invalid", reason: "rejected" };
|
|
110
|
+
return { result: "risky", reason: "blocked" };
|
|
111
|
+
}
|
|
112
|
+
return { result: "risky", reason: "greylisted" };
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Probe one address: the first MX that will talk decides. An MX that refuses us
|
|
116
|
+
* before RCPT (policy 5xx at EHLO/MAIL FROM) or cannot be reached is skipped for the
|
|
117
|
+
* next; when every one does, the verdict is risky, with the reason.
|
|
118
|
+
*/
|
|
119
|
+
export async function probeMailbox(email, opts) {
|
|
120
|
+
const resolver = opts.resolver ?? ((n, t) => dohResolve(n, t));
|
|
121
|
+
const dial = opts.dial ?? dialTcp;
|
|
122
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
123
|
+
const conv = {
|
|
124
|
+
helo: opts.helo,
|
|
125
|
+
mailFrom: opts.mailFrom ?? `postmaster@${opts.helo}`,
|
|
126
|
+
random: opts.random ?? (() => `wren-${randomBytes(6).toString("hex")}`),
|
|
127
|
+
};
|
|
128
|
+
const domain = email.slice(email.lastIndexOf("@") + 1).toLowerCase();
|
|
129
|
+
const hosts = await mailHosts(domain, resolver);
|
|
130
|
+
const transcript = [];
|
|
131
|
+
if (hosts.length === 0)
|
|
132
|
+
return { result: "invalid", reason: "no_mx", mx: null, code: null, transcript };
|
|
133
|
+
let lastReason = "unreachable";
|
|
134
|
+
for (const host of hosts.slice(0, MAX_MX_TRIED)) {
|
|
135
|
+
let session;
|
|
136
|
+
try {
|
|
137
|
+
session = await dial(host, SMTP_PORT, timeoutMs);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
transcript.push({ sent: null, code: 0, reply: `connect ${host}: ${errorName(err)}` });
|
|
141
|
+
lastReason = "unreachable";
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const { code, reply, randomAccepted } = await converse(session, email, conv, transcript);
|
|
146
|
+
return { ...readRcpt(code, reply, randomAccepted), mx: host, code, transcript };
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
session.close();
|
|
150
|
+
if (err instanceof SmtpReply) {
|
|
151
|
+
// A greeting or envelope refusal: about us, not the address. Try the next MX.
|
|
152
|
+
lastReason = replyClass(err.code) === 4 ? "greylisted" : "blocked";
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
transcript.push({ sent: null, code: 0, reply: `${host}: ${errorName(err)}` });
|
|
156
|
+
lastReason = "unreachable";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { result: "risky", reason: lastReason, mx: null, code: null, transcript };
|
|
160
|
+
}
|
|
161
|
+
const errorName = (err) => err instanceof Error
|
|
162
|
+
? `${err.name}${err.code ? ` ${err.code}` : ""}`
|
|
163
|
+
: String(err);
|
|
164
|
+
/** The real thing: a TCP socket read line by line, multi-line replies joined. */
|
|
165
|
+
export const dialTcp = (host, port, timeoutMs) => new Promise((resolveConn, reject) => {
|
|
166
|
+
const socket = createConnection({ host, port });
|
|
167
|
+
let buffer = "";
|
|
168
|
+
let waiting = null;
|
|
169
|
+
const fail = (err) => {
|
|
170
|
+
if (waiting) {
|
|
171
|
+
waiting.reject(err);
|
|
172
|
+
waiting = null;
|
|
173
|
+
}
|
|
174
|
+
else
|
|
175
|
+
reject(err);
|
|
176
|
+
socket.destroy();
|
|
177
|
+
};
|
|
178
|
+
socket.setTimeout(timeoutMs, () => fail(Object.assign(new Error("smtp timeout"), { code: "ETIMEDOUT" })));
|
|
179
|
+
socket.on("error", fail);
|
|
180
|
+
socket.on("close", () => fail(Object.assign(new Error("closed"), { code: "ECONNRESET" })));
|
|
181
|
+
const pump = () => {
|
|
182
|
+
// A reply is complete when its last line has a space after the code ("250 ok"),
|
|
183
|
+
// continuation lines use a dash ("250-SIZE").
|
|
184
|
+
const lines = buffer.split(CRLF);
|
|
185
|
+
const end = lines.findIndex((l) => /^\d{3} /.test(l) || (l.length > 0 && /^\d{3}$/.test(l)));
|
|
186
|
+
if (end < 0 || !waiting)
|
|
187
|
+
return;
|
|
188
|
+
const reply = lines.slice(0, end + 1).join("\n");
|
|
189
|
+
buffer = lines.slice(end + 1).join(CRLF);
|
|
190
|
+
const w = waiting;
|
|
191
|
+
waiting = null;
|
|
192
|
+
w.resolve(reply);
|
|
193
|
+
};
|
|
194
|
+
socket.on("data", (chunk) => {
|
|
195
|
+
buffer += chunk.toString("latin1");
|
|
196
|
+
pump();
|
|
197
|
+
});
|
|
198
|
+
socket.once("connect", () => resolveConn({
|
|
199
|
+
read: () => new Promise((res, rej) => {
|
|
200
|
+
waiting = { resolve: res, reject: rej };
|
|
201
|
+
pump();
|
|
202
|
+
}),
|
|
203
|
+
write: (line) => new Promise((res, rej) => socket.write(`${line}${CRLF}`, (err) => (err ? rej(err) : res()))),
|
|
204
|
+
close: () => {
|
|
205
|
+
socket.removeAllListeners("close");
|
|
206
|
+
socket.end();
|
|
207
|
+
socket.destroy();
|
|
208
|
+
},
|
|
209
|
+
}));
|
|
210
|
+
});
|
|
211
|
+
/**
|
|
212
|
+
* The probe over `probeMailbox`, serialising per MX host with a gap between them: one
|
|
213
|
+
* conversation at a time with any given server, because we are a guest there.
|
|
214
|
+
*/
|
|
215
|
+
export class SmtpProbe {
|
|
216
|
+
opts;
|
|
217
|
+
name = "smtp";
|
|
218
|
+
lastByHost = new Map();
|
|
219
|
+
gapMs;
|
|
220
|
+
sleep;
|
|
221
|
+
constructor(opts) {
|
|
222
|
+
this.opts = opts;
|
|
223
|
+
this.gapMs = opts.perHostGapMs ?? 1_500;
|
|
224
|
+
this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
225
|
+
}
|
|
226
|
+
async verify(email) {
|
|
227
|
+
const domain = email.slice(email.lastIndexOf("@") + 1).toLowerCase();
|
|
228
|
+
const resolver = this.opts.resolver ?? ((n, t) => dohResolve(n, t));
|
|
229
|
+
// The queue key is the primary MX: what we are actually about to knock on.
|
|
230
|
+
const key = (await mailHosts(domain, resolver))[0] ?? domain;
|
|
231
|
+
const previous = this.lastByHost.get(key) ?? Promise.resolve();
|
|
232
|
+
const turn = previous.then(async () => {
|
|
233
|
+
const outcome = await probeMailbox(email, { ...this.opts, resolver });
|
|
234
|
+
await this.sleep(this.gapMs);
|
|
235
|
+
return outcome;
|
|
236
|
+
});
|
|
237
|
+
this.lastByHost.set(key, turn.then(() => undefined, () => undefined));
|
|
238
|
+
const outcome = await turn;
|
|
239
|
+
return {
|
|
240
|
+
result: outcome.result,
|
|
241
|
+
raw: {
|
|
242
|
+
reason: outcome.reason,
|
|
243
|
+
mx: outcome.mx,
|
|
244
|
+
code: outcome.code,
|
|
245
|
+
helo: this.opts.helo,
|
|
246
|
+
transcript: outcome.transcript,
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
//# 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":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a mailbox check can say, and the one interface every way of asking implements.
|
|
3
|
+
*
|
|
4
|
+
* Four answers, and the honest thing is that only two of them are facts:
|
|
5
|
+
* valid the mail server accepted the address; that server says the mailbox is there
|
|
6
|
+
* invalid the mail server refused it by name ("no such user"), or the domain takes no mail
|
|
7
|
+
* catch_all the domain accepts everything, so acceptance says nothing about this address
|
|
8
|
+
* risky nobody would tell us: greylisting, a refused connection, a policy block
|
|
9
|
+
*
|
|
10
|
+
* `raw` is the evidence behind the answer (which MX, which reply code, the transcript),
|
|
11
|
+
* kept so a verdict can be argued with later. It never holds a credential.
|
|
12
|
+
*/
|
|
13
|
+
export declare const MAILBOX_RESULTS: readonly ["valid", "invalid", "risky", "catch_all"];
|
|
14
|
+
export type MailboxResult = (typeof MAILBOX_RESULTS)[number];
|
|
15
|
+
export interface Verdict {
|
|
16
|
+
result: MailboxResult;
|
|
17
|
+
raw: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A way of asking whether a mailbox exists. `SmtpProbe` dials the mail server itself;
|
|
21
|
+
* `RemoteProbe` asks another host that can. Callers depend on this, not on either one.
|
|
22
|
+
*
|
|
23
|
+
* Deliberately not here: whether a verdict may be trusted, and whether it costs money.
|
|
24
|
+
* Those are the caller's policy about a probe, not something a probe knows about itself.
|
|
25
|
+
*/
|
|
26
|
+
export interface MailboxProbe {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
verify(email: string): Promise<Verdict>;
|
|
29
|
+
}
|
package/dist/verdict.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a mailbox check can say, and the one interface every way of asking implements.
|
|
3
|
+
*
|
|
4
|
+
* Four answers, and the honest thing is that only two of them are facts:
|
|
5
|
+
* valid the mail server accepted the address; that server says the mailbox is there
|
|
6
|
+
* invalid the mail server refused it by name ("no such user"), or the domain takes no mail
|
|
7
|
+
* catch_all the domain accepts everything, so acceptance says nothing about this address
|
|
8
|
+
* risky nobody would tell us: greylisting, a refused connection, a policy block
|
|
9
|
+
*
|
|
10
|
+
* `raw` is the evidence behind the answer (which MX, which reply code, the transcript),
|
|
11
|
+
* kept so a verdict can be argued with later. It never holds a credential.
|
|
12
|
+
*/
|
|
13
|
+
export const MAILBOX_RESULTS = ["valid", "invalid", "risky", "catch_all"];
|
|
14
|
+
//# sourceMappingURL=verdict.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verdict.js","sourceRoot":"","sources":["../src/verdict.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,CAAU,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mailifier",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"packageManager": "pnpm@9.15.0",
|
|
5
|
+
"description": "Ask a mail server whether an address exists, over SMTP, without sending anything.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"mailifier": "./dist/main.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "node scripts/clean.mjs && tsc -p tsconfig.build.json",
|
|
28
|
+
"bundle": "node scripts/bundle.mjs",
|
|
29
|
+
"prepare": "pnpm build && pnpm bundle",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"test": "vitest run src",
|
|
32
|
+
"lint": "biome check .",
|
|
33
|
+
"prepublishOnly": "pnpm lint && pnpm typecheck && pnpm test"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@biomejs/biome": "2.5.14",
|
|
37
|
+
"@types/node": "22.20.3",
|
|
38
|
+
"esbuild": "0.28.2",
|
|
39
|
+
"tsx": "4.23.13",
|
|
40
|
+
"typescript": "5.9.3",
|
|
41
|
+
"vitest": "5.0.1"
|
|
42
|
+
},
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/wrenautomation/mailifier.git"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/wrenautomation/mailifier#readme",
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/wrenautomation/mailifier/issues"
|
|
50
|
+
},
|
|
51
|
+
"keywords": [
|
|
52
|
+
"email",
|
|
53
|
+
"verification",
|
|
54
|
+
"smtp",
|
|
55
|
+
"mailbox",
|
|
56
|
+
"deliverability",
|
|
57
|
+
"bounce",
|
|
58
|
+
"mx",
|
|
59
|
+
"rcpt"
|
|
60
|
+
],
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"access": "public"
|
|
63
|
+
}
|
|
64
|
+
}
|