@blamejs/core 0.10.9 → 0.10.12
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 +3 -0
- package/index.js +9 -0
- package/lib/audit.js +1 -0
- package/lib/auth/dpop.js +5 -1
- package/lib/auth/fido-mds3.js +19 -3
- package/lib/auth/jwt.js +2 -1
- package/lib/auth/oauth.js +17 -5
- package/lib/auth/status-list.js +7 -1
- package/lib/crypto-hpke-pq.js +187 -0
- package/lib/crypto.js +11 -3
- package/lib/guard-list-id.js +6 -1
- package/lib/jose-jwe-experimental.js +228 -0
- package/lib/mail-server-imap.js +140 -29
- package/lib/mail-server-jmap.js +44 -4
- package/lib/mail-server-managesieve.js +72 -17
- package/lib/mail-server-pop3.js +35 -0
- package/lib/mail-server-registry.js +363 -0
- package/lib/mail-server-submission.js +33 -0
- package/lib/mcp.js +12 -2
- package/lib/network-dns.js +9 -0
- package/lib/pagination.js +2 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.jose.jwe.experimental
|
|
4
|
+
* @nav Crypto
|
|
5
|
+
* @title JOSE JWE (experimental, ML-KEM)
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* JSON Web Encryption (RFC 7516) with ML-KEM-1024 key encapsulation
|
|
9
|
+
* and XChaCha20-Poly1305 AEAD content encryption. Lives under
|
|
10
|
+
* `b.jose.jwe.experimental` because the JOSE PQC IANA codepoint
|
|
11
|
+
* registration (draft-ietf-jose-pqc-kem-05) hasn't finalized — the
|
|
12
|
+
* namespace name is the contract: codepoints may change between
|
|
13
|
+
* minors without the framework's stable surface being affected.
|
|
14
|
+
*
|
|
15
|
+
* When the JOSE WG closes the draft and IANA registers final
|
|
16
|
+
* codepoints, the same primitives graduate to `b.jose.jwe` (or a
|
|
17
|
+
* stable equivalent) with explicit deprecation of the experimental
|
|
18
|
+
* namespace and a one-minor migration window per the framework's
|
|
19
|
+
* stable-upgrade-policy rule.
|
|
20
|
+
*
|
|
21
|
+
* Compact serialization only — no JSON serialization variant
|
|
22
|
+
* (saves wire-format complexity at this experimental tier;
|
|
23
|
+
* operators that need JWE-JSON wait for the stable surface).
|
|
24
|
+
*
|
|
25
|
+
* @card
|
|
26
|
+
* Experimental JWE with ML-KEM-1024 KEM + XChaCha20-Poly1305 content encryption. Codepoints follow draft-ietf-jose-pqc-kem (may change before IANA registration).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
var bCrypto = require("./crypto");
|
|
30
|
+
var canonicalJson = require("./canonical-json");
|
|
31
|
+
var { defineClass } = require("./framework-error");
|
|
32
|
+
var audit = require("./audit");
|
|
33
|
+
|
|
34
|
+
var JoseJweExperimentalError = defineClass("JoseJweExperimentalError", { alwaysPermanent: true });
|
|
35
|
+
|
|
36
|
+
// Active draft (as of 2026-05-17). When IANA finalizes the codepoint
|
|
37
|
+
// the framework graduates the alg name to its stable form and ships a
|
|
38
|
+
// one-minor deprecation window via the existing `deprecate()` chain.
|
|
39
|
+
var EXPERIMENTAL_ALG = "ML-KEM-1024";
|
|
40
|
+
var EXPERIMENTAL_ENC = "XC20P"; // XChaCha20-Poly1305 per draft-irtf-cfrg-xchacha; aligns with framework PQC-first defaults
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @primitive b.jose.jwe.experimental.encrypt
|
|
44
|
+
* @signature b.jose.jwe.experimental.encrypt(plaintext, recipientPublicKeyPem, opts?)
|
|
45
|
+
* @since 0.10.10
|
|
46
|
+
* @status experimental
|
|
47
|
+
* @related b.jose.jwe.experimental.decrypt, b.crypto.encrypt
|
|
48
|
+
*
|
|
49
|
+
* Encrypt a payload under the recipient's ML-KEM-1024 public key.
|
|
50
|
+
* Returns the JWE compact serialization
|
|
51
|
+
* `<header>.<encrypted_key>.<iv>.<ciphertext>.<tag>` (base64url
|
|
52
|
+
* segments) per RFC 7516 §3.1 with experimental PQC codepoints.
|
|
53
|
+
*
|
|
54
|
+
* Header includes `{ alg: "ML-KEM-1024", enc: "XC20P", typ: "JWE",
|
|
55
|
+
* "x-blamejs-experimental": true }` — operators that scrape JWE
|
|
56
|
+
* envelopes can refuse the experimental marker until the codepoint
|
|
57
|
+
* stabilises.
|
|
58
|
+
*
|
|
59
|
+
* @opts
|
|
60
|
+
* typ: string, // optional JWE "typ" header
|
|
61
|
+
* contentType: string, // optional JWE "cty" header
|
|
62
|
+
* audit: boolean, // default true; emit audit event on encrypt
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* var pair = b.crypto.generateEncryptionKeyPair();
|
|
66
|
+
* var jwe = b.jose.jwe.experimental.encrypt("hello", pair.mlkem.publicKey);
|
|
67
|
+
* typeof jwe; // → "string" (compact form)
|
|
68
|
+
*/
|
|
69
|
+
function encrypt(plaintext, recipientPublicKeyPem, opts) {
|
|
70
|
+
opts = opts || {};
|
|
71
|
+
if (!(plaintext instanceof Buffer)) {
|
|
72
|
+
if (typeof plaintext === "string") plaintext = Buffer.from(plaintext, "utf8");
|
|
73
|
+
else {
|
|
74
|
+
throw new JoseJweExperimentalError("jose-jwe-exp/bad-plaintext",
|
|
75
|
+
"encrypt: plaintext must be a Buffer or string");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (typeof recipientPublicKeyPem !== "string" || recipientPublicKeyPem.length === 0) {
|
|
79
|
+
throw new JoseJweExperimentalError("jose-jwe-exp/bad-key",
|
|
80
|
+
"encrypt: recipientPublicKeyPem must be a non-empty PEM string");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
var header = {
|
|
84
|
+
alg: EXPERIMENTAL_ALG,
|
|
85
|
+
enc: EXPERIMENTAL_ENC,
|
|
86
|
+
typ: opts.typ || "JWE",
|
|
87
|
+
"x-blamejs-experimental": true,
|
|
88
|
+
};
|
|
89
|
+
if (typeof opts.contentType === "string" && opts.contentType.length > 0) {
|
|
90
|
+
header.cty = opts.contentType;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Encapsulate under the recipient's ML-KEM-1024 public key. The
|
|
94
|
+
// framework's `encrypt(plaintext, pemString)` form selects the
|
|
95
|
+
// ML-KEM-only path and returns a base64 envelope string.
|
|
96
|
+
//
|
|
97
|
+
// The experimental JWE serialization carries the framework envelope
|
|
98
|
+
// as a single base64url segment (`ciphertext` slot). The empty
|
|
99
|
+
// `encrypted_key` / `iv` / `tag` segments are valid under RFC 7516
|
|
100
|
+
// §3.1 (compact serialization permits empty Base64URL parts when
|
|
101
|
+
// the cryptographic primitives are AEAD-with-direct-key — the
|
|
102
|
+
// framework envelope is self-contained). Operators that need the
|
|
103
|
+
// segmented shape with each field carved out wait for the
|
|
104
|
+
// post-IANA stable surface where layout is contract.
|
|
105
|
+
var fwEnvelopeB64 = bCrypto.encrypt(plaintext, recipientPublicKeyPem);
|
|
106
|
+
var fwEnvelopeUrl = bCrypto.toBase64Url(Buffer.from(fwEnvelopeB64, "base64"));
|
|
107
|
+
var headerB64 = bCrypto.toBase64Url(Buffer.from(canonicalJson.stringify(header), "utf8"));
|
|
108
|
+
// RFC 7516 §3.1 compact-serialization slots: header / encrypted_key
|
|
109
|
+
// / iv / ciphertext / tag. The framework envelope (a self-contained
|
|
110
|
+
// AEAD output) lives in the `ciphertext` slot (index 3). Other slots
|
|
111
|
+
// are empty under this experimental shape.
|
|
112
|
+
var compact = headerB64 + "..." + fwEnvelopeUrl + ".";
|
|
113
|
+
if (opts.audit !== false) {
|
|
114
|
+
audit.safeEmit({
|
|
115
|
+
action: "jose.jwe.experimental.encrypt",
|
|
116
|
+
outcome: "success",
|
|
117
|
+
metadata: { alg: EXPERIMENTAL_ALG, enc: EXPERIMENTAL_ENC, ptLen: plaintext.length },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return compact;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @primitive b.jose.jwe.experimental.decrypt
|
|
125
|
+
* @signature b.jose.jwe.experimental.decrypt(compact, recipientPrivateKeyPem, opts?)
|
|
126
|
+
* @since 0.10.10
|
|
127
|
+
* @status experimental
|
|
128
|
+
* @related b.jose.jwe.experimental.encrypt
|
|
129
|
+
*
|
|
130
|
+
* Decrypt a compact-serialization JWE produced by the experimental
|
|
131
|
+
* `encrypt` path. Returns the plaintext Buffer. Refuses on alg / enc
|
|
132
|
+
* mismatch, missing experimental marker, or any cryptographic verify
|
|
133
|
+
* failure. Never throws on adversarial input — typed
|
|
134
|
+
* `JoseJweExperimentalError` with a coded refusal.
|
|
135
|
+
*
|
|
136
|
+
* @opts
|
|
137
|
+
* audit: boolean, // default true
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* var plaintext = b.jose.jwe.experimental.decrypt(jwe, pair.mlkem.privateKey);
|
|
141
|
+
* plaintext.toString("utf8"); // → "hello"
|
|
142
|
+
*/
|
|
143
|
+
function decrypt(compact, recipientPrivateKeyPem, opts) {
|
|
144
|
+
opts = opts || {};
|
|
145
|
+
if (typeof compact !== "string" || compact.length === 0) {
|
|
146
|
+
throw new JoseJweExperimentalError("jose-jwe-exp/bad-compact",
|
|
147
|
+
"decrypt: compact must be a non-empty string");
|
|
148
|
+
}
|
|
149
|
+
if (typeof recipientPrivateKeyPem !== "string" || recipientPrivateKeyPem.length === 0) {
|
|
150
|
+
throw new JoseJweExperimentalError("jose-jwe-exp/bad-key",
|
|
151
|
+
"decrypt: recipientPrivateKeyPem must be a non-empty PEM string");
|
|
152
|
+
}
|
|
153
|
+
var parts = compact.split(".");
|
|
154
|
+
if (parts.length !== 5) { // allow:raw-byte-literal — JWE compact serialization is 5 dot-separated segments (RFC 7516 §3.1)
|
|
155
|
+
throw new JoseJweExperimentalError("jose-jwe-exp/bad-format",
|
|
156
|
+
"decrypt: JWE compact serialization MUST have 5 segments (RFC 7516 §3.1), got " + parts.length);
|
|
157
|
+
}
|
|
158
|
+
var header;
|
|
159
|
+
// Header is base64url-decoded; route through safeJson.parse for
|
|
160
|
+
// proto-pollution + depth + size defenses (operator-supplied compact
|
|
161
|
+
// bytes are adversarial). Both the base64url decode AND the JSON
|
|
162
|
+
// parse live inside the same typed try/catch so a malformed header
|
|
163
|
+
// surfaces as the typed `jose-jwe-exp/bad-header` refusal class
|
|
164
|
+
// rather than a raw TypeError leaking from b.crypto.fromBase64Url.
|
|
165
|
+
var headerBytes;
|
|
166
|
+
try { headerBytes = bCrypto.fromBase64Url(parts[0]); }
|
|
167
|
+
catch (_eb) {
|
|
168
|
+
throw new JoseJweExperimentalError("jose-jwe-exp/bad-header",
|
|
169
|
+
"decrypt: protected header is not valid base64url");
|
|
170
|
+
}
|
|
171
|
+
if (headerBytes.length > 4096) { // allow:raw-byte-literal — JWE header byte cap, not bytes-as-storage
|
|
172
|
+
throw new JoseJweExperimentalError("jose-jwe-exp/header-too-large",
|
|
173
|
+
"decrypt: protected header exceeds 4 KiB cap");
|
|
174
|
+
}
|
|
175
|
+
try { header = require("./safe-json").parse(headerBytes.toString("utf8")); } // allow:inline-require — safe-json only needed on the rare decrypt path
|
|
176
|
+
catch (_e) {
|
|
177
|
+
throw new JoseJweExperimentalError("jose-jwe-exp/bad-header",
|
|
178
|
+
"decrypt: protected header is not base64url-encoded JSON");
|
|
179
|
+
}
|
|
180
|
+
if (header.alg !== EXPERIMENTAL_ALG) {
|
|
181
|
+
throw new JoseJweExperimentalError("jose-jwe-exp/alg-mismatch",
|
|
182
|
+
"decrypt: alg '" + header.alg + "' is not '" + EXPERIMENTAL_ALG + "'");
|
|
183
|
+
}
|
|
184
|
+
if (header.enc !== EXPERIMENTAL_ENC) {
|
|
185
|
+
throw new JoseJweExperimentalError("jose-jwe-exp/enc-mismatch",
|
|
186
|
+
"decrypt: enc '" + header.enc + "' is not '" + EXPERIMENTAL_ENC + "'");
|
|
187
|
+
}
|
|
188
|
+
if (header["x-blamejs-experimental"] !== true) {
|
|
189
|
+
throw new JoseJweExperimentalError("jose-jwe-exp/missing-experimental-marker",
|
|
190
|
+
"decrypt: header missing `x-blamejs-experimental: true` — refuse to decrypt unmarked envelopes");
|
|
191
|
+
}
|
|
192
|
+
// Per the experimental serialization (see `encrypt`), the framework
|
|
193
|
+
// envelope lives in segment 3. Other segments must be empty.
|
|
194
|
+
if (parts[1].length > 0 || parts[2].length > 0 || parts[4].length > 0) {
|
|
195
|
+
throw new JoseJweExperimentalError("jose-jwe-exp/bad-format",
|
|
196
|
+
"decrypt: experimental JWE shape requires empty encrypted_key / iv / tag segments");
|
|
197
|
+
}
|
|
198
|
+
var fwEnvelopeBuf;
|
|
199
|
+
try { fwEnvelopeBuf = bCrypto.fromBase64Url(parts[3]); }
|
|
200
|
+
catch (_eb2) {
|
|
201
|
+
throw new JoseJweExperimentalError("jose-jwe-exp/bad-format",
|
|
202
|
+
"decrypt: ciphertext segment is not valid base64url");
|
|
203
|
+
}
|
|
204
|
+
var fwEnvelopeB64 = fwEnvelopeBuf.toString("base64");
|
|
205
|
+
// `raw: true` keeps the decrypted plaintext as a Buffer rather than
|
|
206
|
+
// utf8-decoding it (which would corrupt binary payloads — `0xff`
|
|
207
|
+
// becomes the Unicode replacement character). The JWE primitives
|
|
208
|
+
// document Buffer-in / Buffer-out so the contract holds across
|
|
209
|
+
// arbitrary plaintext shapes (signed-blob carriers, binary tokens).
|
|
210
|
+
var plaintext = bCrypto.decrypt(fwEnvelopeB64,
|
|
211
|
+
{ privateKey: recipientPrivateKeyPem }, { raw: true });
|
|
212
|
+
if (opts.audit !== false) {
|
|
213
|
+
audit.safeEmit({
|
|
214
|
+
action: "jose.jwe.experimental.decrypt",
|
|
215
|
+
outcome: "success",
|
|
216
|
+
metadata: { alg: EXPERIMENTAL_ALG, enc: EXPERIMENTAL_ENC },
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return plaintext;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = {
|
|
223
|
+
encrypt: encrypt,
|
|
224
|
+
decrypt: decrypt,
|
|
225
|
+
EXPERIMENTAL_ALG: EXPERIMENTAL_ALG,
|
|
226
|
+
EXPERIMENTAL_ENC: EXPERIMENTAL_ENC,
|
|
227
|
+
JoseJweExperimentalError: JoseJweExperimentalError,
|
|
228
|
+
};
|
package/lib/mail-server-imap.js
CHANGED
|
@@ -124,6 +124,7 @@ var numericBounds = require("./numeric-bounds");
|
|
|
124
124
|
var validateOpts = require("./validate-opts");
|
|
125
125
|
var guardImapCommand = require("./guard-imap-command");
|
|
126
126
|
var mailServerRateLimit = require("./mail-server-rate-limit");
|
|
127
|
+
var mailServerRegistry = require("./mail-server-registry");
|
|
127
128
|
var mailServerTls = require("./mail-server-tls");
|
|
128
129
|
var { defineClass } = require("./framework-error");
|
|
129
130
|
|
|
@@ -491,37 +492,147 @@ function create(opts) {
|
|
|
491
492
|
_dispatch(state, socket, parsed, lineNoLit, pending.body);
|
|
492
493
|
}
|
|
493
494
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
495
|
+
// Adapter shim — uniform `(state, socket, parsed, literalBody)`
|
|
496
|
+
// dispatch contract over the per-verb handlers. Builds the registry
|
|
497
|
+
// defaults lazily on first dispatch so the closure-scoped handler
|
|
498
|
+
// references are bound when needed (handlers are hoisted by their
|
|
499
|
+
// function-declarations; the registry init runs at dispatch time).
|
|
500
|
+
var _registry = null;
|
|
501
|
+
function _ensureRegistry() {
|
|
502
|
+
if (_registry !== null) return _registry;
|
|
503
|
+
// Per-handler resource budgets. Sized per the verb's known
|
|
504
|
+
// payload shape (LIST scans the folder tree; FETCH walks N
|
|
505
|
+
// messages; APPEND accepts a literal up to maxLiteralBytes).
|
|
506
|
+
var SHORT_MS = 5 * 1000; // allow:raw-time-literal — 5s short-command budget
|
|
507
|
+
var MEDIUM_MS = 30 * 1000; // allow:raw-time-literal — 30s medium-command budget
|
|
508
|
+
var LONG_MS = 2 * 60 * 1000; // allow:raw-time-literal — 2 min long-command budget (FETCH / APPEND)
|
|
509
|
+
var SHORT_B = 8 * 1024; // allow:raw-byte-literal — 8 KiB short-command response cap
|
|
510
|
+
var MEDIUM_B = 1024 * 1024; // allow:raw-byte-literal — 1 MiB medium-command response cap
|
|
511
|
+
var LONG_B = 64 * 1024 * 1024; // allow:raw-byte-literal — 64 MiB FETCH/APPEND response cap
|
|
512
|
+
var defaults = {
|
|
513
|
+
CAPABILITY: { fn: function (s, so, p) { return _handleCapability(s, so, p.tag); },
|
|
514
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
515
|
+
NOOP: { fn: function (s, so, p) { return _writeTagged(so, p.tag, "OK NOOP completed"); },
|
|
516
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
517
|
+
LOGOUT: { fn: function (s, so, p) { return _handleLogout(s, so, p.tag); },
|
|
518
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
519
|
+
ID: { fn: function (s, so, p) { return _handleId(s, so, p.tag, p.args); },
|
|
520
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
521
|
+
STARTTLS: { fn: function (s, so, p) { return _handleStartTls(s, so, p.tag); },
|
|
522
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
523
|
+
AUTHENTICATE: { fn: function (s, so, p) { return _handleAuthenticate(s, so, p.tag, p.args); },
|
|
524
|
+
maxHandlerBytes: MEDIUM_B, maxHandlerMs: MEDIUM_MS },
|
|
525
|
+
LOGIN: { fn: function (s, so, p) { return _handleLogin(s, so, p.tag, p.args); },
|
|
526
|
+
maxHandlerBytes: MEDIUM_B, maxHandlerMs: MEDIUM_MS },
|
|
527
|
+
ENABLE: { fn: function (s, so, p) { return _writeTagged(so, p.tag, "OK ENABLED"); },
|
|
528
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
529
|
+
SELECT: { fn: function (s, so, p) { return _handleSelect(s, so, p.tag, p.args, false); },
|
|
530
|
+
maxHandlerBytes: MEDIUM_B, maxHandlerMs: MEDIUM_MS },
|
|
531
|
+
EXAMINE: { fn: function (s, so, p) { return _handleSelect(s, so, p.tag, p.args, true); },
|
|
532
|
+
maxHandlerBytes: MEDIUM_B, maxHandlerMs: MEDIUM_MS },
|
|
533
|
+
LIST: { fn: function (s, so, p) { return _handleList(s, so, p.tag, p.args); },
|
|
534
|
+
maxHandlerBytes: MEDIUM_B, maxHandlerMs: MEDIUM_MS },
|
|
535
|
+
STATUS: { fn: function (s, so, p) { return _handleStatus(s, so, p.tag, p.args); },
|
|
536
|
+
maxHandlerBytes: MEDIUM_B, maxHandlerMs: MEDIUM_MS },
|
|
537
|
+
NAMESPACE: { fn: function (s, so, p) {
|
|
538
|
+
_writeUntagged(so, "NAMESPACE ((\"\" \"/\")) NIL NIL");
|
|
539
|
+
return _writeTagged(so, p.tag, "OK NAMESPACE completed");
|
|
540
|
+
},
|
|
541
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
542
|
+
APPEND: { fn: function (s, so, p, lit) { return _handleAppend(s, so, p.tag, p.args, lit); },
|
|
543
|
+
maxHandlerBytes: LONG_B, maxHandlerMs: LONG_MS },
|
|
544
|
+
CHECK: { fn: function (s, so, p) { return _writeTagged(so, p.tag, "OK CHECK completed"); },
|
|
545
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
546
|
+
CLOSE: { fn: function (s, so, p) { return _handleClose(s, so, p.tag); },
|
|
547
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
548
|
+
UNSELECT: { fn: function (s, so, p) { return _handleClose(s, so, p.tag); },
|
|
549
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
550
|
+
EXPUNGE: { fn: function (s, so, p) { return _handleExpunge(s, so, p.tag); },
|
|
551
|
+
maxHandlerBytes: MEDIUM_B, maxHandlerMs: MEDIUM_MS },
|
|
552
|
+
FETCH: { fn: function (s, so, p) { return _handleFetch(s, so, p.tag, p.args); },
|
|
553
|
+
maxHandlerBytes: LONG_B, maxHandlerMs: LONG_MS },
|
|
554
|
+
STORE: { fn: function (s, so, p) { return _handleStore(s, so, p.tag, p.args); },
|
|
555
|
+
maxHandlerBytes: MEDIUM_B, maxHandlerMs: MEDIUM_MS },
|
|
556
|
+
UID: { fn: function (s, so, p) { return _handleUid(s, so, p.tag, p.args); },
|
|
557
|
+
maxHandlerBytes: LONG_B, maxHandlerMs: LONG_MS },
|
|
558
|
+
IDLE: { fn: function (s, so, p) { return _handleIdle(s, so, p.tag); },
|
|
559
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: LONG_MS },
|
|
560
|
+
DONE: { fn: function (s, so, p) { return _writeTagged(so, p.tag, "BAD DONE outside IDLE"); },
|
|
561
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
562
|
+
// Defaults for the verbs the v0.9.49 listener didn't dispatch —
|
|
563
|
+
// operators wire concrete handlers via opts.overrides.
|
|
564
|
+
SEARCH: { fn: function (s, so, p) { return _writeTagged(so, p.tag, "NO SEARCH not configured"); },
|
|
565
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
566
|
+
CREATE: { fn: function (s, so, p) { return _writeTagged(so, p.tag, "NO CREATE not configured"); },
|
|
567
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
568
|
+
DELETE: { fn: function (s, so, p) { return _writeTagged(so, p.tag, "NO DELETE not configured"); },
|
|
569
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
570
|
+
RENAME: { fn: function (s, so, p) { return _writeTagged(so, p.tag, "NO RENAME not configured"); },
|
|
571
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
572
|
+
SUBSCRIBE: { fn: function (s, so, p) { return _writeTagged(so, p.tag, "NO SUBSCRIBE not configured"); },
|
|
573
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
574
|
+
UNSUBSCRIBE: { fn: function (s, so, p) { return _writeTagged(so, p.tag, "NO UNSUBSCRIBE not configured"); },
|
|
575
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
576
|
+
COPY: { fn: function (s, so, p) { return _writeTagged(so, p.tag, "NO COPY not configured"); },
|
|
577
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
578
|
+
MOVE: { fn: function (s, so, p) { return _writeTagged(so, p.tag, "NO MOVE not configured"); },
|
|
579
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
580
|
+
};
|
|
581
|
+
_registry = mailServerRegistry.create({
|
|
582
|
+
protocol: "imap",
|
|
583
|
+
defaults: defaults,
|
|
584
|
+
overrides: opts.overrides || {},
|
|
585
|
+
// b.agent.tenant adoption (v0.10.12). Operators wiring multi-
|
|
586
|
+
// tenant IMAP deployments pass `tenantScope` from
|
|
587
|
+
// `b.agent.tenant.create({...})` plus the per-listener tenant id.
|
|
588
|
+
// The registry then gates every dispatch on
|
|
589
|
+
// `tenantScope.check(state.actor, agentTenantId)` before guard
|
|
590
|
+
// validation or audit emission.
|
|
591
|
+
tenantScope: opts.tenantScope || null,
|
|
592
|
+
agentTenantId: opts.agentTenantId || null,
|
|
593
|
+
notFoundHandler: function (verb, _state, socket, parsed) {
|
|
594
|
+
return _writeTagged(socket, parsed.tag,
|
|
595
|
+
"BAD Verb '" + verb + "' not implemented in v1");
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
return _registry;
|
|
599
|
+
}
|
|
498
600
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
601
|
+
function _dispatch(state, socket, parsed, _rawLine, literalBody) {
|
|
602
|
+
// Registry dispatch may return a Promise (async override handler,
|
|
603
|
+
// or a safeAsync.withTimeout-wrapped Promise). The caller's
|
|
604
|
+
// try/catch is synchronous, so a Promise rejection would surface
|
|
605
|
+
// as an unhandled rejection AND the client would never receive
|
|
606
|
+
// the tagged error reply. Attach a catch that converts the
|
|
607
|
+
// rejection into a `BAD`/`NO` tagged response + audit emit.
|
|
608
|
+
var result;
|
|
609
|
+
try {
|
|
610
|
+
result = _ensureRegistry().dispatch(parsed.verb, state, socket, parsed, literalBody);
|
|
611
|
+
} catch (err) {
|
|
612
|
+
_writeTagged(socket, parsed.tag,
|
|
613
|
+
"NO " + ((err && err.message) || "handler threw").slice(0, ERR_CLAMP));
|
|
614
|
+
_emit("mail.server.imap.handler_threw",
|
|
615
|
+
{ connectionId: state.id, verb: parsed.verb,
|
|
616
|
+
error: (err && err.message) || String(err) }, "failure");
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (result && typeof result.then === "function") {
|
|
620
|
+
result.then(
|
|
621
|
+
function () { /* tagged response already written by handler */ },
|
|
622
|
+
function (err) {
|
|
623
|
+
try {
|
|
624
|
+
_writeTagged(socket, parsed.tag,
|
|
625
|
+
"NO " + ((err && err.message) || "handler rejected").slice(0, ERR_CLAMP));
|
|
626
|
+
} catch (_we) { /* socket may already be gone */ }
|
|
627
|
+
try {
|
|
628
|
+
_emit("mail.server.imap.handler_rejected",
|
|
629
|
+
{ connectionId: state.id, verb: parsed.verb,
|
|
630
|
+
error: (err && err.message) || String(err) }, "failure");
|
|
631
|
+
} catch (_ae) { /* drop-silent */ }
|
|
632
|
+
}
|
|
633
|
+
);
|
|
524
634
|
}
|
|
635
|
+
return result;
|
|
525
636
|
}
|
|
526
637
|
|
|
527
638
|
function _capabilityLine(state) {
|
package/lib/mail-server-jmap.js
CHANGED
|
@@ -123,6 +123,7 @@ var bCrypto = require("./crypto");
|
|
|
123
123
|
var safeJson = require("./safe-json");
|
|
124
124
|
var validateOpts = require("./validate-opts");
|
|
125
125
|
var guardJmap = require("./guard-jmap");
|
|
126
|
+
var mailServerRegistry = require("./mail-server-registry");
|
|
126
127
|
var { defineClass } = require("./framework-error");
|
|
127
128
|
|
|
128
129
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
@@ -195,7 +196,40 @@ function create(opts) {
|
|
|
195
196
|
var profile = opts.profile || DEFAULT_PROFILE;
|
|
196
197
|
var posture = opts.posture || null;
|
|
197
198
|
var serverCapabilities = opts.serverCapabilities || {};
|
|
198
|
-
|
|
199
|
+
|
|
200
|
+
// JMAP method registry. Wrap operator-supplied `opts.methods` map
|
|
201
|
+
// through `b.mail.serverRegistry` so per-handler resource budgets
|
|
202
|
+
// (maxHandlerBytes / maxHandlerMs) apply uniformly across the IMAP
|
|
203
|
+
// / JMAP / ManageSieve listeners. Legacy `opts.methods` callers get
|
|
204
|
+
// an auto-default budget (10 MiB / 30s) with a one-time deprecation
|
|
205
|
+
// audit event per process; new callers use `opts.overrides` with
|
|
206
|
+
// explicit budgets per the stricter-mode register contract.
|
|
207
|
+
var LEGACY_JMAP_BYTES = 10 * 1024 * 1024; // allow:raw-byte-literal — 10 MiB legacy auto-budget for JMAP methods
|
|
208
|
+
var LEGACY_JMAP_MS = 30 * 1000; // allow:raw-time-literal — 30s legacy auto-budget
|
|
209
|
+
var _legacyDeprecationEmitted = false;
|
|
210
|
+
var defaults = {};
|
|
211
|
+
var methodNames = Object.keys(opts.methods);
|
|
212
|
+
for (var mi = 0; mi < methodNames.length; mi += 1) {
|
|
213
|
+
var mname = methodNames[mi];
|
|
214
|
+
if (typeof opts.methods[mname] !== "function") continue;
|
|
215
|
+
defaults[mname] = {
|
|
216
|
+
fn: opts.methods[mname],
|
|
217
|
+
maxHandlerBytes: LEGACY_JMAP_BYTES,
|
|
218
|
+
maxHandlerMs: LEGACY_JMAP_MS,
|
|
219
|
+
allowExperimental: true, // legacy callers wired anything; preserve the openness
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
var registry = mailServerRegistry.create({
|
|
223
|
+
protocol: "jmap",
|
|
224
|
+
defaults: defaults,
|
|
225
|
+
overrides: opts.overrides || {},
|
|
226
|
+
// b.agent.tenant adoption (v0.10.12). When `opts.tenantScope` is
|
|
227
|
+
// supplied, every method dispatch first gates on
|
|
228
|
+
// `tenantScope.check(state.actor, agentTenantId)` — JMAP's
|
|
229
|
+
// accountId scoping continues to apply inside operator handlers.
|
|
230
|
+
tenantScope: opts.tenantScope || null,
|
|
231
|
+
agentTenantId: opts.agentTenantId || null,
|
|
232
|
+
});
|
|
199
233
|
var sessionState = bCrypto.generateToken(16); // allow:raw-byte-literal — opaque session-state token length
|
|
200
234
|
|
|
201
235
|
function _emit(action, metadata, outcome) {
|
|
@@ -321,18 +355,24 @@ function create(opts) {
|
|
|
321
355
|
methodResponses.push(["error", { type: refType, description: (e && e.message) || "" }, clientId]);
|
|
322
356
|
continue;
|
|
323
357
|
}
|
|
324
|
-
|
|
325
|
-
if (typeof handler !== "function") {
|
|
358
|
+
if (!registry.has(methodName)) {
|
|
326
359
|
methodResponses.push(["error",
|
|
327
360
|
{ type: "urn:ietf:params:jmap:error:unknownMethod",
|
|
328
361
|
description: "Method '" + methodName + "' not implemented on this server" }, clientId]);
|
|
329
362
|
continue;
|
|
330
363
|
}
|
|
364
|
+
if (!_legacyDeprecationEmitted && registry.source(methodName) === "builtin") {
|
|
365
|
+
_legacyDeprecationEmitted = true;
|
|
366
|
+
_emit("mail.server.jmap.methods_opt_deprecated",
|
|
367
|
+
{ note: "opts.methods is shimmed through b.mail.serverRegistry with auto-budget; " +
|
|
368
|
+
"future minor will require opts.overrides with explicit budgets" },
|
|
369
|
+
"warning");
|
|
370
|
+
}
|
|
331
371
|
try {
|
|
332
372
|
// JMAP methodCalls execute sequentially by spec (RFC 8620 §3.7 —
|
|
333
373
|
// back-references require strict ordering). The await-in-loop
|
|
334
374
|
// pattern is intentional here.
|
|
335
|
-
var result = await
|
|
375
|
+
var result = await registry.dispatch(methodName, actor, resolvedArgs, {
|
|
336
376
|
using: parsed.using,
|
|
337
377
|
createdIds: parsed.createdIds,
|
|
338
378
|
methodName: methodName,
|
|
@@ -136,6 +136,7 @@ var validateOpts = require("./validate-opts");
|
|
|
136
136
|
var guardManageSieveCommand = require("./guard-managesieve-command");
|
|
137
137
|
var safeSieve = require("./safe-sieve");
|
|
138
138
|
var mailServerRateLimit = require("./mail-server-rate-limit");
|
|
139
|
+
var mailServerRegistry = require("./mail-server-registry");
|
|
139
140
|
var { defineClass } = require("./framework-error");
|
|
140
141
|
|
|
141
142
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
@@ -378,7 +379,26 @@ function create(opts) {
|
|
|
378
379
|
return;
|
|
379
380
|
}
|
|
380
381
|
try {
|
|
381
|
-
_dispatch(state, socket, parsed);
|
|
382
|
+
var result = _dispatch(state, socket, parsed);
|
|
383
|
+
// Registry dispatch may return a Promise (async override handler
|
|
384
|
+
// or safeAsync.withTimeout-wrapped Promise). The synchronous
|
|
385
|
+
// try/catch above only catches throw-during-dispatch; Promise
|
|
386
|
+
// rejections need an attached catch to avoid unhandled-rejection
|
|
387
|
+
// termination + missing NO reply.
|
|
388
|
+
if (result && typeof result.then === "function") {
|
|
389
|
+
result.then(
|
|
390
|
+
function () { /* OK reply already written by handler */ },
|
|
391
|
+
function (e) {
|
|
392
|
+
try {
|
|
393
|
+
_emit("mail.server.managesieve.handler_rejected",
|
|
394
|
+
{ connectionId: state.id, verb: parsed && parsed.verb,
|
|
395
|
+
error: (e && e.message) || String(e) }, "failure");
|
|
396
|
+
} catch (_ae) { /* drop-silent */ }
|
|
397
|
+
try { _writeNo(socket, "Internal error"); }
|
|
398
|
+
catch (_we) { /* socket may already be gone */ }
|
|
399
|
+
}
|
|
400
|
+
);
|
|
401
|
+
}
|
|
382
402
|
} catch (e) {
|
|
383
403
|
_emit("mail.server.managesieve.handler_threw",
|
|
384
404
|
{ connectionId: state.id, verb: parsed && parsed.verb,
|
|
@@ -387,23 +407,58 @@ function create(opts) {
|
|
|
387
407
|
}
|
|
388
408
|
}
|
|
389
409
|
|
|
410
|
+
var _registry = null;
|
|
411
|
+
function _ensureRegistry() {
|
|
412
|
+
if (_registry !== null) return _registry;
|
|
413
|
+
var SHORT_MS = 5 * 1000; // allow:raw-time-literal — 5s short-command budget
|
|
414
|
+
var MEDIUM_MS = 30 * 1000; // allow:raw-time-literal — 30s medium-command budget
|
|
415
|
+
var LONG_MS = 2 * 60 * 1000; // allow:raw-time-literal — 2 min PUTSCRIPT / GETSCRIPT budget
|
|
416
|
+
var SHORT_B = 8 * 1024; // allow:raw-byte-literal — 8 KiB short-command cap
|
|
417
|
+
var MEDIUM_B = 1024 * 1024; // allow:raw-byte-literal — 1 MiB medium-command cap
|
|
418
|
+
var LONG_B = 16 * 1024 * 1024; // allow:raw-byte-literal — 16 MiB PUTSCRIPT cap
|
|
419
|
+
var defaults = {
|
|
420
|
+
CAPABILITY: { fn: function (s, so) { return _handleCapability(s, so); },
|
|
421
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
422
|
+
NOOP: { fn: function (s, so, p) { return _handleNoop(s, so, p); },
|
|
423
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
424
|
+
STARTTLS: { fn: function (s, so) { return _handleStartTls(s, so); },
|
|
425
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
426
|
+
LOGOUT: { fn: function (s, so) { return _handleLogout(s, so); },
|
|
427
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
428
|
+
AUTHENTICATE: { fn: function (s, so, p) { return _handleAuthenticate(s, so, p); },
|
|
429
|
+
maxHandlerBytes: MEDIUM_B, maxHandlerMs: MEDIUM_MS },
|
|
430
|
+
HAVESPACE: { fn: function (s, so, p) { return _handleHaveSpace(s, so, p); },
|
|
431
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
432
|
+
PUTSCRIPT: { fn: function (s, so, p) { return _handlePutScript(s, so, p); },
|
|
433
|
+
maxHandlerBytes: LONG_B, maxHandlerMs: LONG_MS },
|
|
434
|
+
LISTSCRIPTS: { fn: function (s, so) { return _handleListScripts(s, so); },
|
|
435
|
+
maxHandlerBytes: MEDIUM_B, maxHandlerMs: MEDIUM_MS },
|
|
436
|
+
SETACTIVE: { fn: function (s, so, p) { return _handleSetActive(s, so, p); },
|
|
437
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
438
|
+
GETSCRIPT: { fn: function (s, so, p) { return _handleGetScript(s, so, p); },
|
|
439
|
+
maxHandlerBytes: LONG_B, maxHandlerMs: LONG_MS },
|
|
440
|
+
DELETESCRIPT: { fn: function (s, so, p) { return _handleDeleteScript(s, so, p); },
|
|
441
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
442
|
+
RENAMESCRIPT: { fn: function (s, so, p) { return _handleRenameScript(s, so, p); },
|
|
443
|
+
maxHandlerBytes: SHORT_B, maxHandlerMs: SHORT_MS },
|
|
444
|
+
};
|
|
445
|
+
_registry = mailServerRegistry.create({
|
|
446
|
+
protocol: "managesieve",
|
|
447
|
+
defaults: defaults,
|
|
448
|
+
overrides: opts.overrides || {},
|
|
449
|
+
// b.agent.tenant adoption (v0.10.12) — see imap factory for the
|
|
450
|
+
// shape.
|
|
451
|
+
tenantScope: opts.tenantScope || null,
|
|
452
|
+
agentTenantId: opts.agentTenantId || null,
|
|
453
|
+
notFoundHandler: function (verb, _state, socket) {
|
|
454
|
+
return _writeNo(socket, "Unknown verb '" + verb + "'");
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
return _registry;
|
|
458
|
+
}
|
|
459
|
+
|
|
390
460
|
function _dispatch(state, socket, parsed) {
|
|
391
|
-
|
|
392
|
-
switch (verb) {
|
|
393
|
-
case "CAPABILITY": return _handleCapability(state, socket);
|
|
394
|
-
case "NOOP": return _handleNoop(state, socket, parsed);
|
|
395
|
-
case "STARTTLS": return _handleStartTls(state, socket);
|
|
396
|
-
case "LOGOUT": return _handleLogout(state, socket);
|
|
397
|
-
case "AUTHENTICATE": return _handleAuthenticate(state, socket, parsed);
|
|
398
|
-
case "HAVESPACE": return _handleHaveSpace(state, socket, parsed);
|
|
399
|
-
case "PUTSCRIPT": return _handlePutScript(state, socket, parsed);
|
|
400
|
-
case "LISTSCRIPTS": return _handleListScripts(state, socket);
|
|
401
|
-
case "SETACTIVE": return _handleSetActive(state, socket, parsed);
|
|
402
|
-
case "GETSCRIPT": return _handleGetScript(state, socket, parsed);
|
|
403
|
-
case "DELETESCRIPT": return _handleDeleteScript(state, socket, parsed);
|
|
404
|
-
case "RENAMESCRIPT": return _handleRenameScript(state, socket, parsed);
|
|
405
|
-
default: return _writeNo(socket, "Unknown verb '" + verb + "'");
|
|
406
|
-
}
|
|
461
|
+
return _ensureRegistry().dispatch(parsed.verb, state, socket, parsed);
|
|
407
462
|
}
|
|
408
463
|
|
|
409
464
|
// _emitCapabilityBanner — RFC 5804 §1.7 capability banner. Lines
|