@blamejs/core 0.18.53 → 0.18.55
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 +228 -0
- package/NOTICE +1 -1
- package/README.md +5 -5
- package/lib/agent-audit.js +27 -2
- package/lib/ai-adverse-decision.js +18 -2
- package/lib/audit-sign.js +24 -5
- package/lib/auth/passkey.js +4 -1
- package/lib/codepoint-class.js +72 -0
- package/lib/cookies.js +7 -10
- package/lib/credential-hash.js +8 -1
- package/lib/crypto.js +7 -5
- package/lib/db-file-lifecycle.js +14 -3
- package/lib/db.js +505 -49
- package/lib/guard-auth.js +34 -11
- package/lib/guard-filename.js +41 -33
- package/lib/guard-html.js +10 -2
- package/lib/guard-list-unsubscribe.js +6 -1
- package/lib/guard-managesieve-command.js +73 -12
- package/lib/guard-regex.js +3 -5
- package/lib/guard-smtp-command.js +20 -4
- package/lib/guard-svg.js +6 -1
- package/lib/guard-yaml.js +60 -15
- package/lib/http-client.js +17 -3
- package/lib/mail-agent.js +29 -13
- package/lib/mail-arc-sign.js +40 -7
- package/lib/mail-auth.js +134 -22
- package/lib/mail-crypto-pgp.js +1 -1
- package/lib/mail-dkim.js +80 -11
- package/lib/mail-helo.js +10 -0
- package/lib/mail-rbl.js +10 -3
- package/lib/mail-send-deliver.js +151 -32
- package/lib/mail-server-imap.js +186 -89
- package/lib/mail-server-jmap.js +31 -4
- package/lib/mail-server-managesieve.js +198 -42
- package/lib/mail-server-mx.js +191 -38
- package/lib/mail-server-net.js +281 -1
- package/lib/mail-server-pop3.js +89 -41
- package/lib/mail-server-rate-limit.js +104 -6
- package/lib/mail-server-submission.js +183 -35
- package/lib/mail-server-tls.js +48 -3
- package/lib/mail-store.js +33 -11
- package/lib/mail.js +355 -17
- package/lib/mcp.js +11 -3
- package/lib/middleware/bearer-auth.js +6 -1
- package/lib/middleware/fetch-metadata.js +5 -1
- package/lib/middleware/headers.js +7 -10
- package/lib/middleware/require-mtls.js +8 -1
- package/lib/network-dns-resolver.js +71 -8
- package/lib/network-dns.js +26 -0
- package/lib/network-smtp-policy.js +42 -10
- package/lib/network-tls.js +18 -0
- package/lib/redact.js +13 -3
- package/lib/retention.js +22 -2
- package/lib/safe-mount-info.js +39 -6
- package/lib/safe-smtp.js +96 -1
- package/lib/safe-url.js +8 -2
- package/lib/self-update.js +4 -1
- package/lib/vendor/MANIFEST.json +12 -12
- package/lib/vendor/blamejs-pki.cjs +672 -75
- package/lib/watcher.js +31 -6
- package/lib/ws-client.js +17 -2
- package/lib/yaml-lex.js +55 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/mail-helo.js
CHANGED
|
@@ -370,6 +370,16 @@ async function _runFcrdns(ip, resolver) {
|
|
|
370
370
|
if (!rev) {
|
|
371
371
|
return result; // unparseable IP — caller already rejected
|
|
372
372
|
}
|
|
373
|
+
// The resolver must be able to answer the question before the answer means
|
|
374
|
+
// anything. A missing method is not a DNS condition, and letting it fall into
|
|
375
|
+
// the catch below turned a broken call into a clean "no reverse name" for
|
|
376
|
+
// every address the check ever ran on — `passed` stayed false, which looks
|
|
377
|
+
// exactly like an address with no PTR record.
|
|
378
|
+
if (typeof resolver.queryPtr !== "function") {
|
|
379
|
+
throw new MailHeloError("mail-helo/resolver-missing-queryptr",
|
|
380
|
+
"fcrdns requires resolver.queryPtr(name); the supplied resolver has none, " +
|
|
381
|
+
"so reverse DNS cannot be checked and a pass cannot be claimed");
|
|
382
|
+
}
|
|
373
383
|
try {
|
|
374
384
|
var ptr = await resolver.queryPtr(rev);
|
|
375
385
|
if (ptr && ptr.rrs) {
|
package/lib/mail-rbl.js
CHANGED
|
@@ -253,9 +253,16 @@ function create(opts) {
|
|
|
253
253
|
} catch (e) {
|
|
254
254
|
// NXDOMAIN is the expected "not listed" response, not an error
|
|
255
255
|
// condition. RFC 5782 §2.1.1 — absence of any A record means
|
|
256
|
-
// "not in list".
|
|
257
|
-
//
|
|
258
|
-
|
|
256
|
+
// "not in list".
|
|
257
|
+
//
|
|
258
|
+
// ONLY NXDOMAIN. The resolver used to report every non-zero RCODE under
|
|
259
|
+
// one code, so this branch also caught SERVFAIL and REFUSED and returned
|
|
260
|
+
// the same clean verdict — a blocklist lookup that failed read exactly
|
|
261
|
+
// like a host that is not on the list. Anyone able to break the query
|
|
262
|
+
// could therefore clear themselves, which is the wrong direction for a
|
|
263
|
+
// blocklist to fail in. A failure now falls through to `rv.error` below,
|
|
264
|
+
// where the caller can see it.
|
|
265
|
+
if (e && e.code === "resolver/nxdomain") {
|
|
259
266
|
// Neutral — not listed; not an error.
|
|
260
267
|
return rv;
|
|
261
268
|
}
|
package/lib/mail-send-deliver.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Turnkey outbound SMTP composer. Wraps the discovery chain
|
|
12
12
|
* (MX-lookup → MTA-STS-fetch + MX-allowlist match → DANE TLSA query
|
|
13
13
|
* → REQUIRETLS handshake hint) around the existing per-host
|
|
14
|
-
* `b.mail.
|
|
14
|
+
* `b.mail.transports.smtp` wire-layer, plus deferred-retry scheduling
|
|
15
15
|
* for transient failures and RFC 3464 DSN generation for permanent
|
|
16
16
|
* ones.
|
|
17
17
|
*
|
|
@@ -32,14 +32,18 @@
|
|
|
32
32
|
* requireTls: true,
|
|
33
33
|
* });
|
|
34
34
|
* // → { delivered: [{ recipient, mxHost, tlsProtocol, ... }],
|
|
35
|
-
* // deferred: [{ recipient, reason, retryAfterMs }],
|
|
35
|
+
* // deferred: [{ recipient, mxHost, reason, retryAfterMs }],
|
|
36
36
|
* // failed: [{ recipient, reason, dsnSent }] }
|
|
37
|
+
* // deferred.mxHost is the receiver that refused, or null when the
|
|
38
|
+
* // deferral happened before any host was reached (an MX lookup that
|
|
39
|
+
* // failed). A queue view without it cannot say which peer to chase.
|
|
37
40
|
*
|
|
38
41
|
* Composes:
|
|
39
42
|
* - `b.network.smtp.policy.mtaSts.fetch` + `.matchMx` → RFC 8461 enforcement
|
|
40
|
-
* - `b.network.smtp.policy.dane.tlsa`
|
|
43
|
+
* - `b.network.smtp.policy.dane.tlsa` + `.verifyChain` → RFC 7672 TLSA query
|
|
44
|
+
* and peer authentication
|
|
41
45
|
* - `b.network.dns.resolver` (operator-supplied) → caching + DoH posture
|
|
42
|
-
* - `b.mail.
|
|
46
|
+
* - `b.mail.transports.smtp` → SMTP wire layer
|
|
43
47
|
* - `b.mail.requireTls` → RFC 8689 REQUIRETLS
|
|
44
48
|
* - `b.mailBounce`-style RFC 3464 DSN generation → permanent-failure
|
|
45
49
|
* report-mail
|
|
@@ -104,7 +108,16 @@ function _classifySmtpOutcome(err, response) {
|
|
|
104
108
|
if (err) {
|
|
105
109
|
var code = err.code || "";
|
|
106
110
|
if (/^(ECONNREFUSED|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|ENOTFOUND)$/.test(code)) return "transient";
|
|
107
|
-
|
|
111
|
+
// A DANE failure is about THIS HOST, not this recipient: the certificate
|
|
112
|
+
// did not match the records, or the records could not be used. The ordinary
|
|
113
|
+
// cause is a rollover part-way through DNS propagation, which resolves
|
|
114
|
+
// itself — and a DSN does not. Classified permanent it also skipped every
|
|
115
|
+
// remaining MX, so one host mid-rollover bounced mail the next host would
|
|
116
|
+
// have taken. RFC 7672 §2.2: try the other hosts, defer if none
|
|
117
|
+
// authenticates. Matched before the policy class below, which keeps its
|
|
118
|
+
// permanent verdict.
|
|
119
|
+
if (/\bdane\b/i.test((err.code || "") + " " + (err.message || ""))) return "transient";
|
|
120
|
+
if (/mta-sts|tls-policy|requiretls/i.test((err.code || "") + " " + (err.message || ""))) return "permanent";
|
|
108
121
|
}
|
|
109
122
|
return "transient";
|
|
110
123
|
}
|
|
@@ -202,8 +215,22 @@ async function _resolveMx(domain, resolver, timeoutMs) {
|
|
|
202
215
|
// .create()` wraps DoH and returns `{ rrs: [{ exchange, priority }],
|
|
203
216
|
// ttl, ... }` — the wrapper carries TTL + provenance metadata.
|
|
204
217
|
// Accept both shapes; refuse anything else.
|
|
218
|
+
//
|
|
219
|
+
// The resolver's records are NOT flat. b.network.dns decodes an MX RR into
|
|
220
|
+
// `{ name, type, typeName, ttl, decoded: { preference, exchange } }`, so
|
|
221
|
+
// reading `.exchange` off the record itself yields undefined — every MX
|
|
222
|
+
// host became undefined and the first thing to touch one failed. The
|
|
223
|
+
// comment above this normalisation asserted the flat shape, and no test
|
|
224
|
+
// caught the disagreement because every one of them passes a hand-written
|
|
225
|
+
// resolver returning exactly what the comment claimed. Only a query against
|
|
226
|
+
// a real DNS response shows the difference.
|
|
205
227
|
if (mxs && !Array.isArray(mxs) && Array.isArray(mxs.rrs)) {
|
|
206
|
-
mxs = mxs.rrs
|
|
228
|
+
mxs = mxs.rrs.map(function (rr) {
|
|
229
|
+
if (rr && rr.decoded && typeof rr.decoded.exchange === "string") {
|
|
230
|
+
return { exchange: rr.decoded.exchange, priority: rr.decoded.preference };
|
|
231
|
+
}
|
|
232
|
+
return rr; // already flat
|
|
233
|
+
});
|
|
207
234
|
}
|
|
208
235
|
if (!Array.isArray(mxs) || mxs.length === 0) {
|
|
209
236
|
throw new DeliverError("deliver/no-mx",
|
|
@@ -277,15 +304,26 @@ async function _applyMtaStsPolicy(domain, mxs, policyMode, auditEmit) {
|
|
|
277
304
|
return filtered;
|
|
278
305
|
}
|
|
279
306
|
|
|
280
|
-
// Apply DANE TLSA query per RFC 7672. Returns array of TLSA records
|
|
281
|
-
//
|
|
282
|
-
// The
|
|
283
|
-
// the
|
|
284
|
-
//
|
|
285
|
-
async function _fetchDaneTlsa(mxHost, port, daneMode, auditEmit) {
|
|
307
|
+
// Apply DANE TLSA query per RFC 7672. Returns the array of TLSA records for
|
|
308
|
+
// the MX host, or null when DANE is off, the records are unusable, or the peer
|
|
309
|
+
// publishes none. The records are handed to the transport, which authenticates
|
|
310
|
+
// the peer's certificate chain against them via dane.verifyChain — the lookup
|
|
311
|
+
// on its own is discovery, and discovery is not what "enforce" promises.
|
|
312
|
+
async function _fetchDaneTlsa(mxHost, port, daneMode, dnssecValidated, resolver, auditEmit) {
|
|
286
313
|
if (daneMode === "off") return null;
|
|
314
|
+
// RFC 7672 §1.3: records that were not DNSSEC-validated MUST NOT be used.
|
|
315
|
+
// Under opportunistic with no assertion they are therefore unusable before
|
|
316
|
+
// they are fetched, so asking for them buys nothing and costs a DNS round
|
|
317
|
+
// trip per MX plus a warn on every delivery. `enforce` cannot reach here
|
|
318
|
+
// without the assertion — create() refuses that combination outright.
|
|
319
|
+
if (!dnssecValidated) return null;
|
|
287
320
|
try {
|
|
288
|
-
|
|
321
|
+
// The operator's resolver goes with the assertion: `dnssecValidated` is a
|
|
322
|
+
// statement about THEIR resolver, so the records have to come from it. A
|
|
323
|
+
// lookup that quietly used node:dns instead would let a non-validating
|
|
324
|
+
// system resolver supply spoofed TLSA data under a validated banner.
|
|
325
|
+
var tlsa = await smtpPolicy().dane.tlsa(mxHost, port || DEFAULT_PORT_SMTP,
|
|
326
|
+
{ dnssecValidated: true, resolver: resolver || undefined });
|
|
289
327
|
return tlsa && tlsa.length > 0 ? tlsa : null;
|
|
290
328
|
} catch (e) {
|
|
291
329
|
auditEmit("mail.send.deliver.dane.skip", "warn",
|
|
@@ -304,19 +342,25 @@ async function _fetchDaneTlsa(mxHost, port, daneMode, auditEmit) {
|
|
|
304
342
|
// outbound relay) can wrap the wire-layer surface without monkey-
|
|
305
343
|
// patching the framework's mail module.
|
|
306
344
|
async function _tryHost(envelope, mxHost, hostnameLocal, opts) {
|
|
307
|
-
|
|
345
|
+
// `transports.smtp`, which is the name `lib/mail.js` actually exports. It was
|
|
346
|
+
// reached for as `smtpTransport` — a name nothing defines — so the default was
|
|
347
|
+
// `undefined`, calling it threw, the throw was classified as a transient peer
|
|
348
|
+
// problem, and an operator who did not pass `transportFactory` got every
|
|
349
|
+
// recipient deferred 4.4.4 forever with no socket ever opened. The option is
|
|
350
|
+
// documented as an override, so the default is the path most callers take.
|
|
351
|
+
var factory = opts.transportFactory || mailModule().transports.smtp;
|
|
308
352
|
var transport = factory({
|
|
309
353
|
host: mxHost,
|
|
310
354
|
port: opts.port || DEFAULT_PORT_SMTP,
|
|
311
355
|
ehloName: hostnameLocal,
|
|
312
356
|
timeoutMs: opts.perHostTimeoutMs || DEFAULT_PER_HOST_TIMEOUT_MS,
|
|
313
357
|
requireTls: envelope.requireTls === true,
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
|
|
358
|
+
// The TLSA records fetched for THIS MX host. The transport matches the
|
|
359
|
+
// peer's certificate chain against them once the handshake completes and
|
|
360
|
+
// refuses the send on a mismatch (RFC 7672 §2.2). Absent when the peer
|
|
361
|
+
// publishes none, or when DANE is off — the transport then behaves as it
|
|
362
|
+
// always did.
|
|
363
|
+
dane: envelope.tlsa || undefined,
|
|
320
364
|
});
|
|
321
365
|
return transport.send({
|
|
322
366
|
from: envelope.from,
|
|
@@ -356,14 +400,21 @@ async function _deliverOne(envelope, recipient, ctx) {
|
|
|
356
400
|
}
|
|
357
401
|
var lastErr = null;
|
|
358
402
|
var lastResponse = null;
|
|
403
|
+
// The host the deferral is about. Null until one is tried, so a recipient
|
|
404
|
+
// deferred before any peer was reached reports "no host" rather than
|
|
405
|
+
// omitting the field — a consumer can then tell that apart from a host that
|
|
406
|
+
// simply was not recorded.
|
|
407
|
+
var lastMxHost = null;
|
|
359
408
|
for (var i = 0; i < mxs.length; i += 1) {
|
|
360
409
|
var mx = mxs[i];
|
|
361
|
-
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
|
|
410
|
+
lastMxHost = mx.exchange;
|
|
411
|
+
// DANE per-MX lookup. The records go to the transport, which matches the
|
|
412
|
+
// peer's certificate chain against them during the handshake.
|
|
413
|
+
var mxTlsa = null;
|
|
365
414
|
try {
|
|
366
|
-
await _fetchDaneTlsa(mx.exchange, ctx.port, ctx.policy.dane,
|
|
415
|
+
mxTlsa = await _fetchDaneTlsa(mx.exchange, ctx.port, ctx.policy.dane,
|
|
416
|
+
ctx.policy.dnssecValidated, ctx.resolver,
|
|
417
|
+
ctx.auditEmit);
|
|
367
418
|
} catch (daneErr) {
|
|
368
419
|
// DANE "enforce": a TLSA lookup failure means this MX host cannot
|
|
369
420
|
// be used for authenticated delivery (RFC 7672 §2.2). Fail this
|
|
@@ -385,6 +436,7 @@ async function _deliverOne(envelope, recipient, ctx) {
|
|
|
385
436
|
recipient: recipient,
|
|
386
437
|
rfc822: envelope.rfc822,
|
|
387
438
|
requireTls: envelope.requireTls,
|
|
439
|
+
tlsa: mxTlsa,
|
|
388
440
|
}, mx.exchange, ctx.hostname, ctx);
|
|
389
441
|
ctx.auditEmit("mail.send.deliver.delivered", "success", {
|
|
390
442
|
recipient: recipient, mxHost: mx.exchange, mxPriority: mx.priority,
|
|
@@ -411,8 +463,13 @@ async function _deliverOne(envelope, recipient, ctx) {
|
|
|
411
463
|
}
|
|
412
464
|
}
|
|
413
465
|
// All MX hosts returned transient — overall outcome is transient
|
|
414
|
-
// (defer + retry).
|
|
466
|
+
// (defer + retry). `mxHost` names the last host tried, because a queue an
|
|
467
|
+
// operator reads on a bad day has to answer WHICH receiver refused: a domain
|
|
468
|
+
// with several MX hosts defers on one of them, and that decides whether to
|
|
469
|
+
// wait, to contact that receiver, or to look at one's own transport security
|
|
470
|
+
// against that host.
|
|
415
471
|
return { recipient: recipient, outcome: "transient",
|
|
472
|
+
mxHost: lastMxHost,
|
|
416
473
|
reason: (lastErr && lastErr.message) || "all MX hosts failed transiently",
|
|
417
474
|
reasonCode: (lastResponse && lastResponse.code) || "4.4.4" };
|
|
418
475
|
}
|
|
@@ -428,7 +485,7 @@ async function _deliverOne(envelope, recipient, ctx) {
|
|
|
428
485
|
* Build a turnkey delivery handle. Returns a `deliver(envelope)`
|
|
429
486
|
* function that takes a single multi-recipient envelope, resolves
|
|
430
487
|
* MX records per recipient domain, applies the operator's configured
|
|
431
|
-
* MTA-STS / DANE policy, attempts delivery via `b.mail.
|
|
488
|
+
* MTA-STS / DANE policy, attempts delivery via `b.mail.transports.smtp`,
|
|
432
489
|
* and returns a per-recipient outcome split into `delivered` /
|
|
433
490
|
* `deferred` / `failed` arrays.
|
|
434
491
|
*
|
|
@@ -437,6 +494,23 @@ async function _deliverOne(envelope, recipient, ctx) {
|
|
|
437
494
|
* after the budget elapses. The primitive does not own a background
|
|
438
495
|
* scheduler — operator job-runner owns the retry lifecycle.
|
|
439
496
|
*
|
|
497
|
+
* DANE (RFC 7672) authenticates the peer, not just its DNS. When the peer
|
|
498
|
+
* publishes TLSA records, they are fetched and the certificate chain it
|
|
499
|
+
* presents during the handshake is matched against them; a peer whose chain
|
|
500
|
+
* matches none of its own records is refused and the next MX is tried.
|
|
501
|
+
*
|
|
502
|
+
* The TLSA lookup goes through `opts.resolver` when one is supplied, because
|
|
503
|
+
* the DNSSEC assertion below is a statement about THAT resolver. A resolver
|
|
504
|
+
* that cannot answer TLSA is refused rather than bypassed.
|
|
505
|
+
*
|
|
506
|
+
* Because RFC 7672 §1.3 forbids using records that were not DNSSEC-validated
|
|
507
|
+
* and node:dns does not expose the AD bit, only the operator can say whether
|
|
508
|
+
* their resolver validates. `policy.dnssecValidated: true` is that statement,
|
|
509
|
+
* and `dane: "enforce"` requires it: without it every peer that publishes TLSA
|
|
510
|
+
* would be refused while every peer that publishes none was delivered to.
|
|
511
|
+
* Under `"opportunistic"` the records are unusable without it, so none are
|
|
512
|
+
* fetched.
|
|
513
|
+
*
|
|
440
514
|
* Failed recipients trigger DSN composition: a RFC 3464 multipart/
|
|
441
515
|
* report message is built per failed recipient and handed to the
|
|
442
516
|
* operator-supplied `dsn.onPermanentFailure(envelope, recipientResult,
|
|
@@ -452,6 +526,7 @@ async function _deliverOne(envelope, recipient, ctx) {
|
|
|
452
526
|
* policy: {
|
|
453
527
|
* mtaSts: "enforce" | "testing" | "off", // default "enforce" — RFC 8461 posture
|
|
454
528
|
* dane: "opportunistic" | "enforce" | "off", // default "opportunistic" — RFC 7672
|
|
529
|
+
* dnssecValidated: boolean, // default false — assert the resolver DNSSEC-validates; required by dane "enforce"
|
|
455
530
|
* },
|
|
456
531
|
* retry: {
|
|
457
532
|
* maxAttempts: number, // default 5
|
|
@@ -521,7 +596,7 @@ function create(opts) {
|
|
|
521
596
|
var port = opts.port || DEFAULT_PORT_SMTP;
|
|
522
597
|
|
|
523
598
|
var policy = opts.policy || {};
|
|
524
|
-
validateOpts(policy, ["mtaSts", "dane"], "mail.send.deliver.create.policy");
|
|
599
|
+
validateOpts(policy, ["mtaSts", "dane", "dnssecValidated"], "mail.send.deliver.create.policy");
|
|
525
600
|
var policyMtaSts = policy.mtaSts || "enforce";
|
|
526
601
|
if (["enforce", "testing", "off"].indexOf(policyMtaSts) === -1) {
|
|
527
602
|
throw new DeliverError("deliver/bad-policy-mtaSts",
|
|
@@ -532,6 +607,25 @@ function create(opts) {
|
|
|
532
607
|
throw new DeliverError("deliver/bad-policy-dane",
|
|
533
608
|
"mail.send.deliver.create.policy.dane must be opportunistic|enforce|off");
|
|
534
609
|
}
|
|
610
|
+
// RFC 7672 §1.3 forbids using TLSA records that were not DNSSEC-validated,
|
|
611
|
+
// and node:dns does not surface the AD bit, so only the operator can say
|
|
612
|
+
// whether their resolver validates. Without that assertion `enforce` refuses
|
|
613
|
+
// every peer that publishes TLSA while delivering normally to every peer that
|
|
614
|
+
// publishes none — the sender's misconfiguration charged to the recipient who
|
|
615
|
+
// did the work. It is a property of this sender's resolver, so it is settled
|
|
616
|
+
// once at create() rather than surfacing per-peer as a delivery failure.
|
|
617
|
+
if (policy.dnssecValidated !== undefined && typeof policy.dnssecValidated !== "boolean") {
|
|
618
|
+
throw new DeliverError("deliver/bad-policy-dnssec",
|
|
619
|
+
"mail.send.deliver.create.policy.dnssecValidated must be a boolean when set");
|
|
620
|
+
}
|
|
621
|
+
var daneDnssecValidated = policy.dnssecValidated === true;
|
|
622
|
+
if (policyDane === "enforce" && !daneDnssecValidated) {
|
|
623
|
+
throw new DeliverError("deliver/dane-no-dnssec",
|
|
624
|
+
"policy.dane \"enforce\" requires policy.dnssecValidated: true — TLSA records " +
|
|
625
|
+
"must be DNSSEC-validated before use (RFC 7672 §1.3), and without that " +
|
|
626
|
+
"assertion enforce can only refuse the peers that publish them. Set it when " +
|
|
627
|
+
"the resolver validates DNSSEC; otherwise use \"opportunistic\" or \"off\"");
|
|
628
|
+
}
|
|
535
629
|
|
|
536
630
|
var retryOpts = opts.retry || {};
|
|
537
631
|
validateOpts(retryOpts, ["maxAttempts", "backoffMs"], "mail.send.deliver.create.retry");
|
|
@@ -578,8 +672,22 @@ function create(opts) {
|
|
|
578
672
|
throw new DeliverError("deliver/bad-envelope",
|
|
579
673
|
"deliver: envelope is required");
|
|
580
674
|
}
|
|
581
|
-
|
|
582
|
-
|
|
675
|
+
// The EMPTY string is a value here, not a missing one: RFC 5321 §4.5.5
|
|
676
|
+
// requires the null reverse path `MAIL FROM:<>` for a delivery status
|
|
677
|
+
// notification, and `b.mail.transports.smtp` already turns `""` into
|
|
678
|
+
// exactly that. Refusing it as "non-empty required" meant this composer
|
|
679
|
+
// could not send the one message shape the spec reserves a syntax for, and
|
|
680
|
+
// its own documented DSN example put a real address in the reverse path —
|
|
681
|
+
// so a bounce that failed would bounce back to the bounce's sender.
|
|
682
|
+
//
|
|
683
|
+
// The framework's other readers of an envelope sender already know this:
|
|
684
|
+
// `b.mail.spf.verify` treats an empty `mailFrom` as the null sender and
|
|
685
|
+
// falls back to the HELO identity, which is RFC 7208 §2.4 exactly.
|
|
686
|
+
if (typeof envelope.from !== "string") {
|
|
687
|
+
throw new DeliverError("deliver/bad-envelope-from",
|
|
688
|
+
"deliver.envelope.from must be a string — use \"\" for the null " +
|
|
689
|
+
"reverse path (MAIL FROM:<>) that RFC 5321 requires of a DSN");
|
|
690
|
+
}
|
|
583
691
|
if (!Array.isArray(envelope.to) || envelope.to.length === 0) {
|
|
584
692
|
throw new DeliverError("deliver/bad-envelope-to",
|
|
585
693
|
"deliver.envelope.to must be a non-empty array");
|
|
@@ -596,7 +704,8 @@ function create(opts) {
|
|
|
596
704
|
|
|
597
705
|
var ctx = {
|
|
598
706
|
resolver: opts.resolver || null,
|
|
599
|
-
policy: { mtaSts: policyMtaSts, dane: policyDane
|
|
707
|
+
policy: { mtaSts: policyMtaSts, dane: policyDane,
|
|
708
|
+
dnssecValidated: daneDnssecValidated },
|
|
600
709
|
hostname: opts.hostname,
|
|
601
710
|
port: port,
|
|
602
711
|
mxLookupTimeoutMs: mxLookupTimeoutMs,
|
|
@@ -638,6 +747,10 @@ function create(opts) {
|
|
|
638
747
|
var idx = Math.min(attempts - 1, backoffMs.length - 1);
|
|
639
748
|
deferred.push({
|
|
640
749
|
recipient: res.recipient,
|
|
750
|
+
// The receiver the deferral is about, or null when none was
|
|
751
|
+
// reached. A queue that records the reason and the retry time and
|
|
752
|
+
// not the peer cannot answer the first question an operator asks.
|
|
753
|
+
mxHost: res.mxHost === undefined ? null : res.mxHost,
|
|
641
754
|
reason: res.reason,
|
|
642
755
|
reasonCode: res.reasonCode,
|
|
643
756
|
attempt: attempts,
|
|
@@ -648,7 +761,13 @@ function create(opts) {
|
|
|
648
761
|
}
|
|
649
762
|
// permanent (either direct or transient-converted-to-permanent)
|
|
650
763
|
var dsnSent = false;
|
|
651
|
-
|
|
764
|
+
// A message sent with the NULL reverse path gets no DSN. RFC 5321 §4.5.5
|
|
765
|
+
// is explicit, and the reason is the whole point of the null sender: the
|
|
766
|
+
// DSN's `To:` is the original `from`, so bouncing a bounce addresses it
|
|
767
|
+
// to nobody and, where the peer is less careful, to itself. This is the
|
|
768
|
+
// loop the empty reverse path exists to stop, and it is reachable the
|
|
769
|
+
// moment `""` is accepted above.
|
|
770
|
+
if (dsnOpts && envelope.from !== "") {
|
|
652
771
|
try {
|
|
653
772
|
var dsnMessage = _buildDsnMessage({
|
|
654
773
|
dsnFrom: dsnOpts.from,
|