@blamejs/blamejs-shop 0.1.38 → 0.2.1

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 (64) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/lib/asset-manifest.json +3 -3
  3. package/lib/storefront.js +94 -21
  4. package/lib/vendor/MANIFEST.json +2 -2
  5. package/lib/vendor/blamejs/CHANGELOG.md +10 -0
  6. package/lib/vendor/blamejs/README.md +1 -1
  7. package/lib/vendor/blamejs/SECURITY.md +2 -1
  8. package/lib/vendor/blamejs/api-snapshot.json +2 -2
  9. package/lib/vendor/blamejs/lib/archive-read.js +17 -0
  10. package/lib/vendor/blamejs/lib/auth/fal.js +12 -0
  11. package/lib/vendor/blamejs/lib/auth/jwt-external.js +15 -11
  12. package/lib/vendor/blamejs/lib/auth/jwt.js +2 -2
  13. package/lib/vendor/blamejs/lib/auth/oauth.js +7 -6
  14. package/lib/vendor/blamejs/lib/auth/oid4vci.js +3 -3
  15. package/lib/vendor/blamejs/lib/auth/saml.js +15 -12
  16. package/lib/vendor/blamejs/lib/auth/sd-jwt-vc.js +3 -3
  17. package/lib/vendor/blamejs/lib/calendar.js +9 -2
  18. package/lib/vendor/blamejs/lib/circuit-breaker.js +5 -4
  19. package/lib/vendor/blamejs/lib/crypto-hpke.js +1 -1
  20. package/lib/vendor/blamejs/lib/crypto-oprf.js +2 -2
  21. package/lib/vendor/blamejs/lib/crypto.js +2 -2
  22. package/lib/vendor/blamejs/lib/framework-error.js +2 -1
  23. package/lib/vendor/blamejs/lib/guard-filename.js +90 -5
  24. package/lib/vendor/blamejs/lib/guard-jwt.js +3 -2
  25. package/lib/vendor/blamejs/lib/guard-smtp-command.js +2 -2
  26. package/lib/vendor/blamejs/lib/mail-auth.js +2 -2
  27. package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +1 -1
  28. package/lib/vendor/blamejs/lib/mail-crypto-smime.js +7 -7
  29. package/lib/vendor/blamejs/lib/mail-crypto.js +1 -1
  30. package/lib/vendor/blamejs/lib/mail-dav.js +5 -4
  31. package/lib/vendor/blamejs/lib/mail-deploy.js +3 -2
  32. package/lib/vendor/blamejs/lib/mail-server-imap.js +1 -1
  33. package/lib/vendor/blamejs/lib/mail-server-jmap.js +1 -1
  34. package/lib/vendor/blamejs/lib/mail-server-managesieve.js +1 -1
  35. package/lib/vendor/blamejs/lib/mail-server-mx.js +142 -47
  36. package/lib/vendor/blamejs/lib/mail-server-submission.js +3 -3
  37. package/lib/vendor/blamejs/lib/mail-store.js +2 -2
  38. package/lib/vendor/blamejs/lib/mail.js +2 -2
  39. package/lib/vendor/blamejs/lib/network-tls.js +10 -7
  40. package/lib/vendor/blamejs/lib/safe-decompress.js +8 -6
  41. package/lib/vendor/blamejs/lib/safe-ical.js +12 -12
  42. package/lib/vendor/blamejs/lib/safe-mime.js +6 -6
  43. package/lib/vendor/blamejs/lib/safe-sieve.js +1 -1
  44. package/lib/vendor/blamejs/lib/safe-smtp.js +1 -1
  45. package/lib/vendor/blamejs/package.json +1 -1
  46. package/lib/vendor/blamejs/release-notes/v0.13.10.json +44 -0
  47. package/lib/vendor/blamejs/release-notes/v0.13.11.json +27 -0
  48. package/lib/vendor/blamejs/release-notes/v0.13.12.json +36 -0
  49. package/lib/vendor/blamejs/release-notes/v0.13.13.json +18 -0
  50. package/lib/vendor/blamejs/release-notes/v0.13.9.json +27 -0
  51. package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +76 -0
  52. package/lib/vendor/blamejs/test/layer-0-primitives/auth-jwt-defenses.test.js +4 -4
  53. package/lib/vendor/blamejs/test/layer-0-primitives/calendar.test.js +31 -0
  54. package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +104 -7
  55. package/lib/vendor/blamejs/test/layer-0-primitives/fal.test.js +26 -0
  56. package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime.test.js +5 -5
  57. package/lib/vendor/blamejs/test/layer-0-primitives/mail-dav.test.js +1 -1
  58. package/lib/vendor/blamejs/test/layer-0-primitives/mail-server-mx.test.js +166 -1
  59. package/lib/vendor/blamejs/test/layer-0-primitives/rate-limit-cluster.test.js +45 -27
  60. package/lib/vendor/blamejs/test/layer-0-primitives/safe-ical.test.js +2 -2
  61. package/lib/vendor/blamejs/test/layer-0-primitives/sandbox.test.js +12 -12
  62. package/lib/vendor/blamejs/test/layer-0-primitives/scheduler-exactly-once.test.js +15 -9
  63. package/lib/vendor/blamejs/test/layer-0-primitives/websocket-channels.test.js +0 -6
  64. 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);
@@ -351,8 +351,8 @@ function create(opts) {
351
351
  }
352
352
 
353
353
  // Default-on guardDomain hardening for HELO / MAIL FROM / RCPT TO.
354
- // Same posture as mail-server-mx — IDN homograph (CVE-2017-5469
355
- // 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
356
356
  // (RFC 1035 §2.3.4), bare-IP-as-domain refusal (CVE-2021-22931
357
357
  // class). Operators with a closed-network deployment pass
358
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
@@ -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");
@@ -15,7 +15,7 @@
15
15
  * delivery-time iTIP processing, and the scheduling primitives that
16
16
  * compose against ical bytes.
17
17
  *
18
- * Defends `CVE-2024-39687` (ical4j RRULE recursion / "Outlook
18
+ * Defends the ical4j RRULE-recursion expansion-DoS class ("Outlook
19
19
  * calendar bomb" — a hostile RRULE with unbounded COUNT and
20
20
  * recursive BYxxx expansion can pin a CalDAV server's CPU at 100%
21
21
  * until the request times out). Caps:
@@ -36,8 +36,8 @@
36
36
  * instances than this cap.
37
37
  * - RRULE BYDAY / BYMONTH / BYMONTHDAY / BYHOUR / BYMINUTE /
38
38
  * BYSECOND / BYSETPOS / BYWEEKNO / BYYEARDAY list-length cap
39
- * (24 entries) — refused regardless of profile. CVE-2024-39687
40
- * achieves expansion blow-up by stacking long BYxxx lists.
39
+ * (24 entries) — refused regardless of profile. The recursion
40
+ * DoS achieves expansion blow-up by stacking long BYxxx lists.
41
41
  *
42
42
  * Header-injection / control-char defense: refuses NUL, C0 control
43
43
  * bytes (other than TAB inside QUOTED-PRINTABLE-shaped values), and
@@ -75,8 +75,8 @@
75
75
  * @card
76
76
  * Bounded RFC 5545 iCalendar parser — caps total bytes, nesting
77
77
  * depth, RRULE COUNT and BYxxx list-lengths; refuses NUL / C0 / DEL
78
- * inside property values; allowlists property names; defends
79
- * CVE-2024-39687 (ical4j RRULE recursion / Outlook calendar-bomb).
78
+ * inside property values; allowlists property names; defends the
79
+ * ical4j RRULE-recursion expansion-DoS class (Outlook calendar-bomb).
80
80
  */
81
81
 
82
82
  var C = require("./constants");
@@ -84,8 +84,8 @@ var { defineClass } = require("./framework-error");
84
84
 
85
85
  var SafeIcalError = defineClass("SafeIcalError", { alwaysPermanent: true });
86
86
 
87
- // RRULE caps are enforced regardless of profile — CVE-2024-39687 has
88
- // no safe permissive posture.
87
+ // RRULE caps are enforced regardless of profile — the recursion-DoS
88
+ // class has no safe permissive posture.
89
89
  var RRULE_MAX_COUNT = 10000; // allow:raw-byte-literal — RFC 5545 §3.3.10 recurrence-count cap
90
90
  var RRULE_MAX_BY_ENTRIES = 24; // allow:raw-byte-literal — BYxxx list-length cap
91
91
 
@@ -223,7 +223,7 @@ function parse(text, opts) {
223
223
  if (byteLen > caps.maxBytes) {
224
224
  throw new SafeIcalError("safe-ical/oversize-bytes",
225
225
  "safeIcal.parse: input " + byteLen + " bytes exceeds maxBytes=" + caps.maxBytes +
226
- " (CVE-2024-39687-class defense)");
226
+ " (calendar-bomb defense)");
227
227
  }
228
228
 
229
229
  var lines = _unfold(s, caps);
@@ -466,7 +466,7 @@ function _parseComponent(lines, startIdx, ctx, depth) {
466
466
  if (depth > ctx.caps.maxNestingDepth) {
467
467
  throw new SafeIcalError("safe-ical/oversize-nesting",
468
468
  "safeIcal.parse: nesting depth exceeds maxNestingDepth=" +
469
- ctx.caps.maxNestingDepth + " (CVE-2024-39687-class defense)");
469
+ ctx.caps.maxNestingDepth + " (calendar-bomb defense)");
470
470
  }
471
471
  ctx.componentCount += 1;
472
472
  if (ctx.componentCount > ctx.caps.maxComponents) {
@@ -516,7 +516,7 @@ function _parseComponent(lines, startIdx, ctx, depth) {
516
516
  "safeIcal.parse: unknown property '" + pn +
517
517
  "' (extend via opts.extraProperties or use X- prefix)");
518
518
  }
519
- // RRULE caps — CVE-2024-39687 defense.
519
+ // RRULE caps — recursion-DoS / calendar-bomb defense.
520
520
  if (pn === "RRULE" || pn === "EXRULE") {
521
521
  _validateRrule(ln.value);
522
522
  }
@@ -558,7 +558,7 @@ function _validateRrule(value) {
558
558
  if (!isFinite(n) || n < 0 || n > RRULE_MAX_COUNT) {
559
559
  throw new SafeIcalError("safe-ical/oversize-rrule-count",
560
560
  "safeIcal.parse: RRULE COUNT=" + val + " exceeds cap=" +
561
- RRULE_MAX_COUNT + " (CVE-2024-39687 defense)");
561
+ RRULE_MAX_COUNT + " (calendar-bomb defense)");
562
562
  }
563
563
  } else if (key === "BYDAY" || key === "BYMONTH" || key === "BYMONTHDAY" ||
564
564
  key === "BYHOUR" || key === "BYMINUTE" || key === "BYSECOND" ||
@@ -567,7 +567,7 @@ function _validateRrule(value) {
567
567
  if (entries.length > RRULE_MAX_BY_ENTRIES) {
568
568
  throw new SafeIcalError("safe-ical/oversize-rrule-by",
569
569
  "safeIcal.parse: RRULE " + key + " list length " + entries.length +
570
- " exceeds cap=" + RRULE_MAX_BY_ENTRIES + " (CVE-2024-39687 defense)");
570
+ " exceeds cap=" + RRULE_MAX_BY_ENTRIES + " (calendar-bomb defense)");
571
571
  }
572
572
  }
573
573
  }
@@ -27,7 +27,7 @@
27
27
  * crypto.
28
28
  *
29
29
  * Defends `CVE-2024-39929` (Exim MIME multipart parser) and
30
- * `CVE-2025-30258` (gnumail truncated-MIME-tree class) by capping
30
+ * `CVE-2026-26312` (Stalwart nested `message/rfc822` MIME OOM) by capping
31
31
  * total parts, nesting depth, boundary length, header bytes,
32
32
  * header-line bytes, decoded body bytes, message bytes — plus
33
33
  * charset + transfer-encoding allowlists.
@@ -41,7 +41,7 @@
41
41
  * incoming message above a threshold.
42
42
  *
43
43
  * @card
44
- * Bounded MIME parser — walks RFC 5322 + 2045 / 2046 / 2047 + EAI message structure into a part tree with hard caps on depth, part count, body size, header bytes, and charset / transfer-encoding allowlists. Defends CVE-2024-39929 + CVE-2025-30258.
44
+ * Bounded MIME parser — walks RFC 5322 + 2045 / 2046 / 2047 + EAI message structure into a part tree with hard caps on depth, part count, body size, header bytes, and charset / transfer-encoding allowlists. Defends CVE-2024-39929 + CVE-2026-26312.
45
45
  */
46
46
 
47
47
  var C = require("./constants");
@@ -332,13 +332,13 @@ function _parsePart(buf, ctx, depth) {
332
332
  if (depth > ctx.maxNestingDepth) {
333
333
  throw new SafeMimeError("safe-mime/oversize-nesting",
334
334
  "safeMime.parse: nesting depth exceeded maxNestingDepth=" + ctx.maxNestingDepth +
335
- " (CVE-2024-39929-class defense)");
335
+ " (CVE-2024-39929 class defense)");
336
336
  }
337
337
  ctx.partCount += 1;
338
338
  if (ctx.partCount > ctx.maxParts) {
339
339
  throw new SafeMimeError("safe-mime/oversize-part-count",
340
340
  "safeMime.parse: total parts exceeded maxParts=" + ctx.maxParts +
341
- " (CVE-2024-39929-class defense)");
341
+ " (CVE-2024-39929 class defense)");
342
342
  }
343
343
 
344
344
  var sep = _findHeaderBodySep(buf);
@@ -668,7 +668,7 @@ function _decodeRfc2047Words(value) {
668
668
  raw = Buffer.from(text.replace(/_/g, " ").replace(/=([0-9A-Fa-f]{2})/g,
669
669
  function (__, hex) { return String.fromCharCode(parseInt(hex, 16)); }), "binary"); // allow:raw-byte-literal — parseInt radix 16, not bytes
670
670
  }
671
- // RFC 2047 §5 / CVE-2020-7244 header-injection defense — after
671
+ // RFC 2047 §5 encoded-word header-injection defense — after
672
672
  // base64 / Q-encoded decode, check the DECODED bytes for header
673
673
  // separators (CR, LF, NUL). A sender that base64-encodes
674
674
  // `\r\nBcc: attacker@x.com` would otherwise reach the consumer's
@@ -680,7 +680,7 @@ function _decodeRfc2047Words(value) {
680
680
  if (b === 0x0d /* CR */ || b === 0x0a /* LF */ || b === 0x00 /* NUL */) {
681
681
  throw new SafeMimeError("safe-mime/rfc2047-header-injection",
682
682
  "RFC 2047 encoded-word decoded to bytes containing CR/LF/NUL " +
683
- "(byte index " + bi + "); refusing per RFC 2047 §5 / CVE-2020-7244 class");
683
+ "(byte index " + bi + "); refusing per RFC 2047 §5 (encoded-word header injection)");
684
684
  }
685
685
  }
686
686
  return _decodeBufferAs(raw, charset);
@@ -619,7 +619,7 @@ function parse(script, opts) {
619
619
  * Parse-only validation — returns `{ ok, requiredCaps, issues }`
620
620
  * shape mirroring the rest of the guard family. Operator-facing
621
621
  * primitives that want a JMAP-style `SieveScript/validate` response
622
- * (RFC 9404) compose this and surface `issues` directly.
622
+ * (RFC 9661 — JMAP for Sieve Scripts) compose this and surface `issues` directly.
623
623
  *
624
624
  * @opts
625
625
  * profile: "strict" | "balanced" | "permissive",
@@ -23,7 +23,7 @@
23
23
  * - RFC 5321 §2.3.8 — line termination MUST be CRLF
24
24
  * - RFC 5321 §4.5.2 — dot-stuffing on the SMTP body
25
25
  * - RFC 5321 §4.1.1.4 — DATA command terminates with `<CRLF>.<CRLF>`
26
- * - CVE-2023-51764 / -51765 / -51766 / 2024-32178 — SMTP
26
+ * - CVE-2023-51764 / -51765 / -51766 — SMTP
27
27
  * smuggling (parsers that accept bare-LF dot-terminators).
28
28
  * The guard primitive `b.guardSmtpCommand.detectBodySmuggling`
29
29
  * owns smuggling detection; the safe-* terminator scanner
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.13.8",
3
+ "version": "0.13.13",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",