@blamejs/blamejs-shop 0.1.37 → 0.2.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.
Files changed (73) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/lib/admin.js +205 -42
  3. package/lib/asset-manifest.json +5 -5
  4. package/lib/storefront.js +428 -51
  5. package/lib/vendor/MANIFEST.json +2 -2
  6. package/lib/vendor/blamejs/CHANGELOG.md +12 -0
  7. package/lib/vendor/blamejs/README.md +2 -2
  8. package/lib/vendor/blamejs/SECURITY.md +1 -0
  9. package/lib/vendor/blamejs/api-snapshot.json +2 -2
  10. package/lib/vendor/blamejs/lib/ai-disclosure.js +2 -3
  11. package/lib/vendor/blamejs/lib/archive-gz.js +5 -3
  12. package/lib/vendor/blamejs/lib/archive-read.js +101 -35
  13. package/lib/vendor/blamejs/lib/archive-tar-read.js +86 -31
  14. package/lib/vendor/blamejs/lib/archive-tar.js +2 -3
  15. package/lib/vendor/blamejs/lib/auth/fal.js +12 -0
  16. package/lib/vendor/blamejs/lib/auth/jwt-external.js +15 -11
  17. package/lib/vendor/blamejs/lib/auth/jwt.js +2 -2
  18. package/lib/vendor/blamejs/lib/auth/oauth.js +7 -6
  19. package/lib/vendor/blamejs/lib/auth/oid4vci.js +3 -3
  20. package/lib/vendor/blamejs/lib/auth/saml.js +15 -12
  21. package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +3 -3
  22. package/lib/vendor/blamejs/lib/backup/index.js +2 -3
  23. package/lib/vendor/blamejs/lib/calendar.js +9 -2
  24. package/lib/vendor/blamejs/lib/circuit-breaker.js +5 -4
  25. package/lib/vendor/blamejs/lib/cose.js +4 -3
  26. package/lib/vendor/blamejs/lib/crypto-hpke.js +1 -1
  27. package/lib/vendor/blamejs/lib/crypto-oprf.js +2 -2
  28. package/lib/vendor/blamejs/lib/crypto.js +2 -2
  29. package/lib/vendor/blamejs/lib/framework-error.js +2 -1
  30. package/lib/vendor/blamejs/lib/guard-jwt.js +3 -2
  31. package/lib/vendor/blamejs/lib/guard-smtp-command.js +2 -2
  32. package/lib/vendor/blamejs/lib/mail-auth.js +2 -2
  33. package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +1 -1
  34. package/lib/vendor/blamejs/lib/mail-crypto-smime.js +7 -7
  35. package/lib/vendor/blamejs/lib/mail-crypto.js +1 -1
  36. package/lib/vendor/blamejs/lib/mail-dav.js +5 -4
  37. package/lib/vendor/blamejs/lib/mail-deploy.js +3 -2
  38. package/lib/vendor/blamejs/lib/mail-server-imap.js +1 -1
  39. package/lib/vendor/blamejs/lib/mail-server-jmap.js +10 -10
  40. package/lib/vendor/blamejs/lib/mail-server-managesieve.js +1 -1
  41. package/lib/vendor/blamejs/lib/mail-server-mx.js +142 -47
  42. package/lib/vendor/blamejs/lib/mail-server-submission.js +6 -5
  43. package/lib/vendor/blamejs/lib/mail-store.js +2 -2
  44. package/lib/vendor/blamejs/lib/mail.js +2 -2
  45. package/lib/vendor/blamejs/lib/mdoc.js +14 -14
  46. package/lib/vendor/blamejs/lib/network-dnssec.js +10 -8
  47. package/lib/vendor/blamejs/lib/network-tls.js +10 -7
  48. package/lib/vendor/blamejs/lib/safe-decompress.js +8 -6
  49. package/lib/vendor/blamejs/lib/safe-ical.js +12 -12
  50. package/lib/vendor/blamejs/lib/safe-mime.js +6 -6
  51. package/lib/vendor/blamejs/lib/safe-sieve.js +1 -1
  52. package/lib/vendor/blamejs/lib/safe-smtp.js +1 -1
  53. package/lib/vendor/blamejs/package.json +1 -1
  54. package/lib/vendor/blamejs/release-notes/v0.13.10.json +44 -0
  55. package/lib/vendor/blamejs/release-notes/v0.13.11.json +27 -0
  56. package/lib/vendor/blamejs/release-notes/v0.13.12.json +36 -0
  57. package/lib/vendor/blamejs/release-notes/v0.13.7.json +27 -0
  58. package/lib/vendor/blamejs/release-notes/v0.13.8.json +27 -0
  59. package/lib/vendor/blamejs/release-notes/v0.13.9.json +27 -0
  60. package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +82 -0
  61. package/lib/vendor/blamejs/test/layer-0-primitives/auth-jwt-defenses.test.js +4 -4
  62. package/lib/vendor/blamejs/test/layer-0-primitives/calendar.test.js +31 -0
  63. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +188 -6
  64. package/lib/vendor/blamejs/test/layer-0-primitives/fal.test.js +26 -0
  65. package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime.test.js +5 -5
  66. package/lib/vendor/blamejs/test/layer-0-primitives/mail-dav.test.js +1 -1
  67. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +166 -1
  68. package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-cluster.test.js +25 -18
  69. package/lib/vendor/blamejs/test/layer-0-primitives/safe-ical.test.js +2 -2
  70. package/lib/vendor/blamejs/test/layer-0-primitives/sandbox.test.js +12 -12
  71. package/lib/vendor/blamejs/test/layer-0-primitives/scheduler-exactly-once.test.js +15 -9
  72. package/lib/vendor/blamejs/test/layer-0-primitives/websocket-channels.test.js +0 -6
  73. package/package.json +1 -1
@@ -25,7 +25,7 @@
25
25
  *
26
26
  * ## Defenses baked in
27
27
  *
28
- * - **SMTP smuggling** (CVE-2023-51764 / CVE-2024-32178) — every
28
+ * - **SMTP smuggling** (CVE-2023-51764 Postfix / CVE-2023-51765 Sendmail / CVE-2023-51766 Exim) — every
29
29
  * wire line passes through `b.guardSmtpCommand.validate` which
30
30
  * refuses bare LF, bare CR, NUL, C0 controls, DEL, and oversize.
31
31
  * The DATA body's `\r\n.\r\n` terminator is matched on canonical
@@ -67,9 +67,11 @@
67
67
  *
68
68
  * - `mail.server.mx.connect` — IP, TLS state, FCrDNS hostname
69
69
  * - `mail.server.mx.helo` — HELO greeting, helo-gate verdict
70
- * - `mail.server.mx.mail_from` sender, SPF verdict, alignment verdict
71
- * - `mail.server.mx.rcpt_to` recipient, RBL verdict, greylist verdict
72
- * - `mail.server.mx.data_accepted` message size, DKIM verdict, DMARC verdict
70
+ * - `mail.server.mx.helo_gate_refused` HELO identity refused (gate action)
71
+ * - `mail.server.mx.mail_from` sender address
72
+ * - `mail.server.mx.rcpt_to` recipient, rblListed flag, greylist action
73
+ * - `mail.server.mx.rbl_refused` — connecting IP on a DNS blocklist (zones)
74
+ * - `mail.server.mx.greylist_deferred` — (ip, from, rcpt) first-seen 450 deferral
73
75
  * - `mail.server.mx.data_refused` — refusal reason + SMTP code (5xx vs 4xx)
74
76
  * - `mail.server.mx.delivered` — agent.handoff ack
75
77
  * - `mail.server.mx.tls_handshake_failed` — handshake error
@@ -100,19 +102,30 @@
100
102
  *
101
103
  * Every gate is a primitive that already exists. The MX slice is a
102
104
  * state-machine + wire-protocol coordinator — no new crypto, no
103
- * new parsing, no new RFC-layer primitives. If a gate isn't ready
104
- * (e.g. operator hasn't wired `b.mail.auth.dmarc`), the listener
105
- * skips that phase with an audit note rather than synthesizing a
106
- * verdict.
105
+ * new parsing, no new RFC-layer primitives. When the operator
106
+ * doesn't wire a gate (e.g. omits `opts.greylist`), the listener
107
+ * skips that phase rather than synthesizing a verdict.
108
+ *
109
+ * Connection-level gates are wired into the live state machine:
110
+ * `opts.helo` (HELO identity) evaluates at HELO/EHLO; `opts.rbl`
111
+ * (connecting-IP DNS blocklist, evaluated once per connection) and
112
+ * `opts.greylist` ((ip, from, rcpt) first-seen deferral) evaluate at
113
+ * RCPT TO and surface their verdicts on the `rcpt_to` event. The
114
+ * message-authentication gates (`b.guardEnvelope` SPF/DKIM/DMARC
115
+ * alignment) require the inbound SPF + DKIM verification results as
116
+ * inputs; that inbound-auth pipeline composes `b.mail.spf` +
117
+ * `b.mail.dmarc` + DKIM verification and lands as a follow-up, at
118
+ * which point the DATA-phase envelope/DMARC gate wires in. Until
119
+ * then operators run those checks on the delivered message via the
120
+ * agent handoff.
107
121
  *
108
122
  * @card
109
123
  * Inbound SMTP / MX listener. RFC 5321 state machine with SMTP-
110
124
  * smuggling defense baked into the wire-protocol layer (RFC 5321
111
- * §2.3.8 + CVE-2023-51764 / CVE-2024-32178), open-relay refusal by
125
+ * §2.3.8 + CVE-2023-51764 / 51765 / 51766), open-relay refusal by
112
126
  * default, STARTTLS-stripping defense (CVE-2021-38371), and the
113
- * framework's mail-gate cascade (HELO / RBL / greylist /
114
- * guardEnvelope / DMARC / safeMime / guardEmail) running at the
115
- * appropriate phase.
127
+ * connection-level gate cascade (HELO identity / RBL / greylist)
128
+ * running at the appropriate phase.
116
129
  */
117
130
 
118
131
  var net = require("node:net");
@@ -149,6 +162,7 @@ var REPLY_221_BYE = "221";
149
162
  var REPLY_250_OK = "250";
150
163
  var REPLY_354_START_INPUT = "354";
151
164
  var REPLY_421_SERVICE_NOT_AVAIL = "421"; // allow:raw-byte-literal — SMTP transient code
165
+ var REPLY_450_MAILBOX_BUSY = "450"; // allow:raw-byte-literal — SMTP transient code (greylist tempfail)
152
166
  var REPLY_451_LOCAL_ERROR = "451"; // allow:raw-byte-literal — SMTP transient code
153
167
  var REPLY_452_INSUFFICIENT_STG = "452"; // allow:raw-byte-literal — SMTP transient code
154
168
  var REPLY_500_SYNTAX = "500"; // allow:raw-byte-literal — SMTP permanent code
@@ -177,11 +191,9 @@ var RE_SIZE = /SIZE=(\d+)/i;
177
191
  * @opts
178
192
  * tlsContext: TlsContext, // required — b.network.tls.context() output (no implicit plaintext)
179
193
  * greeting: string, // default "blamejs ESMTP" — HELO/EHLO 220-line banner
180
- * helo: b.mail.helo, // optional gate
181
- * rbl: b.mail.rbl, // optional gate
182
- * greylist: b.mail.greylist, // optional gate
183
- * envelope: b.guardEnvelope, // optional gate (SPF/DKIM alignment)
184
- * dmarc: b.mail.auth.dmarc, // optional gate
194
+ * helo: b.mail.helo, // optional gate — HELO identity (FCrDNS / shape / self-name)
195
+ * rbl: b.mail.rbl.create(…), // optional gate — DNS blocklist on the connecting IP
196
+ * greylist: b.mail.greylist.create(…), // optional gate — defer first-seen (ip, from, rcpt)
185
197
  * agent: b.mail.agent, // optional delivery handoff
186
198
  * relayAllowedFor: [{ cidr, scope }], // operator-explicit relay allowlist; default [] = MX-only
187
199
  * localDomains: [string], // RCPT TO local-domain allowlist (refuse non-local with 550 5.7.1)
@@ -199,7 +211,6 @@ var RE_SIZE = /SIZE=(\d+)/i;
199
211
  * helo: b.mail.helo,
200
212
  * rbl: b.mail.rbl.create({ providers: ["zen.spamhaus.org"] }),
201
213
  * greylist: b.mail.greylist.create({ store: greylistStore }),
202
- * envelope: b.guardEnvelope,
203
214
  * agent: b.mail.agent.create({ store: mailStore }),
204
215
  * localDomains: ["example.com"],
205
216
  * });
@@ -260,8 +271,8 @@ function create(opts) {
260
271
 
261
272
  // Default-on operator-supplied-domain hardening. opts.localDomains
262
273
  // and the HELO / MAIL FROM / RCPT TO domain validations all route
263
- // through `b.guardDomain` for IDN homograph defense (CVE-2017-5469
264
- // class), special-use-domain refusal (RFC 6761), label-length cap
274
+ // through `b.guardDomain` for IDN homograph / Punycode-spoof defense
275
+ // (mixed-script confusable class), special-use-domain refusal (RFC 6761), label-length cap
265
276
  // (RFC 1035 §2.3.4), and bare-IP-as-domain refusal (CVE-2021-22931
266
277
  // class). Operators with a closed-network deployment can pass
267
278
  // `guardDomain: false` to skip; the default keeps the protection on.
@@ -367,6 +378,15 @@ function create(opts) {
367
378
  var lineBuffer = Buffer.alloc(0);
368
379
  var bodyCollector = null;
369
380
  var inDataBody = false;
381
+ // Async command pump: gates (HELO / RBL / greylist / envelope /
382
+ // DMARC) may await DNS or a store, so command handling is async.
383
+ // `pumpChain` FIFO-serializes per-chunk processing so a gate
384
+ // resolving cannot let a later pipelined command (RFC 2920) jump
385
+ // ahead of an earlier one — reply ordering + the per-command
386
+ // smuggling defenses stay intact. `connClosed` short-circuits any
387
+ // chunk queued before a teardown.
388
+ var pumpChain = Promise.resolve();
389
+ var connClosed = false;
370
390
 
371
391
  socket.setTimeout(idleTimeoutMs);
372
392
  socket.on("timeout", function () {
@@ -382,6 +402,7 @@ function create(opts) {
382
402
  });
383
403
 
384
404
  socket.on("close", function () {
405
+ connClosed = true;
385
406
  connections.delete(socket);
386
407
  });
387
408
 
@@ -395,20 +416,35 @@ function create(opts) {
395
416
  // 220 banner — RFC 5321 §3.1.
396
417
  _writeReply(socket, REPLY_220_READY, greeting + " ready");
397
418
 
398
- socket.on("data", function (chunk) {
399
- try { _ingestBytes(state, socket, chunk); }
400
- catch (err) {
419
+ // Feed a chunk into the per-connection command pump. Chains each
420
+ // chunk behind the previous one's full (async) processing so command
421
+ // handlers + their gates run strictly in arrival order. Used by BOTH
422
+ // the plaintext `socket.on("data")` path AND the post-STARTTLS
423
+ // TLSSocket onData path — otherwise gate awaits on the upgraded
424
+ // socket would overlap later TLS chunks (the default strict/balanced
425
+ // profiles require STARTTLS before MAIL, so the gates run there) and
426
+ // async gate rejections would go unhandled instead of producing the
427
+ // 421 path. `activeSock` is whichever socket is current (plaintext or
428
+ // TLS) so the 421/close lands on the right transport.
429
+ function _feedChunk(activeSock, chunk) {
430
+ pumpChain = pumpChain.then(function () {
431
+ if (connClosed) return undefined;
432
+ return _ingestBytes(state, activeSock, chunk);
433
+ }).catch(function (err) {
434
+ if (connClosed) return;
401
435
  _emit("mail.server.mx.handler_threw",
402
436
  { connectionId: state.id, error: (err && err.message) || String(err) },
403
437
  "failure");
404
- try { _writeReply(socket, REPLY_421_SERVICE_NOT_AVAIL, "4.3.0 Server error"); }
438
+ try { _writeReply(activeSock, REPLY_421_SERVICE_NOT_AVAIL, "4.3.0 Server error"); }
405
439
  catch (_e) { /* socket already gone */ }
406
- _closeConnection(socket);
407
- }
408
- });
440
+ _closeConnection(activeSock);
441
+ });
442
+ }
443
+
444
+ socket.on("data", function (chunk) { _feedChunk(socket, chunk); });
409
445
 
410
446
  // ---- Byte-level ingestion --------------------------------------------
411
- function _ingestBytes(state, socket, chunk) {
447
+ async function _ingestBytes(state, socket, chunk) {
412
448
  if (inDataBody) {
413
449
  // DATA body — accumulate via boundedChunkCollector, watch for
414
450
  // canonical "\r\n.\r\n" terminator only. Bare-LF dot terminator
@@ -444,9 +480,9 @@ function create(opts) {
444
480
  var endIdx = safeSmtp.findDotTerminator(collected);
445
481
  if (endIdx !== -1) {
446
482
  var body = collected.subarray(0, endIdx);
447
- _finalizeDataBody(state, socket, body);
448
483
  inDataBody = false;
449
484
  bodyCollector = null;
485
+ await _finalizeDataBody(state, socket, body);
450
486
  }
451
487
  return;
452
488
  }
@@ -464,12 +500,13 @@ function create(opts) {
464
500
  while ((crlf = lineBuffer.indexOf(crlfNeedle)) !== -1) {
465
501
  var line = lineBuffer.subarray(0, crlf).toString("utf8");
466
502
  lineBuffer = lineBuffer.subarray(crlf + 2);
467
- _handleCommand(state, socket, line);
503
+ await _handleCommand(state, socket, line);
468
504
  if (inDataBody) return;
505
+ if (connClosed) return;
469
506
  }
470
507
  }
471
508
 
472
- function _handleCommand(state, socket, line) {
509
+ async function _handleCommand(state, socket, line) {
473
510
  // Per-line guard — refuse bare LF / NUL / C0 / DEL / oversize
474
511
  // BEFORE state-machine dispatch.
475
512
  try {
@@ -494,16 +531,16 @@ function create(opts) {
494
531
  switch (verb) {
495
532
  case "EHLO":
496
533
  case "HELO":
497
- _handleEhlo(state, socket, line, verb);
534
+ await _handleEhlo(state, socket, line, verb);
498
535
  return;
499
536
  case "STARTTLS":
500
537
  _handleStartTls(state, socket);
501
538
  return;
502
539
  case "MAIL":
503
- _handleMailFrom(state, socket, line);
540
+ await _handleMailFrom(state, socket, line);
504
541
  return;
505
542
  case "RCPT":
506
- _handleRcptTo(state, socket, line);
543
+ await _handleRcptTo(state, socket, line);
507
544
  return;
508
545
  case "DATA":
509
546
  _handleData(state, socket);
@@ -531,7 +568,7 @@ function create(opts) {
531
568
  }
532
569
 
533
570
  // ---- EHLO / HELO ------------------------------------------------------
534
- function _handleEhlo(state, socket, line, verb) {
571
+ async function _handleEhlo(state, socket, line, verb) {
535
572
  var helo = line.slice(verb.length).trim();
536
573
  if (!helo) {
537
574
  _writeReply(socket, REPLY_501_BAD_ARGS, "5.5.4 " + verb + " requires a domain argument");
@@ -552,6 +589,25 @@ function create(opts) {
552
589
  return;
553
590
  }
554
591
  }
592
+ // Operator HELO-identity gate (b.mail.helo) — FCrDNS / HELO-shape /
593
+ // self-name spoofing checks. Composed when the operator wires
594
+ // `opts.helo`; skipped silently otherwise (no synthesized verdict).
595
+ // Hard-reject actions (reject-shape / match-self-refused /
596
+ // literal-mismatch) refuse the connection; "accept" and the
597
+ // advisory "soft-*" actions pass (the soft verdict rides the event).
598
+ if (opts.helo && typeof opts.helo.evaluate === "function") {
599
+ var heloGate = await opts.helo.evaluate(
600
+ { claimedName: helo, ip: state.remoteAddress, tls: state.tls }, {});
601
+ state.heloVerdict = heloGate && heloGate.action;
602
+ if (heloGate && heloGate.action && heloGate.action !== "accept" &&
603
+ heloGate.action.indexOf("soft") !== 0) {
604
+ _emit("mail.server.mx.helo_gate_refused",
605
+ { connectionId: state.id, helo: helo, action: heloGate.action }, "denied");
606
+ _writeReply(socket, REPLY_550_MAILBOX_UNAVAIL,
607
+ "5.7.1 " + verb + " identity refused (" + heloGate.action + ")");
608
+ return;
609
+ }
610
+ }
555
611
  state.helo = helo;
556
612
  state.stage = "ehlo";
557
613
  // Multi-line 250 capabilities advertisement per RFC 5321 §4.1.1.1.
@@ -572,7 +628,8 @@ function create(opts) {
572
628
  _writeReply(socket, REPLY_250_OK, greeting + " greets " + helo);
573
629
  }
574
630
  _emit("mail.server.mx.helo",
575
- { connectionId: state.id, verb: verb, helo: helo, tls: state.tls });
631
+ { connectionId: state.id, verb: verb, helo: helo, tls: state.tls,
632
+ heloVerdict: state.heloVerdict || null });
576
633
  }
577
634
 
578
635
  // ---- STARTTLS ---------------------------------------------------------
@@ -604,13 +661,11 @@ function create(opts) {
604
661
  state.helo = null;
605
662
  },
606
663
  onData: function (tlsSocket, chunk) {
607
- try { _ingestBytes(state, tlsSocket, chunk); }
608
- catch (err) {
609
- _emit("mail.server.mx.handler_threw",
610
- { connectionId: state.id, error: (err && err.message) || String(err) },
611
- "failure");
612
- _closeConnection(tlsSocket);
613
- }
664
+ // Route the upgraded socket through the SAME serialized pump as
665
+ // the plaintext path — post-STARTTLS is where the gates run in
666
+ // the default strict/balanced profiles, so it MUST be serialized
667
+ // and its async rejections MUST hit the 421 path.
668
+ _feedChunk(tlsSocket, chunk);
614
669
  },
615
670
  onError: function (err) {
616
671
  _emit("mail.server.mx.tls_handshake_failed",
@@ -626,7 +681,7 @@ function create(opts) {
626
681
  }
627
682
 
628
683
  // ---- MAIL FROM --------------------------------------------------------
629
- function _handleMailFrom(state, socket, line) {
684
+ async function _handleMailFrom(state, socket, line) {
630
685
  if (!state.tls && _requiresStartTls()) {
631
686
  _writeReply(socket, REPLY_530_AUTH_REQUIRED, "5.7.0 Must issue a STARTTLS command first");
632
687
  return;
@@ -677,7 +732,7 @@ function create(opts) {
677
732
  }
678
733
 
679
734
  // ---- RCPT TO ----------------------------------------------------------
680
- function _handleRcptTo(state, socket, line) {
735
+ async function _handleRcptTo(state, socket, line) {
681
736
  if (state.stage !== "rcpt") {
682
737
  _writeReply(socket, REPLY_503_BAD_SEQUENCE, "5.5.1 MAIL FROM first");
683
738
  return;
@@ -740,9 +795,49 @@ function create(opts) {
740
795
  return;
741
796
  }
742
797
  }
798
+ // RBL gate (b.mail.rbl) — DNS blocklist check on the connecting
799
+ // IP. The verdict is per-connection, so it's evaluated once and
800
+ // cached on state; a listed IP refuses with 554. Skipped silently
801
+ // when opts.rbl isn't wired.
802
+ if (opts.rbl && typeof opts.rbl.query === "function") {
803
+ if (state.rblVerdict === undefined) {
804
+ state.rblVerdict = await opts.rbl.query(state.remoteAddress);
805
+ }
806
+ if (state.rblVerdict && Array.isArray(state.rblVerdict.listed) &&
807
+ state.rblVerdict.listed.length > 0) {
808
+ _trackRefusedRcpt(state, rcpt, "rbl-listed");
809
+ _emit("mail.server.mx.rbl_refused",
810
+ { connectionId: state.id, remoteAddress: state.remoteAddress,
811
+ zones: state.rblVerdict.listed.map(function (l) { return l.zone; }) }, "denied");
812
+ _writeReply(socket, REPLY_554_TRANSACTION_FAILED,
813
+ "5.7.1 Connecting IP is on a DNS blocklist");
814
+ return;
815
+ }
816
+ }
817
+ // Greylist gate (b.mail.greylist) — defer first sight of an
818
+ // (ip, mailFrom, rcpt) tuple with a 450 tempfail; legitimate
819
+ // senders retry and pass. "defer" → 450; "accept" → continue.
820
+ // Skipped silently when opts.greylist isn't wired.
821
+ var greyVerdict = null;
822
+ if (opts.greylist && typeof opts.greylist.check === "function") {
823
+ greyVerdict = await opts.greylist.check(
824
+ { ip: state.remoteAddress, mailFrom: state.mailFrom || "", rcptTo: rcpt });
825
+ if (greyVerdict && greyVerdict.action === "defer") {
826
+ _emit("mail.server.mx.greylist_deferred",
827
+ { connectionId: state.id, remoteAddress: state.remoteAddress,
828
+ mailFrom: state.mailFrom, rcptTo: rcpt,
829
+ reason: greyVerdict.reason }, "denied");
830
+ _writeReply(socket, REPLY_450_MAILBOX_BUSY,
831
+ "4.7.1 Greylisted — please retry shortly");
832
+ return;
833
+ }
834
+ }
743
835
  state.rcpts.push(rcpt);
744
836
  _emit("mail.server.mx.rcpt_to",
745
- { connectionId: state.id, rcptTo: rcpt, rcptCount: state.rcpts.length });
837
+ { connectionId: state.id, rcptTo: rcpt, rcptCount: state.rcpts.length,
838
+ rblListed: !!(state.rblVerdict && Array.isArray(state.rblVerdict.listed) &&
839
+ state.rblVerdict.listed.length > 0),
840
+ greylist: greyVerdict ? greyVerdict.action : null });
746
841
  _writeReply(socket, REPLY_250_OK, "2.1.5 Recipient OK");
747
842
  }
748
843
 
@@ -764,7 +859,7 @@ function create(opts) {
764
859
  });
765
860
  }
766
861
 
767
- function _finalizeDataBody(state, socket, body) {
862
+ async function _finalizeDataBody(state, socket, body) {
768
863
  // body is the raw bytes BEFORE dot-stuffing reversal. RFC 5321
769
864
  // §4.5.2 — a single leading "." is doubled on the wire; undo.
770
865
  var dedotted = safeSmtp.dotUnstuff(body);
@@ -40,7 +40,7 @@
40
40
  *
41
41
  * ## Wire-protocol defenses (inherited from MX listener pattern)
42
42
  *
43
- * - SMTP smuggling (CVE-2023-51764 / -51765 / -51766 / 2024-32178 /
43
+ * - SMTP smuggling (CVE-2023-51764 / -51765 / -51766 /
44
44
  * RFC 5321 §2.3.8): every wire line through
45
45
  * `b.guardSmtpCommand.validate`; DATA-body terminator scan
46
46
  * through `b.safeSmtp.findDotTerminator` (strict-CRLF);
@@ -79,11 +79,12 @@
79
79
  *
80
80
  * - **DKIM signing pre-relay** — operator wires `b.mail.dkim.sign`
81
81
  * in their outbound agent.
82
- * - **CHUNKING (BDAT) extension** — RFC 3030 BDAT not yet
83
- * supported on submission; clients use DATA instead.
84
82
  * - **Per-actor outbound quota** — operator implements via
85
83
  * `b.dailyByteQuota` against the authenticated actor.
86
84
  *
85
+ * (CHUNKING / BDAT, RFC 3030, IS supported — advertised in EHLO and
86
+ * handled alongside DATA.)
87
+ *
87
88
  * ## Composition contract
88
89
  *
89
90
  * Every gate is a primitive that already exists. Submission listener
@@ -350,8 +351,8 @@ function create(opts) {
350
351
  }
351
352
 
352
353
  // Default-on guardDomain hardening for HELO / MAIL FROM / RCPT TO.
353
- // Same posture as mail-server-mx — IDN homograph (CVE-2017-5469
354
- // class), special-use-domain refusal (RFC 6761), label-length cap
354
+ // Same posture as mail-server-mx — IDN homograph / Punycode-spoof
355
+ // (mixed-script confusable class), special-use-domain refusal (RFC 6761), label-length cap
355
356
  // (RFC 1035 §2.3.4), bare-IP-as-domain refusal (CVE-2021-22931
356
357
  // class). Operators with a closed-network deployment pass
357
358
  // `guardDomain: false` to skip; the default keeps protection on.
@@ -50,7 +50,7 @@
50
50
  * of caller; only `b.legalHold.release` can flip the flag.
51
51
  *
52
52
  * Parses messages on append via `b.safeMime.parse` (bounded
53
- * substrate, defends CVE-2024-39929 + CVE-2025-30258). Validates
53
+ * substrate, defends CVE-2024-39929 + CVE-2026-26312). Validates
54
54
  * `Message-Id` via `b.guardMessageId.validate`.
55
55
  *
56
56
  * @card
@@ -526,7 +526,7 @@ function _appendMessage(args) {
526
526
  "appendMessage: folder '" + args.folderName + "' not found");
527
527
  }
528
528
 
529
- // Parse via safe-mime — bounded; defends CVE-2024-39929 + CVE-2025-30258.
529
+ // Parse via safe-mime — bounded; defends CVE-2024-39929 + CVE-2026-26312.
530
530
  var tree = safeMime.parse(buf, args.safeMimeOpts);
531
531
 
532
532
  // Extract canonical fields.
@@ -1594,10 +1594,10 @@ function create(opts) {
1594
1594
  var auditOn = opts.audit !== false;
1595
1595
 
1596
1596
  // Default-on guardDomain hardening for every outbound recipient + the
1597
- // sender address. Refuses CVE-2017-5469-class IDN homograph spoofs in
1597
+ // sender address. Refuses IDN homograph / mixed-script-confusable spoofs in
1598
1598
  // recipient or from domains, RFC 6761 special-use domain names
1599
1599
  // (`.localhost`, `.test`, `.invalid`, `.example`) in production sends,
1600
- // RFC 1035 §2.3.4 label-length violations, and CVE-2021-22931-class
1600
+ // RFC 1035 §2.3.4 label-length violations, and CVE-2021-22931 class
1601
1601
  // bare-IP-as-domain (DNS-rebinding allowlist-bypass class). Operators
1602
1602
  // sending to address literals (`<x@[1.2.3.4]>`) — rare; mostly mailing-
1603
1603
  // list internals — pass `guardDomain: false` to opt out, or pass
@@ -30,22 +30,22 @@
30
30
  * <code>opts.trustAnchorsPem</code> additionally verifies the issuer
31
31
  * certificate chain and its validity at the asserted time.
32
32
  *
33
- * <strong>Scope.</strong> This is issuer-data authentication
34
- * (ISO 18013-5 §9.1.2.4) — the data is genuine and issuer-signed. The
35
- * mdoc <em>device authentication</em> half (DeviceSigned / the
36
- * SessionTranscript-bound holder-binding proof, §9.1.3) is deferred:
37
- * it needs the live session transcript a verifier negotiates, so it is
38
- * a presentation-protocol concern rather than a credential check.
39
- * Composes <code>b.cose</code> + <code>b.cbor</code>; no new runtime
40
- * dependency. Distinct from W3C VCDM (<code>b.vc</code>) and IETF
41
- * SD-JWT VC (<code>b.auth.sdJwtVc</code>) the three credential
42
- * ecosystems.
33
+ * <strong>Scope.</strong> Two halves are verified: issuer-data
34
+ * authentication (ISO 18013-5 §9.1.2.4 — the data is genuine and
35
+ * issuer-signed, via <code>verifyIssuerSigned</code>) and mdoc device
36
+ * authentication (§9.1.3 holder binding over the verifier's
37
+ * <code>SessionTranscript</code>, via <code>verifyDeviceAuth</code>).
38
+ * Device auth covers the COSE_Sign1 signature variant; the COSE_Mac0
39
+ * (deviceMac) variant is refused rather than mis-verified. Composes
40
+ * <code>b.cose</code> + <code>b.cbor</code>; no new runtime dependency.
41
+ * Distinct from W3C VCDM (<code>b.vc</code>) and IETF SD-JWT VC
42
+ * (<code>b.auth.sdJwtVc</code>) — the three credential ecosystems.
43
43
  *
44
44
  * @card
45
- * ISO 18013-5 mdoc / mDL issuer-data verification — checks the
46
- * COSE_Sign1 IssuerAuth, the MSO validity window, and every disclosed
47
- * element's digest against the Mobile Security Object. Composes
48
- * b.cose + b.cbor; device-auth holder-binding deferred.
45
+ * ISO 18013-5 mdoc / mDL verification — issuer-data (COSE_Sign1
46
+ * IssuerAuth, MSO validity window, disclosed-element digests) plus
47
+ * device-auth holder binding (§9.1.3 signature variant over the session
48
+ * transcript). Composes b.cose + b.cbor.
49
49
  */
50
50
 
51
51
  var nodeCrypto = require("node:crypto");
@@ -30,16 +30,18 @@
30
30
  * <code>dnssec/uncanonicalizable-type</code> rather than mis-validated
31
31
  * — the security-critical DNSKEY / DS and the name-free address /
32
32
  * text types (A, AAAA, TXT, …) are fully supported. The recursive
33
- * chain-walk (root → TLD → zone), NSEC / NSEC3 denial-of-existence,
34
- * and the IANA root trust-anchor bundle are deferred: these primitives
35
- * are the per-RRset building blocks a chain-walker composes.
33
+ * chain-walk (root → TLD → zone via <code>verifyChain</code> against the
34
+ * bundled IANA root trust anchors) and NSEC / NSEC3 denial-of-existence
35
+ * (<code>verifyDenial</code> / <code>nsec3Hash</code>) ship alongside the
36
+ * per-RRset verification core.
36
37
  *
37
38
  * @card
38
- * Local DNSSEC verification (RFC 4035) — verify an RRSIG over a
39
- * canonicalised RRset against a DNSKEY (RSA / ECDSA P-256·P-384 /
40
- * Ed25519), plus DS-digest + key-tag. Don't trust the upstream AD bit;
41
- * verify the signature. Name-bearing RR types are refused, not
42
- * mis-validated; chain-walk + NSEC3 deferred.
39
+ * Local DNSSEC verification (RFC 4035 / 4034 / 5155) — verify an RRSIG
40
+ * over a canonicalised RRset against a DNSKEY (RSA / ECDSA P-256·P-384 /
41
+ * Ed25519) + DS-digest + key-tag, walk the root→zone chain to the IANA
42
+ * trust anchors, and check NSEC / NSEC3 denial of existence. Don't trust
43
+ * the upstream AD bit; verify the signature. Name-bearing RR types are
44
+ * refused, not mis-validated.
43
45
  */
44
46
 
45
47
  var nodeCrypto = require("node:crypto");
@@ -462,12 +462,15 @@ function applyToContext(opts) {
462
462
  // setKeyShares(["X25519MLKEM768", "X25519"]) → string[] (after)
463
463
  // resetKeyShares() → restores default
464
464
 
465
- // RFC 9794 (PQ TLS Hybrid Key Exchange) named-group ordering. The
466
- // preferred groups (the first the peer mutually supports wins) put the
467
- // IANA-registered hybrid named groups ahead of the classical fallback:
465
+ // PQ/T-hybrid named-group ordering (RFC 9794 is the PQ/T-hybrid
466
+ // *terminology*; the TLS codepoints come from the IANA TLS Supported
467
+ // Groups registry + draft-kwiatkowski-tls-ecdhe-mlkem +
468
+ // draft-ietf-tls-hybrid-design). The preferred groups (the first the peer
469
+ // mutually supports wins) put the IANA-registered hybrid named groups
470
+ // ahead of the classical fallback:
468
471
  //
469
- // X25519MLKEM768 — codepoint 0x11EC, RFC 9794 default hybrid
470
- // SecP256r1MLKEM768 — codepoint 0x11EB, RFC 9794 optional hybrid
472
+ // X25519MLKEM768 — codepoint 0x11EC (IANA; draft-kwiatkowski-tls-ecdhe-mlkem)
473
+ // SecP256r1MLKEM768 — codepoint 0x11EB (IANA; NIST-curve hybrid)
471
474
  // (NIST-curve fallback for FIPS-mandated peers
472
475
  // that refuse X25519)
473
476
  // SecP384r1MLKEM1024 — draft-kwiatkowski-tls-ecdhe-mlkem-02 codepoint
@@ -520,7 +523,7 @@ function resetKeyShares() {
520
523
  return getKeyShares();
521
524
  }
522
525
 
523
- // preferredGroups — RFC 9794 alias surface for the named-group list.
526
+ // preferredGroups — alias surface for the named-group list.
524
527
  // `set(list)` overrides the default ordering; `get()` reads the active
525
528
  // list; `reset()` restores the framework default. The setKeyShares /
526
529
  // getKeyShares / resetKeyShares names are kept as the lower-level
@@ -604,7 +607,7 @@ function buildOptions(opts) {
604
607
 
605
608
  // PQC group preference. Caller may narrow (drop a group) but not
606
609
  // widen — every requested group must appear in the framework
607
- // preferred list. Both `groups` (RFC 9794 alias) and `ecdhCurve`
610
+ // preferred list. Both `groups` (alias) and `ecdhCurve`
608
611
  // (Node TLS option) are accepted; `groups` wins when both supplied.
609
612
  var requested = null;
610
613
  if (Array.isArray(opts.groups)) {
@@ -54,10 +54,12 @@
54
54
  * bytes ever cross the audit boundary on the bomb-class path.
55
55
  *
56
56
  * Threat model:
57
- * - **CVE-2025-0725** (libcurl + zlib decompression amplification)
58
- * bounded output + ratio cap defeat the amplification.
59
- * - **CVE-2024-zlib** class (decompression-bomb research, gzip /
60
- * deflate / brotli variants) bounded output prevents OOM.
57
+ * - **Decompression bomb** (CWE-409 improper handling of highly
58
+ * compressed data; the classic 42.zip nested-bomb expands to
59
+ * petabytes from kilobytes) across gzip / deflate / brotli —
60
+ * the bounded-output cap + expansion-ratio cap refuse before the
61
+ * allocation, so no decompressed bytes are ever materialized past
62
+ * the cap.
61
63
  * - **Efail-class** (CVE-2017-17688 / 17689) — operators decrypting
62
64
  * MIME parts compose `b.safeDecompress` on the inner deflate
63
65
  * streams; the bounded-output posture defeats the unbounded-
@@ -73,8 +75,8 @@
73
75
  * - [RFC 1951](https://www.rfc-editor.org/rfc/rfc1951) deflate
74
76
  * - [RFC 1952](https://www.rfc-editor.org/rfc/rfc1952) gzip
75
77
  * - [RFC 7932](https://www.rfc-editor.org/rfc/rfc7932) brotli
76
- * - [CVE-2025-0725](https://nvd.nist.gov/vuln/detail/CVE-2025-0725)
77
- * - [CVE-2024-zlib](https://nvd.nist.gov/) decompression-bomb class
78
+ * - [CWE-409](https://cwe.mitre.org/data/definitions/409.html) improper
79
+ * handling of highly compressed data (decompression bomb)
78
80
  */
79
81
 
80
82
  var zlib = require("node:zlib");