@blamejs/core 0.9.28 → 0.9.38
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/CHANGELOG.md +885 -875
- package/index.js +18 -1
- package/lib/agent-snapshot.js +346 -0
- package/lib/agent-trace.js +218 -0
- package/lib/guard-all.js +1 -0
- package/lib/guard-dsn.js +379 -0
- package/lib/guard-envelope.js +294 -0
- package/lib/guard-smtp-command.js +484 -0
- package/lib/guard-snapshot-envelope.js +168 -0
- package/lib/guard-trace-context.js +172 -0
- package/lib/ip-utils.js +102 -0
- package/lib/mail-auth.js +4 -35
- package/lib/mail-greylist.js +448 -0
- package/lib/mail-helo.js +473 -0
- package/lib/mail-rbl.js +392 -0
- package/lib/mail.js +2 -1
- package/lib/network-dns-resolver.js +500 -0
- package/lib/network.js +1 -0
- package/lib/redis-client.js +2 -1
- package/lib/safe-dns.js +665 -0
- package/lib/tracing.js +36 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.mail.greylist
|
|
4
|
+
* @nav Mail
|
|
5
|
+
* @title Mail Greylist
|
|
6
|
+
* @order 545
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* RFC 6647 email greylisting primitive. Defers first-seen senders
|
|
10
|
+
* with a 4yz SMTP tempfail so transient connections (spam, snowshoe
|
|
11
|
+
* campaigns, single-attempt botnets) drop and legitimate MTAs (which
|
|
12
|
+
* retry per RFC 5321 §4.5.4) re-connect after the operator-configured
|
|
13
|
+
* minimum delay and pass through.
|
|
14
|
+
*
|
|
15
|
+
* ## Triplet fingerprint (RFC 6647 §4.4)
|
|
16
|
+
*
|
|
17
|
+
* Each greylist entry is keyed by the operator-recommended triple:
|
|
18
|
+
*
|
|
19
|
+
* - **Connection IP** (CIDR-normalized — default `/24` for IPv4,
|
|
20
|
+
* `/64` for IPv6 so adjacent retry hosts from the same MTA
|
|
21
|
+
* cluster share a single entry).
|
|
22
|
+
* - **RFC 5321 MailFrom** (envelope-from; lowercased; bounces
|
|
23
|
+
* carry `<>` and key as the literal empty string).
|
|
24
|
+
* - **First RFC 5321 RcptTo** — RFC 6647 §4.4 notes legitimate
|
|
25
|
+
* MTAs don't reorder recipients on retry, so keying on the
|
|
26
|
+
* first RcptTo is sufficient.
|
|
27
|
+
*
|
|
28
|
+
* The triplet is hashed via `b.crypto.namespaceHash("mail.greylist",
|
|
29
|
+
* ip-cidr + "\\0" + mailfrom + "\\0" + first-rcpt)` so the on-disk
|
|
30
|
+
* key is unlinkable to the PII triplet (privacy + GDPR Art. 5(1)(c)
|
|
31
|
+
* data minimization).
|
|
32
|
+
*
|
|
33
|
+
* ## Window + TTL (RFC 6647 §4.5)
|
|
34
|
+
*
|
|
35
|
+
* - **`minDelayMs`** — minimum delay between first-seen and
|
|
36
|
+
* accept-on-retry (default 5 minutes; RFC 6647 §4.5 recommends
|
|
37
|
+
* "from one minute to 24 hours"). Retries inside the window get
|
|
38
|
+
* another 4yz tempfail; the existing fingerprint stays in place.
|
|
39
|
+
* - **`whitelistTtlMs`** — duration to remember a passed-greylist
|
|
40
|
+
* entry (default 36 days; RFC 6647 §4.5 recommends ≥1 week). On
|
|
41
|
+
* expiry the entry is gc'd and the next first-seen attempt is
|
|
42
|
+
* deferred again.
|
|
43
|
+
* - **`maxEntries`** — operator-configurable upper bound on the
|
|
44
|
+
* active-fingerprint store (default 1M). Bounds memory for the
|
|
45
|
+
* in-memory backend; the dbStore backend is bounded by DB row
|
|
46
|
+
* count.
|
|
47
|
+
*
|
|
48
|
+
* ## Backend abstraction
|
|
49
|
+
*
|
|
50
|
+
* `b.mail.greylist.create({ store: <store>, ... })` accepts any
|
|
51
|
+
* `{ get(key) → entry|null, put(key, entry, ttlMs), delete(key),
|
|
52
|
+
* gc(olderThanMs) → count }`-shaped backend. In-memory default
|
|
53
|
+
* ships for single-process MX deployments; the operator wires a
|
|
54
|
+
* sqlite-backed adapter or external DB for multi-process MX
|
|
55
|
+
* fleets (a retry landing on a different process needs to see the
|
|
56
|
+
* fingerprint planted by the first attempt).
|
|
57
|
+
*
|
|
58
|
+
* ## Verdict
|
|
59
|
+
*
|
|
60
|
+
* `instance.check({ ip, mailFrom, rcptTo })` → `{ action, reason,
|
|
61
|
+
* firstSeenAt?, ttlExpiresAt? }`:
|
|
62
|
+
*
|
|
63
|
+
* - **`"defer"`** — first-seen or retry-too-soon. Operator returns
|
|
64
|
+
* SMTP `451 4.7.1 <reason>` (RFC 6647 §4.5 + RFC 5321 §4.2.5).
|
|
65
|
+
* - **`"accept"`** — within the post-acceptance whitelist window;
|
|
66
|
+
* operator continues the SMTP transaction.
|
|
67
|
+
* - **`"accept-first-pass"`** — retry after `minDelayMs` elapsed
|
|
68
|
+
* on a previously-deferred fingerprint; operator continues AND
|
|
69
|
+
* the framework marks the fingerprint as whitelisted for the
|
|
70
|
+
* full TTL window.
|
|
71
|
+
*
|
|
72
|
+
* ## CVE / threat model
|
|
73
|
+
*
|
|
74
|
+
* - **Snowshoe + single-attempt bot flood** — the defining defense:
|
|
75
|
+
* transient sources don't retry, so they never reach the message
|
|
76
|
+
* body. Pre-DKIM / pre-content-scan defense — cheap rejection.
|
|
77
|
+
* - **Fingerprint-store poisoning** — operator-supplied IPs +
|
|
78
|
+
* mailfrom strings are hashed (no raw PII on disk) and bounded
|
|
79
|
+
* (`maxEntries`); a hostile peer that tries to inflate the store
|
|
80
|
+
* hits the cap and the framework rotates oldest-first.
|
|
81
|
+
* - **CIDR-aggregation bypass** — operators with retry-aware MTA
|
|
82
|
+
* clusters (Gmail, Outlook, AWS SES) need /24 IPv4 and /64 IPv6
|
|
83
|
+
* so the cluster's retry from a different host in the same
|
|
84
|
+
* subnet passes; the defaults match real-world MTA behavior.
|
|
85
|
+
*
|
|
86
|
+
* ## When NOT to greylist
|
|
87
|
+
*
|
|
88
|
+
* - Listserv submissions (operator opts the listserv source out
|
|
89
|
+
* via `allowedSources` per RFC 6647 §6.2).
|
|
90
|
+
* - First-time newsletter sign-up confirmations (a single first
|
|
91
|
+
* attempt would defer the confirmation email; operator opts
|
|
92
|
+
* submission relay paths out).
|
|
93
|
+
* - High-priority transactional sources the operator has direct
|
|
94
|
+
* relationship with (banking, healthcare 2FA, etc.).
|
|
95
|
+
*
|
|
96
|
+
* @card
|
|
97
|
+
* RFC 6647 SMTP greylisting. Triplet fingerprint (IP CIDR + MailFrom + first RcptTo), namespace-hashed, configurable minDelayMs + whitelistTtlMs windows. Defers first-seen senders with SMTP 451 4.7.1; legitimate retries pass and stay whitelisted. Pluggable backend.
|
|
98
|
+
*/
|
|
99
|
+
|
|
100
|
+
var C = require("./constants");
|
|
101
|
+
var { defineClass } = require("./framework-error");
|
|
102
|
+
var bCrypto = require("./crypto");
|
|
103
|
+
var lazyRequire = require("./lazy-require");
|
|
104
|
+
var ipUtils = require("./ip-utils");
|
|
105
|
+
|
|
106
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
107
|
+
|
|
108
|
+
var MailGreylistError = defineClass("MailGreylistError", { alwaysPermanent: true });
|
|
109
|
+
|
|
110
|
+
var DEFAULT_MIN_DELAY_MS = C.TIME.minutes(5);
|
|
111
|
+
var DEFAULT_WHITELIST_TTL = C.TIME.days(36);
|
|
112
|
+
var DEFAULT_MAX_ENTRIES = 1000000; // allow:raw-byte-literal — entry-count cap, not bytes
|
|
113
|
+
var DEFAULT_IPV4_PREFIX = 24; // allow:raw-byte-literal — RFC 6647 §4.4 IP-clustering granularity
|
|
114
|
+
var DEFAULT_IPV6_PREFIX = 64; // allow:raw-byte-literal — RFC 6647 §4.4 IPv6 IP-clustering granularity
|
|
115
|
+
var DEFAULT_PROFILE = "strict";
|
|
116
|
+
|
|
117
|
+
var PROFILES = Object.freeze({
|
|
118
|
+
// Strict: low delay, modest whitelist TTL. Catches snowshoe but
|
|
119
|
+
// retries from legitimate MTAs (which back off 5-15 min on tempfail)
|
|
120
|
+
// pass quickly.
|
|
121
|
+
strict: { minDelayMs: C.TIME.minutes(5), whitelistTtlMs: C.TIME.days(36), ipv4Prefix: 24, ipv6Prefix: 64 }, // allow:raw-byte-literal — RFC 6647 §4.4 prefixes
|
|
122
|
+
// Balanced: minimum 1 min delay, shorter TTL for higher churn.
|
|
123
|
+
balanced: { minDelayMs: C.TIME.minutes(1), whitelistTtlMs: C.TIME.days(7), ipv4Prefix: 24, ipv6Prefix: 64 }, // allow:raw-byte-literal — RFC 6647 §4.4 prefixes
|
|
124
|
+
// Permissive: 30s delay, 30-day TTL. For operators that want
|
|
125
|
+
// greylisting present but minimally visible.
|
|
126
|
+
permissive: { minDelayMs: C.TIME.seconds(30), whitelistTtlMs: C.TIME.days(30), ipv4Prefix: 32, ipv6Prefix: 128 }, // allow:raw-byte-literal — RFC 6647 §4.4 prefixes
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
var COMPLIANCE_POSTURES = Object.freeze({
|
|
130
|
+
hipaa: "strict",
|
|
131
|
+
"pci-dss": "strict",
|
|
132
|
+
gdpr: "strict",
|
|
133
|
+
soc2: "strict",
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
var IPV4_RE = /^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/; // allow:regex-no-length-cap — anchored + per-octet repeat-cap
|
|
137
|
+
var IPV6_RE = /^[0-9a-fA-F:]+$/; // allow:regex-no-length-cap — length-checked separately
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @primitive b.mail.greylist.create
|
|
141
|
+
* @signature b.mail.greylist.create(opts?)
|
|
142
|
+
* @since 0.9.34
|
|
143
|
+
* @status stable
|
|
144
|
+
* @related b.mail.rbl.create
|
|
145
|
+
*
|
|
146
|
+
* Build a greylist instance. Returns an object with `.check(ctx) →
|
|
147
|
+
* Promise<verdict>` and `.gc({ olderThanMs }) → Promise<{ removed }>`.
|
|
148
|
+
*
|
|
149
|
+
* @opts
|
|
150
|
+
* profile: "strict" | "balanced" | "permissive",
|
|
151
|
+
* posture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
|
|
152
|
+
* store: { get, put, delete, gc } — pluggable backend
|
|
153
|
+
* minDelayMs: number — overrides profile minimum-delay window
|
|
154
|
+
* whitelistTtlMs: number — overrides profile post-acceptance TTL
|
|
155
|
+
* maxEntries: number — in-memory backend's entry cap
|
|
156
|
+
* allowedSources: Array<string> — IPs / CIDRs that skip greylisting
|
|
157
|
+
* audit: b.audit namespace
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* var gl = b.mail.greylist.create({ profile: "strict" });
|
|
161
|
+
* var v = await gl.check({
|
|
162
|
+
* ip: "203.0.113.42",
|
|
163
|
+
* mailFrom: "sender@example.com",
|
|
164
|
+
* rcptTo: "alice@operator.example",
|
|
165
|
+
* });
|
|
166
|
+
* if (v.action === "defer") return reply(451, "4.7.1 " + v.reason);
|
|
167
|
+
*/
|
|
168
|
+
function create(opts) {
|
|
169
|
+
opts = opts || {};
|
|
170
|
+
var profile = opts.profile || (opts.posture && COMPLIANCE_POSTURES[opts.posture]) || DEFAULT_PROFILE;
|
|
171
|
+
if (!PROFILES[profile]) {
|
|
172
|
+
throw new MailGreylistError("mail-greylist/bad-profile",
|
|
173
|
+
"create: unknown profile '" + profile + "'");
|
|
174
|
+
}
|
|
175
|
+
var caps = PROFILES[profile];
|
|
176
|
+
var minDelayMs = typeof opts.minDelayMs === "number" ? opts.minDelayMs : caps.minDelayMs;
|
|
177
|
+
var whitelistTtlMs = typeof opts.whitelistTtlMs === "number" ? opts.whitelistTtlMs : caps.whitelistTtlMs;
|
|
178
|
+
var maxEntries = typeof opts.maxEntries === "number" ? opts.maxEntries : DEFAULT_MAX_ENTRIES;
|
|
179
|
+
var ipv4Prefix = caps.ipv4Prefix;
|
|
180
|
+
var ipv6Prefix = caps.ipv6Prefix;
|
|
181
|
+
var auditImpl = opts.audit || audit();
|
|
182
|
+
var allowedSources = Array.isArray(opts.allowedSources) ? opts.allowedSources.slice() : [];
|
|
183
|
+
|
|
184
|
+
if (!isFinite(minDelayMs) || minDelayMs < 0) {
|
|
185
|
+
throw new MailGreylistError("mail-greylist/bad-input",
|
|
186
|
+
"create: minDelayMs must be a non-negative finite number");
|
|
187
|
+
}
|
|
188
|
+
if (!isFinite(whitelistTtlMs) || whitelistTtlMs <= 0) {
|
|
189
|
+
throw new MailGreylistError("mail-greylist/bad-input",
|
|
190
|
+
"create: whitelistTtlMs must be a positive finite number");
|
|
191
|
+
}
|
|
192
|
+
if (!isFinite(maxEntries) || maxEntries <= 0) {
|
|
193
|
+
throw new MailGreylistError("mail-greylist/bad-input",
|
|
194
|
+
"create: maxEntries must be a positive finite number");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
var store = opts.store || _memoryStore(maxEntries);
|
|
198
|
+
|
|
199
|
+
async function check(ctx) {
|
|
200
|
+
if (!ctx || typeof ctx !== "object") {
|
|
201
|
+
throw new MailGreylistError("mail-greylist/bad-input",
|
|
202
|
+
"check: ctx must be a plain object");
|
|
203
|
+
}
|
|
204
|
+
if (typeof ctx.ip !== "string" || ctx.ip.length === 0) {
|
|
205
|
+
throw new MailGreylistError("mail-greylist/bad-input",
|
|
206
|
+
"check: ctx.ip must be a non-empty string");
|
|
207
|
+
}
|
|
208
|
+
if (typeof ctx.mailFrom !== "string") {
|
|
209
|
+
throw new MailGreylistError("mail-greylist/bad-input",
|
|
210
|
+
"check: ctx.mailFrom must be a string (use '' for bounce sender)");
|
|
211
|
+
}
|
|
212
|
+
if (typeof ctx.rcptTo !== "string" || ctx.rcptTo.length === 0) {
|
|
213
|
+
throw new MailGreylistError("mail-greylist/bad-input",
|
|
214
|
+
"check: ctx.rcptTo must be a non-empty string");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Allowed-source bypass — operator-supplied IPs / CIDRs that
|
|
218
|
+
// skip greylisting (RFC 6647 §6.2). Listservs, transactional
|
|
219
|
+
// partners, etc.
|
|
220
|
+
if (_isAllowed(ctx.ip, allowedSources)) {
|
|
221
|
+
_emitAudit(auditImpl, "mail.greylist.bypassed", {
|
|
222
|
+
reason: "allowed-source", ip: ctx.ip,
|
|
223
|
+
});
|
|
224
|
+
return { action: "accept", reason: "allowed-source" };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
var cidr = _cidrKey(ctx.ip, ipv4Prefix, ipv6Prefix);
|
|
228
|
+
var fingerprint = _hashFingerprint(cidr, ctx.mailFrom.toLowerCase(), ctx.rcptTo.toLowerCase());
|
|
229
|
+
|
|
230
|
+
var now = typeof ctx.now === "number" ? ctx.now : Date.now();
|
|
231
|
+
var existing = await store.get(fingerprint);
|
|
232
|
+
|
|
233
|
+
if (!existing) {
|
|
234
|
+
// First-seen: defer + persist fingerprint with firstSeenAt.
|
|
235
|
+
await store.put(fingerprint, {
|
|
236
|
+
firstSeenAt: now,
|
|
237
|
+
whitelistedAt: null,
|
|
238
|
+
kind: "deferred",
|
|
239
|
+
}, minDelayMs + whitelistTtlMs); // total lifetime so the entry survives the delay window
|
|
240
|
+
_emitAudit(auditImpl, "mail.greylist.deferred", {
|
|
241
|
+
firstSeen: true, cidr: cidr,
|
|
242
|
+
});
|
|
243
|
+
return { action: "defer", reason: "first-seen", firstSeenAt: now };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (existing.kind === "whitelisted") {
|
|
247
|
+
if (now > existing.ttlExpiresAt) {
|
|
248
|
+
// TTL elapsed without recent traffic — RFC 6647 §4.5
|
|
249
|
+
// recommends gc'ing and re-greylisting on the next attempt.
|
|
250
|
+
// We do it on read so a low-traffic deployment doesn't carry
|
|
251
|
+
// stale rows.
|
|
252
|
+
await store.delete(fingerprint);
|
|
253
|
+
await store.put(fingerprint, {
|
|
254
|
+
firstSeenAt: now,
|
|
255
|
+
whitelistedAt: null,
|
|
256
|
+
kind: "deferred",
|
|
257
|
+
}, minDelayMs + whitelistTtlMs);
|
|
258
|
+
_emitAudit(auditImpl, "mail.greylist.deferred", {
|
|
259
|
+
firstSeen: false, expired: true, cidr: cidr,
|
|
260
|
+
});
|
|
261
|
+
return { action: "defer", reason: "whitelist-expired-resnap", firstSeenAt: now };
|
|
262
|
+
}
|
|
263
|
+
_emitAudit(auditImpl, "mail.greylist.accepted", { cidr: cidr });
|
|
264
|
+
return {
|
|
265
|
+
action: "accept",
|
|
266
|
+
reason: "whitelisted",
|
|
267
|
+
ttlExpiresAt: existing.ttlExpiresAt,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Deferred entry — check if retry-after delay elapsed.
|
|
272
|
+
if (now - existing.firstSeenAt < minDelayMs) {
|
|
273
|
+
_emitAudit(auditImpl, "mail.greylist.deferred", {
|
|
274
|
+
firstSeen: false, retryTooSoon: true, cidr: cidr,
|
|
275
|
+
});
|
|
276
|
+
return {
|
|
277
|
+
action: "defer",
|
|
278
|
+
reason: "retry-too-soon",
|
|
279
|
+
firstSeenAt: existing.firstSeenAt,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// First-pass: retry-after-delay accepted. Mark whitelisted.
|
|
284
|
+
var ttlExpiresAt = now + whitelistTtlMs;
|
|
285
|
+
await store.put(fingerprint, {
|
|
286
|
+
firstSeenAt: existing.firstSeenAt,
|
|
287
|
+
whitelistedAt: now,
|
|
288
|
+
ttlExpiresAt: ttlExpiresAt,
|
|
289
|
+
kind: "whitelisted",
|
|
290
|
+
}, whitelistTtlMs);
|
|
291
|
+
_emitAudit(auditImpl, "mail.greylist.first_pass", {
|
|
292
|
+
delayMs: now - existing.firstSeenAt, cidr: cidr,
|
|
293
|
+
});
|
|
294
|
+
return {
|
|
295
|
+
action: "accept-first-pass",
|
|
296
|
+
reason: "retry-after-delay",
|
|
297
|
+
firstSeenAt: existing.firstSeenAt,
|
|
298
|
+
ttlExpiresAt: ttlExpiresAt,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function gc(gcOpts) {
|
|
303
|
+
gcOpts = gcOpts || {};
|
|
304
|
+
var olderThanMs = typeof gcOpts.olderThanMs === "number" ? gcOpts.olderThanMs : whitelistTtlMs;
|
|
305
|
+
var removed = await store.gc(olderThanMs);
|
|
306
|
+
_emitAudit(auditImpl, "mail.greylist.gc", { removed: removed });
|
|
307
|
+
return { removed: removed };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
check: check,
|
|
312
|
+
gc: gc,
|
|
313
|
+
profile: profile,
|
|
314
|
+
minDelayMs: minDelayMs,
|
|
315
|
+
whitelistTtlMs: whitelistTtlMs,
|
|
316
|
+
MailGreylistError: MailGreylistError,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* @primitive b.mail.greylist.compliancePosture
|
|
322
|
+
* @signature b.mail.greylist.compliancePosture(posture)
|
|
323
|
+
* @since 0.9.34
|
|
324
|
+
* @status stable
|
|
325
|
+
*
|
|
326
|
+
* Return the effective profile name for a compliance posture, or
|
|
327
|
+
* `null` for unknown posture names.
|
|
328
|
+
*
|
|
329
|
+
* @example
|
|
330
|
+
* b.mail.greylist.compliancePosture("hipaa"); // → "strict"
|
|
331
|
+
*/
|
|
332
|
+
function compliancePosture(posture) {
|
|
333
|
+
return COMPLIANCE_POSTURES[posture] || null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function _hashFingerprint(cidr, mailFrom, rcptTo) {
|
|
337
|
+
// Namespace-hash so the on-disk key is unlinkable to the PII
|
|
338
|
+
// triplet — operator dumps of the greylist table don't leak
|
|
339
|
+
// sender / recipient pairs. The framework's hash primitive
|
|
340
|
+
// (sha3-512 inside namespaceHash) is bound to the "mail.greylist"
|
|
341
|
+
// namespace so a hash never collides with another consumer's hash
|
|
342
|
+
// of the same plaintext.
|
|
343
|
+
return bCrypto.namespaceHash("mail.greylist",
|
|
344
|
+
cidr + "\u0000" + mailFrom + "\u0000" + rcptTo);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function _cidrKey(ip, ipv4Prefix, ipv6Prefix) {
|
|
348
|
+
if (IPV4_RE.test(ip)) {
|
|
349
|
+
var octets = ip.split(".").map(function (s) { return parseInt(s, 10); });
|
|
350
|
+
var prefix = Math.min(32, Math.max(0, ipv4Prefix)); // allow:raw-byte-literal — IPv4 address bit width
|
|
351
|
+
// Apply prefix: zero out the host bits.
|
|
352
|
+
var int = (octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]; // allow:raw-byte-literal — IPv4 byte shifts
|
|
353
|
+
var mask = prefix === 0 ? 0 : (~0 << (32 - prefix)) >>> 0; // allow:raw-byte-literal — IPv4 mask construction
|
|
354
|
+
var masked = (int & mask) >>> 0;
|
|
355
|
+
return [
|
|
356
|
+
(masked >>> 24) & 0xff, // allow:raw-byte-literal — IPv4 byte extraction
|
|
357
|
+
(masked >>> 16) & 0xff,
|
|
358
|
+
(masked >>> 8) & 0xff,
|
|
359
|
+
masked & 0xff,
|
|
360
|
+
].join(".") + "/" + prefix;
|
|
361
|
+
}
|
|
362
|
+
if (IPV6_RE.test(ip)) {
|
|
363
|
+
// Expand IPv6, then mask. Reuse the expansion approach from
|
|
364
|
+
// mail-rbl by inlining since this is a different prefix shape.
|
|
365
|
+
var expanded = ipUtils.expandIpv6Hex(ip);
|
|
366
|
+
if (!expanded) {
|
|
367
|
+
throw new MailGreylistError("mail-greylist/bad-input",
|
|
368
|
+
"IP '" + ip + "' is not a parseable IPv6 address");
|
|
369
|
+
}
|
|
370
|
+
// expanded is 32 hex chars; mask to ipv6Prefix bits.
|
|
371
|
+
var prefixBits = Math.min(128, Math.max(0, ipv6Prefix)); // allow:raw-byte-literal — IPv6 address bit width
|
|
372
|
+
var keepNibbles = Math.floor(prefixBits / 4); // allow:raw-byte-literal — bits-per-hex-nibble
|
|
373
|
+
var keptHex = expanded.slice(0, keepNibbles);
|
|
374
|
+
return keptHex + "*/" + prefixBits;
|
|
375
|
+
}
|
|
376
|
+
throw new MailGreylistError("mail-greylist/bad-input",
|
|
377
|
+
"IP '" + ip + "' is not a parseable IPv4 or IPv6 address");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function _isAllowed(ip, allowedSources) {
|
|
381
|
+
// Allowed-source matching — exact IP only for v0.9.34. CIDR
|
|
382
|
+
// matching deferred to the operator's own b.middleware.ipAllowlist
|
|
383
|
+
// when fine-grained subnet rules are needed; this primitive's
|
|
384
|
+
// value-add is the triplet-greylist contract.
|
|
385
|
+
return allowedSources.indexOf(ip) !== -1;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function _emitAudit(auditImpl, action, metadata) {
|
|
389
|
+
try {
|
|
390
|
+
if (auditImpl && typeof auditImpl.safeEmit === "function") {
|
|
391
|
+
auditImpl.safeEmit({ action: action, outcome: "success", metadata: metadata });
|
|
392
|
+
}
|
|
393
|
+
} catch (_e) { /* drop-silent — audit failure must not block accept loop */ }
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function _memoryStore(maxEntries) {
|
|
397
|
+
var data = new Map();
|
|
398
|
+
var insertionOrder = [];
|
|
399
|
+
return {
|
|
400
|
+
get: async function (key) {
|
|
401
|
+
var entry = data.get(key);
|
|
402
|
+
return entry ? entry.value : null;
|
|
403
|
+
},
|
|
404
|
+
put: async function (key, value, ttlMs) {
|
|
405
|
+
if (!data.has(key)) {
|
|
406
|
+
if (data.size >= maxEntries) {
|
|
407
|
+
// Evict oldest.
|
|
408
|
+
var oldest = insertionOrder.shift();
|
|
409
|
+
if (oldest) data.delete(oldest);
|
|
410
|
+
}
|
|
411
|
+
insertionOrder.push(key);
|
|
412
|
+
}
|
|
413
|
+
data.set(key, { value: value, putAt: Date.now(), ttlMs: ttlMs });
|
|
414
|
+
},
|
|
415
|
+
delete: async function (key) {
|
|
416
|
+
data.delete(key);
|
|
417
|
+
var i = insertionOrder.indexOf(key);
|
|
418
|
+
if (i !== -1) insertionOrder.splice(i, 1);
|
|
419
|
+
},
|
|
420
|
+
gc: async function (olderThanMs) {
|
|
421
|
+
var now = Date.now();
|
|
422
|
+
var removed = 0;
|
|
423
|
+
data.forEach(function (entry, key) {
|
|
424
|
+
if (now - entry.putAt > olderThanMs) {
|
|
425
|
+
data.delete(key);
|
|
426
|
+
var i = insertionOrder.indexOf(key);
|
|
427
|
+
if (i !== -1) insertionOrder.splice(i, 1);
|
|
428
|
+
removed += 1;
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
return removed;
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
module.exports = {
|
|
437
|
+
create: create,
|
|
438
|
+
compliancePosture: compliancePosture,
|
|
439
|
+
PROFILES: PROFILES,
|
|
440
|
+
COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
|
|
441
|
+
MailGreylistError: MailGreylistError,
|
|
442
|
+
_hashFingerprint: _hashFingerprint,
|
|
443
|
+
_cidrKey: _cidrKey,
|
|
444
|
+
_DEFAULT_MIN_DELAY_MS: DEFAULT_MIN_DELAY_MS,
|
|
445
|
+
_DEFAULT_WHITELIST_TTL: DEFAULT_WHITELIST_TTL,
|
|
446
|
+
_DEFAULT_IPV4_PREFIX: DEFAULT_IPV4_PREFIX,
|
|
447
|
+
_DEFAULT_IPV6_PREFIX: DEFAULT_IPV6_PREFIX,
|
|
448
|
+
};
|