@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
package/lib/mail-rbl.js
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.mail.rbl
|
|
4
|
+
* @nav Mail
|
|
5
|
+
* @title Mail RBL
|
|
6
|
+
* @order 540
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* RFC 5782 DNS-based blocklist (DNSBL) + allowlist (DNSWL) query
|
|
10
|
+
* primitive. Composes `b.network.dns.resolver` for the underlying
|
|
11
|
+
* DNS queries and surfaces a structured `{ listed, allowed,
|
|
12
|
+
* neutral, errors }` shape for the MX listener (v0.9.34) and
|
|
13
|
+
* submission listener (v0.9.35) to consume per-connection.
|
|
14
|
+
*
|
|
15
|
+
* ## Query construction
|
|
16
|
+
*
|
|
17
|
+
* - **IPv4** — octets reversed, blocklist domain suffixed. RFC 5782
|
|
18
|
+
* §2.1: address `192.0.2.99` against `bl.spamcop.net` becomes the
|
|
19
|
+
* query name `99.2.0.192.bl.spamcop.net`.
|
|
20
|
+
* - **IPv6** — nibble-reversed across all 128 bits (32 hex nibbles),
|
|
21
|
+
* blocklist domain suffixed. RFC 5782 §2.4: address
|
|
22
|
+
* `2001:db8::1` against `ugly.example.com` becomes
|
|
23
|
+
* `1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ugly.example.com`.
|
|
24
|
+
* - **Domain blocklists** (Spamhaus DBL / SURBL — RFC 5782 §3) —
|
|
25
|
+
* query the domain directly against the list zone, no reverse.
|
|
26
|
+
*
|
|
27
|
+
* ## A-record semantics (RFC 5782 §2.1)
|
|
28
|
+
*
|
|
29
|
+
* The A-record return is a SEMANTIC code, not a routable address.
|
|
30
|
+
* Convention is `127.0.0.x`:
|
|
31
|
+
* - `127.0.0.2` — listed (generic).
|
|
32
|
+
* - `127.0.0.4+` — operator-specific sub-list (Spamhaus uses
|
|
33
|
+
* `127.0.0.4` SBL, `127.0.0.5` XBL, etc.).
|
|
34
|
+
* - `127.255.255.252+` — RFC 5782 §5 test addresses.
|
|
35
|
+
* The primitive exposes the raw bytes so operator's MX policy can
|
|
36
|
+
* inspect the sub-list code.
|
|
37
|
+
*
|
|
38
|
+
* ## TXT-record reason (RFC 5782 §2.2)
|
|
39
|
+
*
|
|
40
|
+
* Many DNSBLs publish a TXT record alongside the A — short prose
|
|
41
|
+
* describing why the IP is listed (often with a URL for delisting).
|
|
42
|
+
* The primitive fetches it lazily — operator opts in via
|
|
43
|
+
* `{ withReason: true }` per-query when they want to render the
|
|
44
|
+
* reason back to the peer via SMTP 550 message.
|
|
45
|
+
*
|
|
46
|
+
* ## DNSWL allowlists
|
|
47
|
+
*
|
|
48
|
+
* `b.mail.rbl.create({ ..., allowlists: [...] })` — operator wires
|
|
49
|
+
* any list as DNSBL (refuse on listed) OR DNSWL (allow on listed).
|
|
50
|
+
* Same query shape; the verdict semantics differ. RFC 5782 §3.2
|
|
51
|
+
* notes TXT records on DNSWLs are operationally less useful since
|
|
52
|
+
* SMTP can't advise the peer WHY they were accepted, but the field
|
|
53
|
+
* is surfaced for audit visibility regardless.
|
|
54
|
+
*
|
|
55
|
+
* ## CVE / threat model
|
|
56
|
+
*
|
|
57
|
+
* - **Blocklist-cache amplification** — each list query goes through
|
|
58
|
+
* `b.network.dns.resolver` so cache + TTL + serve-stale already
|
|
59
|
+
* defend against amplification + flood from a single hostile peer.
|
|
60
|
+
* - **DoS-by-query** — operator-configurable per-connection
|
|
61
|
+
* concurrent-query cap (default 8) and per-IP query timeout
|
|
62
|
+
* (default 5s); a slow / unresponsive list can't stall the MX
|
|
63
|
+
* listener.
|
|
64
|
+
* - **DNS-poisoning** — every response parses through `b.safeDns`
|
|
65
|
+
* (bounded RR counts, bounded TXT length) via the resolver, so
|
|
66
|
+
* a poisoned upstream response can't smuggle oversized rdata.
|
|
67
|
+
*
|
|
68
|
+
* ## Why it exists
|
|
69
|
+
*
|
|
70
|
+
* The MX listener (v0.9.34) needs RBL queries on every accepted
|
|
71
|
+
* connection for SPF / IP-reputation evaluation; the submission
|
|
72
|
+
* listener checks operator's own submission-rate / spam-source
|
|
73
|
+
* lists. Without this primitive each consumer rolls its own
|
|
74
|
+
* reverse-IP construction + A-record sub-code interpretation, and
|
|
75
|
+
* the per-list query timeout / cap is operator-specific instead of
|
|
76
|
+
* framework-shared.
|
|
77
|
+
*
|
|
78
|
+
* @card
|
|
79
|
+
* RFC 5782 DNSBL + DNSWL query primitive. Composes b.network.dns.resolver;
|
|
80
|
+
* reverses IPv4 octets + IPv6 nibbles; surfaces A-record return code
|
|
81
|
+
* and optional TXT reason. Operator-configurable list set + concurrent-
|
|
82
|
+
* query cap + per-list timeout.
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
var C = require("./constants");
|
|
86
|
+
var { defineClass } = require("./framework-error");
|
|
87
|
+
var lazyRequire = require("./lazy-require");
|
|
88
|
+
var ipUtils = require("./ip-utils");
|
|
89
|
+
|
|
90
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
91
|
+
|
|
92
|
+
var MailRblError = defineClass("MailRblError", { alwaysPermanent: true });
|
|
93
|
+
|
|
94
|
+
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
|
|
95
|
+
var IPV6_HEX_RE = /^[0-9a-fA-F:]+$/; // allow:regex-no-length-cap — checked by length cap below
|
|
96
|
+
var IPV6_MAX_LEN = 39; // allow:raw-byte-literal — max IPv6 textual length (8 groups × 4 hex + 7 colons)
|
|
97
|
+
|
|
98
|
+
var DEFAULT_TIMEOUT_MS = C.TIME.seconds(5);
|
|
99
|
+
var DEFAULT_CONCURRENCY = 8; // allow:raw-byte-literal — concurrent-query cap, not bytes
|
|
100
|
+
var DEFAULT_PROFILE = "strict";
|
|
101
|
+
|
|
102
|
+
var PROFILES = Object.freeze({
|
|
103
|
+
strict: { maxConcurrent: 8, perListTimeoutMs: C.TIME.seconds(5), maxListsPerQuery: 16 }, // allow:raw-byte-literal — list-count cap
|
|
104
|
+
balanced: { maxConcurrent: 16, perListTimeoutMs: C.TIME.seconds(10), maxListsPerQuery: 32 }, // allow:raw-byte-literal — list-count cap
|
|
105
|
+
permissive: { maxConcurrent: 32, perListTimeoutMs: C.TIME.seconds(20), maxListsPerQuery: 64 }, // allow:raw-byte-literal — list-count cap
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
var COMPLIANCE_POSTURES = Object.freeze({
|
|
109
|
+
hipaa: "strict",
|
|
110
|
+
"pci-dss": "strict",
|
|
111
|
+
gdpr: "strict",
|
|
112
|
+
soc2: "strict",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @primitive b.mail.rbl.create
|
|
117
|
+
* @signature b.mail.rbl.create(opts)
|
|
118
|
+
* @since 0.9.33
|
|
119
|
+
* @status stable
|
|
120
|
+
* @related b.network.dns.resolver.create, b.safeDns.parseResponse
|
|
121
|
+
*
|
|
122
|
+
* Build an RBL query instance. Returns an object with
|
|
123
|
+
* `.query(ip, opts) → Promise<verdict>` and
|
|
124
|
+
* `.queryDomain(domain, opts) → Promise<verdict>` methods.
|
|
125
|
+
*
|
|
126
|
+
* @opts
|
|
127
|
+
* resolver: b.network.dns.resolver.create() instance, required
|
|
128
|
+
* blocklists: Array<string> — DNS zones (e.g. "bl.spamcop.net")
|
|
129
|
+
* allowlists: Array<string> — DNSWL zones (e.g. "list.dnswl.org")
|
|
130
|
+
* profile: "strict" | "balanced" | "permissive"
|
|
131
|
+
* posture: "hipaa" | "pci-dss" | "gdpr" | "soc2"
|
|
132
|
+
* withReason: boolean — default false; fetch TXT record per A hit
|
|
133
|
+
* audit: b.audit namespace
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* var rbl = b.mail.rbl.create({
|
|
137
|
+
* resolver: b.network.dns.resolver.create(),
|
|
138
|
+
* blocklists: ["zen.spamhaus.org", "bl.spamcop.net"],
|
|
139
|
+
* });
|
|
140
|
+
* var verdict = await rbl.query("192.0.2.99", { withReason: true });
|
|
141
|
+
* if (verdict.listed.length) refuseConnection(verdict.listed[0].reason);
|
|
142
|
+
*/
|
|
143
|
+
function create(opts) {
|
|
144
|
+
opts = opts || {};
|
|
145
|
+
if (!opts.resolver || typeof opts.resolver.query !== "function") {
|
|
146
|
+
throw new MailRblError("mail-rbl/bad-resolver",
|
|
147
|
+
"create: opts.resolver must be a b.network.dns.resolver.create() instance");
|
|
148
|
+
}
|
|
149
|
+
var profile = opts.profile || (opts.posture && COMPLIANCE_POSTURES[opts.posture]) || DEFAULT_PROFILE;
|
|
150
|
+
if (!PROFILES[profile]) {
|
|
151
|
+
throw new MailRblError("mail-rbl/bad-profile",
|
|
152
|
+
"create: unknown profile '" + profile + "'");
|
|
153
|
+
}
|
|
154
|
+
var caps = PROFILES[profile];
|
|
155
|
+
var blocklists = Array.isArray(opts.blocklists) ? opts.blocklists.slice() : [];
|
|
156
|
+
var allowlists = Array.isArray(opts.allowlists) ? opts.allowlists.slice() : [];
|
|
157
|
+
var withReason = opts.withReason === true;
|
|
158
|
+
var auditImpl = opts.audit || audit();
|
|
159
|
+
if (blocklists.length + allowlists.length === 0) {
|
|
160
|
+
throw new MailRblError("mail-rbl/no-lists",
|
|
161
|
+
"create: must configure at least one blocklist or allowlist");
|
|
162
|
+
}
|
|
163
|
+
if (blocklists.length + allowlists.length > caps.maxListsPerQuery) {
|
|
164
|
+
throw new MailRblError("mail-rbl/too-many-lists",
|
|
165
|
+
"create: " + (blocklists.length + allowlists.length) +
|
|
166
|
+
" lists configured; profile cap is " + caps.maxListsPerQuery);
|
|
167
|
+
}
|
|
168
|
+
_validateZoneNames(blocklists.concat(allowlists));
|
|
169
|
+
|
|
170
|
+
async function query(ip, qopts) {
|
|
171
|
+
qopts = qopts || {};
|
|
172
|
+
if (typeof ip !== "string" || ip.length === 0) {
|
|
173
|
+
throw new MailRblError("mail-rbl/bad-input",
|
|
174
|
+
"query: ip must be a non-empty string");
|
|
175
|
+
}
|
|
176
|
+
var reverse = reverseIp(ip);
|
|
177
|
+
return _walkLists(reverse, qopts);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function queryDomain(domain, qopts) {
|
|
181
|
+
qopts = qopts || {};
|
|
182
|
+
if (typeof domain !== "string" || domain.length === 0) {
|
|
183
|
+
throw new MailRblError("mail-rbl/bad-input",
|
|
184
|
+
"queryDomain: domain must be a non-empty string");
|
|
185
|
+
}
|
|
186
|
+
if (domain.indexOf(".") === -1) {
|
|
187
|
+
throw new MailRblError("mail-rbl/bad-input",
|
|
188
|
+
"queryDomain: domain must contain at least one label separator");
|
|
189
|
+
}
|
|
190
|
+
return _walkLists(domain, qopts);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function _walkLists(prefix, qopts) {
|
|
194
|
+
var perQueryReason = qopts.withReason !== undefined ? qopts.withReason === true : withReason;
|
|
195
|
+
var allLists = blocklists.map(function (z) { return { zone: z, kind: "block" }; })
|
|
196
|
+
.concat(allowlists.map(function (z) { return { zone: z, kind: "allow" }; }));
|
|
197
|
+
|
|
198
|
+
var verdict = { listed: [], allowed: [], neutral: [], errors: [] };
|
|
199
|
+
var inFlight = 0;
|
|
200
|
+
var idx = 0;
|
|
201
|
+
|
|
202
|
+
return new Promise(function (resolve) {
|
|
203
|
+
function _emit() {
|
|
204
|
+
// Schedule up to maxConcurrent at any time.
|
|
205
|
+
while (inFlight < caps.maxConcurrent && idx < allLists.length) {
|
|
206
|
+
var entry = allLists[idx];
|
|
207
|
+
idx += 1;
|
|
208
|
+
inFlight += 1;
|
|
209
|
+
_checkList(prefix, entry, perQueryReason, caps).then(function (rv) {
|
|
210
|
+
inFlight -= 1;
|
|
211
|
+
if (rv.error) {
|
|
212
|
+
verdict.errors.push({ list: rv.list, message: rv.error });
|
|
213
|
+
} else if (rv.listed) {
|
|
214
|
+
if (rv.kind === "allow") verdict.allowed.push(rv);
|
|
215
|
+
else verdict.listed.push(rv);
|
|
216
|
+
} else {
|
|
217
|
+
verdict.neutral.push({ list: rv.list });
|
|
218
|
+
}
|
|
219
|
+
if (idx >= allLists.length && inFlight === 0) resolve(verdict);
|
|
220
|
+
else _emit();
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
if (idx >= allLists.length && inFlight === 0) resolve(verdict);
|
|
224
|
+
}
|
|
225
|
+
_emit();
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
query: query,
|
|
231
|
+
queryDomain: queryDomain,
|
|
232
|
+
blocklists: blocklists,
|
|
233
|
+
allowlists: allowlists,
|
|
234
|
+
profile: profile,
|
|
235
|
+
MailRblError: MailRblError,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
async function _checkList(prefix, entry, perQueryReason, capsArg) {
|
|
239
|
+
var name = prefix + "." + entry.zone;
|
|
240
|
+
var rv = { list: entry.zone, kind: entry.kind, listed: false };
|
|
241
|
+
try {
|
|
242
|
+
var aResp = await _withTimeout(opts.resolver.queryA(name), capsArg.perListTimeoutMs);
|
|
243
|
+
if (aResp && aResp.rrs && aResp.rrs.length > 0) {
|
|
244
|
+
rv.listed = true;
|
|
245
|
+
rv.returnCode = aResp.rrs[0].decoded;
|
|
246
|
+
if (perQueryReason) {
|
|
247
|
+
try {
|
|
248
|
+
var txtResp = await _withTimeout(opts.resolver.queryTxt(name), capsArg.perListTimeoutMs);
|
|
249
|
+
if (txtResp && txtResp.rrs && txtResp.rrs.length > 0) {
|
|
250
|
+
rv.reason = (txtResp.rrs[0].decoded || []).join("");
|
|
251
|
+
}
|
|
252
|
+
} catch (_e) { /* TXT failure is non-fatal; A bit already set the verdict */ }
|
|
253
|
+
}
|
|
254
|
+
_emitAudit(auditImpl, "mail.rbl." + entry.kind + "_listed", {
|
|
255
|
+
list: entry.zone, returnCode: rv.returnCode,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
} catch (e) {
|
|
259
|
+
// NXDOMAIN is the expected "not listed" response, not an error
|
|
260
|
+
// condition. RFC 5782 §2.1.1 — absence of any A record means
|
|
261
|
+
// "not in list". Resolver surfaces this as resolver/nxdomain-or-
|
|
262
|
+
// error which we treat as the neutral verdict.
|
|
263
|
+
if (e && e.code === "resolver/nxdomain-or-error") {
|
|
264
|
+
// Neutral — not listed; not an error.
|
|
265
|
+
return rv;
|
|
266
|
+
}
|
|
267
|
+
rv.error = (e && e.message) || String(e);
|
|
268
|
+
}
|
|
269
|
+
return rv;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* @primitive b.mail.rbl.reverseIp
|
|
275
|
+
* @signature b.mail.rbl.reverseIp(ip)
|
|
276
|
+
* @since 0.9.33
|
|
277
|
+
* @status stable
|
|
278
|
+
*
|
|
279
|
+
* Build the reverse-DNS query name for an IPv4 or IPv6 address per
|
|
280
|
+
* RFC 5782 §2.1 / §2.4. Pure-functional helper exposed for operator
|
|
281
|
+
* tests and the `b.mail.dnsbl` extension primitive.
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* b.mail.rbl.reverseIp("192.0.2.99"); // → "99.2.0.192"
|
|
285
|
+
* b.mail.rbl.reverseIp("2001:db8::1"); // → "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2"
|
|
286
|
+
*/
|
|
287
|
+
function reverseIp(ip) {
|
|
288
|
+
if (typeof ip !== "string" || ip.length === 0) {
|
|
289
|
+
throw new MailRblError("mail-rbl/bad-input",
|
|
290
|
+
"reverseIp: ip must be a non-empty string");
|
|
291
|
+
}
|
|
292
|
+
// IPv4 first.
|
|
293
|
+
if (IPV4_RE.test(ip)) {
|
|
294
|
+
return ip.split(".").reverse().join(".");
|
|
295
|
+
}
|
|
296
|
+
// IPv6 — accept canonical / compressed forms. Expand and nibble-reverse.
|
|
297
|
+
if (ip.length > IPV6_MAX_LEN || !IPV6_HEX_RE.test(ip)) {
|
|
298
|
+
throw new MailRblError("mail-rbl/bad-input",
|
|
299
|
+
"reverseIp: '" + ip + "' is not a valid IPv4 or IPv6 address");
|
|
300
|
+
}
|
|
301
|
+
var expanded = ipUtils.expandIpv6Hex(ip);
|
|
302
|
+
if (!expanded) {
|
|
303
|
+
throw new MailRblError("mail-rbl/bad-input",
|
|
304
|
+
"reverseIp: '" + ip + "' is not a parseable IPv6 address");
|
|
305
|
+
}
|
|
306
|
+
// expanded is 32 hex chars (128 bits / 4 = 32 nibbles); reverse + dot-join.
|
|
307
|
+
var rev = [];
|
|
308
|
+
for (var i = expanded.length - 1; i >= 0; i -= 1) rev.push(expanded[i]);
|
|
309
|
+
return rev.join(".");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* @primitive b.mail.rbl.compliancePosture
|
|
314
|
+
* @signature b.mail.rbl.compliancePosture(posture)
|
|
315
|
+
* @since 0.9.33
|
|
316
|
+
* @status stable
|
|
317
|
+
*
|
|
318
|
+
* Return the effective profile name for a compliance posture, or
|
|
319
|
+
* `null` for unknown posture names.
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* b.mail.rbl.compliancePosture("hipaa"); // → "strict"
|
|
323
|
+
*/
|
|
324
|
+
function compliancePosture(posture) {
|
|
325
|
+
return COMPLIANCE_POSTURES[posture] || null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function _validateZoneNames(zones) {
|
|
329
|
+
for (var i = 0; i < zones.length; i += 1) {
|
|
330
|
+
var z = zones[i];
|
|
331
|
+
if (typeof z !== "string" || z.length === 0 || z.length > 253) { // allow:raw-byte-literal — RFC 1035 §2.3.4 total name cap
|
|
332
|
+
throw new MailRblError("mail-rbl/bad-zone",
|
|
333
|
+
"list zone '" + z + "' must be a non-empty string under 253 bytes");
|
|
334
|
+
}
|
|
335
|
+
if (z.indexOf("..") !== -1 || z.charAt(0) === "." || z.charAt(z.length - 1) === ".") {
|
|
336
|
+
throw new MailRblError("mail-rbl/bad-zone",
|
|
337
|
+
"list zone '" + z + "' has malformed dots");
|
|
338
|
+
}
|
|
339
|
+
// ASCII label-shape — DNSBL zones are always ASCII (IDN punycode
|
|
340
|
+
// if non-ASCII upstream).
|
|
341
|
+
for (var c = 0; c < z.length; c += 1) {
|
|
342
|
+
var cc = z.charCodeAt(c);
|
|
343
|
+
if (cc < 0x20 || cc === 0x7f || cc > 0x7e) { // allow:raw-byte-literal — RFC 1035 ASCII zone-name shape
|
|
344
|
+
throw new MailRblError("mail-rbl/bad-zone",
|
|
345
|
+
"list zone '" + z + "' contains non-ASCII or control chars");
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function _withTimeout(promise, timeoutMs) {
|
|
352
|
+
return new Promise(function (resolve, reject) {
|
|
353
|
+
var done = false;
|
|
354
|
+
var t = setTimeout(function () {
|
|
355
|
+
if (done) return;
|
|
356
|
+
done = true;
|
|
357
|
+
reject(new MailRblError("mail-rbl/timeout",
|
|
358
|
+
"list query exceeded " + timeoutMs + "ms"));
|
|
359
|
+
}, timeoutMs);
|
|
360
|
+
promise.then(function (v) {
|
|
361
|
+
if (done) return;
|
|
362
|
+
done = true;
|
|
363
|
+
clearTimeout(t);
|
|
364
|
+
resolve(v);
|
|
365
|
+
}, function (e) {
|
|
366
|
+
if (done) return;
|
|
367
|
+
done = true;
|
|
368
|
+
clearTimeout(t);
|
|
369
|
+
reject(e);
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function _emitAudit(auditImpl, action, metadata) {
|
|
375
|
+
try {
|
|
376
|
+
if (auditImpl && typeof auditImpl.safeEmit === "function") {
|
|
377
|
+
auditImpl.safeEmit({ action: action, outcome: "success", metadata: metadata });
|
|
378
|
+
}
|
|
379
|
+
} catch (_e) { /* drop-silent — audit failures don't break query path */ }
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
void DEFAULT_TIMEOUT_MS; // referenced indirectly via PROFILES.strict.perListTimeoutMs
|
|
383
|
+
void DEFAULT_CONCURRENCY; // referenced indirectly via PROFILES.strict.maxConcurrent
|
|
384
|
+
|
|
385
|
+
module.exports = {
|
|
386
|
+
create: create,
|
|
387
|
+
reverseIp: reverseIp,
|
|
388
|
+
compliancePosture: compliancePosture,
|
|
389
|
+
PROFILES: PROFILES,
|
|
390
|
+
COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
|
|
391
|
+
MailRblError: MailRblError,
|
|
392
|
+
};
|
package/lib/mail.js
CHANGED
|
@@ -65,6 +65,7 @@ var guardEmail = lazyRequire(function () { return require("./guard-email"); });
|
|
|
65
65
|
var guardFilename = lazyRequire(function () { return require("./guard-filename"); });
|
|
66
66
|
var fileType = lazyRequire(function () { return require("./file-type"); });
|
|
67
67
|
var dkim = require("./mail-dkim");
|
|
68
|
+
var ipUtils = require("./ip-utils");
|
|
68
69
|
var mailAuth = require("./mail-auth");
|
|
69
70
|
var mailBimi = require("./mail-bimi");
|
|
70
71
|
var mailUnsubscribe = require("./mail-unsubscribe");
|
|
@@ -785,7 +786,7 @@ function smtpTransport(opts) {
|
|
|
785
786
|
var host = opts.host;
|
|
786
787
|
var servername = opts.servername;
|
|
787
788
|
if (servername === undefined) {
|
|
788
|
-
servername = (
|
|
789
|
+
servername = (ipUtils.isIPv4Shape(host) || (host && host.indexOf(":") !== -1))
|
|
789
790
|
? undefined : host;
|
|
790
791
|
}
|
|
791
792
|
|