@blamejs/core 0.9.49 → 0.10.2

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 (82) hide show
  1. package/CHANGELOG.md +952 -908
  2. package/index.js +25 -0
  3. package/lib/_test/crypto-fixtures.js +67 -0
  4. package/lib/agent-event-bus.js +52 -6
  5. package/lib/agent-idempotency.js +169 -16
  6. package/lib/agent-orchestrator.js +263 -9
  7. package/lib/agent-posture-chain.js +163 -5
  8. package/lib/agent-saga.js +146 -16
  9. package/lib/agent-snapshot.js +349 -19
  10. package/lib/agent-stream.js +34 -2
  11. package/lib/agent-tenant.js +179 -23
  12. package/lib/agent-trace.js +84 -21
  13. package/lib/auth/aal.js +8 -1
  14. package/lib/auth/ciba.js +6 -1
  15. package/lib/auth/dpop.js +7 -2
  16. package/lib/auth/fal.js +17 -8
  17. package/lib/auth/jwt-external.js +128 -4
  18. package/lib/auth/oauth.js +232 -10
  19. package/lib/auth/oid4vci.js +67 -7
  20. package/lib/auth/openid-federation.js +71 -25
  21. package/lib/auth/passkey.js +140 -6
  22. package/lib/auth/sd-jwt-vc.js +78 -5
  23. package/lib/circuit-breaker.js +10 -2
  24. package/lib/cli.js +13 -0
  25. package/lib/compliance.js +176 -8
  26. package/lib/crypto-field.js +114 -14
  27. package/lib/crypto.js +216 -20
  28. package/lib/db.js +1 -0
  29. package/lib/guard-graphql.js +37 -0
  30. package/lib/guard-jmap.js +321 -0
  31. package/lib/guard-managesieve-command.js +566 -0
  32. package/lib/guard-pop3-command.js +317 -0
  33. package/lib/guard-regex.js +138 -1
  34. package/lib/guard-smtp-command.js +58 -3
  35. package/lib/guard-xml.js +39 -1
  36. package/lib/mail-agent.js +20 -7
  37. package/lib/mail-arc-sign.js +12 -8
  38. package/lib/mail-auth.js +323 -34
  39. package/lib/mail-crypto-pgp.js +934 -0
  40. package/lib/mail-crypto-smime.js +340 -0
  41. package/lib/mail-crypto.js +108 -0
  42. package/lib/mail-dav.js +1224 -0
  43. package/lib/mail-deploy.js +492 -0
  44. package/lib/mail-dkim.js +431 -26
  45. package/lib/mail-journal.js +435 -0
  46. package/lib/mail-scan.js +502 -0
  47. package/lib/mail-server-imap.js +64 -26
  48. package/lib/mail-server-jmap.js +488 -0
  49. package/lib/mail-server-managesieve.js +853 -0
  50. package/lib/mail-server-mx.js +40 -30
  51. package/lib/mail-server-pop3.js +836 -0
  52. package/lib/mail-server-rate-limit.js +13 -0
  53. package/lib/mail-server-submission.js +70 -24
  54. package/lib/mail-server-tls.js +445 -0
  55. package/lib/mail-sieve.js +557 -0
  56. package/lib/mail-spam-score.js +284 -0
  57. package/lib/mail.js +99 -0
  58. package/lib/metrics.js +80 -3
  59. package/lib/middleware/dpop.js +58 -3
  60. package/lib/middleware/idempotency-key.js +255 -42
  61. package/lib/middleware/protected-resource-metadata.js +114 -2
  62. package/lib/network-dns-resolver.js +33 -0
  63. package/lib/network-tls.js +46 -0
  64. package/lib/otel-export.js +13 -4
  65. package/lib/outbox.js +62 -12
  66. package/lib/pqc-agent.js +13 -5
  67. package/lib/retry.js +23 -9
  68. package/lib/router.js +23 -1
  69. package/lib/safe-ical.js +634 -0
  70. package/lib/safe-icap.js +502 -0
  71. package/lib/safe-mime.js +15 -0
  72. package/lib/safe-sieve.js +684 -0
  73. package/lib/safe-smtp.js +57 -0
  74. package/lib/safe-url.js +37 -0
  75. package/lib/safe-vcard.js +473 -0
  76. package/lib/self-update-standalone-verifier.js +32 -3
  77. package/lib/self-update.js +153 -33
  78. package/lib/vendor/MANIFEST.json +161 -156
  79. package/lib/vendor-data.js +127 -9
  80. package/lib/vex.js +324 -59
  81. package/package.json +1 -1
  82. package/sbom.cdx.json +6 -6
@@ -0,0 +1,853 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.mail.server.managesieve
4
+ * @nav Mail
5
+ * @title Mail ManageSieve Server
6
+ * @order 560
7
+ *
8
+ * @intro
9
+ * ManageSieve listener (RFC 5804 — "A Protocol for Remotely Managing
10
+ * Sieve Scripts"). Lets MUAs upload, replace, list, activate, fetch,
11
+ * delete, and rename Sieve filter scripts on the server. Composes
12
+ * `b.safeSieve.validate` for pre-storage validation per RFC 5804 §2.3:
13
+ * "An implementation MUST verify the script's validity ... and MUST
14
+ * reject scripts which fail validity tests."
15
+ *
16
+ * ## State machine (RFC 5804 §1)
17
+ *
18
+ * ```
19
+ * NOT-AUTHENTICATED → STARTTLS → AUTHENTICATED → LOGOUT
20
+ * ```
21
+ *
22
+ * - **NOT-AUTHENTICATED**: CAPABILITY / NOOP / STARTTLS /
23
+ * AUTHENTICATE / LOGOUT. The listener sends an unsolicited
24
+ * capability banner on connect (RFC 5804 §1.7).
25
+ * - **STARTTLS** (transient): triggered by `STARTTLS`. Pre-handshake
26
+ * receive buffer is drained before the TLS upgrade to defend the
27
+ * STARTTLS-injection class (CVE-2021-38371 / CVE-2021-33515 /
28
+ * CVE-2011-0411). Capabilities are re-emitted post-TLS so the
29
+ * client sees the post-TLS mechanism list (RFC 5804 §2.2).
30
+ * - **AUTHENTICATED**: HAVESPACE / PUTSCRIPT / LISTSCRIPTS /
31
+ * SETACTIVE / GETSCRIPT / DELETESCRIPT / RENAMESCRIPT / NOOP /
32
+ * CAPABILITY / LOGOUT.
33
+ *
34
+ * ## Wire-protocol defenses
35
+ *
36
+ * - **No-implicit-plaintext** — `opts.tlsContext` is required at
37
+ * `create()`. Operators that genuinely need plaintext (intra-rack
38
+ * testing) explicitly pass `allowPlaintext: true`, which emits a
39
+ * `mail.server.managesieve.plaintext_warning` audit on every boot.
40
+ *
41
+ * - **AUTHENTICATE-mechanism advertisement parity** — `CAPABILITY`
42
+ * output advertises ONLY the mechanisms listed in
43
+ * `opts.auth.mechanisms`. The framework hardcodes no defaults; an
44
+ * operator who omits `mechanisms` gets a listener that refuses
45
+ * every AUTHENTICATE attempt with "mechanism not advertised"
46
+ * (avoids the IMAP v0.9.49 Codex P2 class — advertising AUTH=PLAIN
47
+ * when authConfig is null sets clients up to attempt PLAIN against
48
+ * a listener that hasn't wired the verifier).
49
+ *
50
+ * - **Cleartext-AUTH refusal under strict** — RFC 5804 §1.1 + RFC
51
+ * 4954 §4. `AUTHENTICATE PLAIN` / `LOGIN` / `SCRAM*` pre-TLS under
52
+ * strict refused at both the validator and the dispatch boundary.
53
+ * `AUTHENTICATE EXTERNAL` exempt (TLS client-cert credential, not
54
+ * a password).
55
+ *
56
+ * - **STARTTLS injection (CVE-2021-33515 class)** — STARTTLS upgrade
57
+ * clears the per-connection receive buffer; any pipelined command
58
+ * queued before the upgrade is discarded. Capabilities are
59
+ * re-emitted on the post-TLS socket per RFC 5804 §2.2.
60
+ *
61
+ * - **PUTSCRIPT pre-validation (RFC 5804 §2.3)** — every PUTSCRIPT
62
+ * payload is parsed via `b.safeSieve.validate` before
63
+ * `mailStore.sieveScripts.put`. Invalid scripts are refused with
64
+ * `NO (QUOTA/MAXSCRIPTS) "..."` per §2.3 + audited with the
65
+ * `safe-sieve/...` issue code so operators can correlate refusals.
66
+ *
67
+ * - **Per-IP rate limit + AUTH-failure budget** — composes
68
+ * `b.mail.server.rateLimit` (default-on). Brute-force protection
69
+ * applies to AUTHENTICATE failures identically to POP3/IMAP.
70
+ *
71
+ * ## Audit lifecycle
72
+ *
73
+ * - `mail.server.managesieve.connect` — IP, TLS state
74
+ * - `mail.server.managesieve.auth_attempt` — mech
75
+ * - `mail.server.managesieve.auth_success` — mech, tenantId
76
+ * - `mail.server.managesieve.auth_failed` — mech, reason
77
+ * - `mail.server.managesieve.auth_refused_cleartext` — mech
78
+ * - `mail.server.managesieve.starttls_upgraded`
79
+ * - `mail.server.managesieve.starttls_handshake_failed`
80
+ * - `mail.server.managesieve.putscript` — name, bytes
81
+ * - `mail.server.managesieve.putscript_refused` — name, reason (safeSieve issue code)
82
+ * - `mail.server.managesieve.getscript` — name
83
+ * - `mail.server.managesieve.listscripts` — count
84
+ * - `mail.server.managesieve.setactive` — name (empty == deactivate-all)
85
+ * - `mail.server.managesieve.delete` — name
86
+ * - `mail.server.managesieve.rename` — old, new
87
+ * - `mail.server.managesieve.havespace` — name, size, ok
88
+ * - `mail.server.managesieve.logout`
89
+ * - `mail.server.managesieve.listening` — port, address
90
+ * - `mail.server.managesieve.closed`
91
+ * - `mail.server.managesieve.socket_error`
92
+ * - `mail.server.managesieve.handler_threw` — verb, error
93
+ *
94
+ * ## What v1 does NOT ship
95
+ *
96
+ * - **CHECKSCRIPT** (RFC 5804 §2.12) — parse-only verb. Operators
97
+ * who want it compose `b.safeSieve.validate` directly via JMAP
98
+ * `SieveScript/validate` (RFC 9404). The MTA-side ManageSieve
99
+ * surface is `PUTSCRIPT` + `HAVESPACE`; CHECKSCRIPT adds a third
100
+ * entry point with no operator demand yet.
101
+ * - **UNAUTHENTICATE** (RFC 5804 §2.14) — exotic. Operators close
102
+ * the TCP connection or send `LOGOUT` + reconnect.
103
+ *
104
+ * ## Composition contract
105
+ *
106
+ * - `b.guardManageSieveCommand` — wire-protocol gate
107
+ * - `b.safeSieve.validate` — PUTSCRIPT pre-validation
108
+ * - `b.mail.server.rateLimit` — DoS defense
109
+ * - `b.mailStore` — operator-supplied backend (must expose
110
+ * `sieveScripts.put(actor, name, body)` /
111
+ * `sieveScripts.list(actor)` /
112
+ * `sieveScripts.get(actor, name)` /
113
+ * `sieveScripts.setActive(actor, name)` /
114
+ * `sieveScripts.delete(actor, name)` /
115
+ * `sieveScripts.rename(actor, oldName, newName)` /
116
+ * `sieveScripts.haveSpace(actor, name, size)`)
117
+ * - operator's `auth.verify(mechanism, credentials)` async predicate
118
+ * - `b.network.tls.context` — TLS posture
119
+ *
120
+ * @card
121
+ * ManageSieve listener (RFC 5804). State machine NOT-AUTH → STARTTLS
122
+ * → AUTH → LOGOUT. Composes b.guardManageSieveCommand +
123
+ * b.safeSieve.validate (PUTSCRIPT pre-validation per §2.3) +
124
+ * b.mail.server.rateLimit + operator-supplied mailStore + SASL
125
+ * authenticator. STARTTLS-injection defense + AUTH-failure budget +
126
+ * cleartext-AUTH refusal under strict + LITERAL+ support (RFC 7888).
127
+ */
128
+
129
+ var net = require("node:net");
130
+ var mailServerTls = require("./mail-server-tls");
131
+ var lazyRequire = require("./lazy-require");
132
+ var C = require("./constants");
133
+ var bCrypto = require("./crypto");
134
+ var numericBounds = require("./numeric-bounds");
135
+ var validateOpts = require("./validate-opts");
136
+ var guardManageSieveCommand = require("./guard-managesieve-command");
137
+ var safeSieve = require("./safe-sieve");
138
+ var mailServerRateLimit = require("./mail-server-rate-limit");
139
+ var { defineClass } = require("./framework-error");
140
+
141
+ var audit = lazyRequire(function () { return require("./audit"); });
142
+
143
+ var MailServerManageSieveError = defineClass("MailServerManageSieveError",
144
+ { alwaysPermanent: true });
145
+
146
+ // RFC 5804 §1 default port (IANA-assigned).
147
+ var DEFAULT_PORT = 4190; // allow:raw-byte-literal — RFC 5804 §1 / IANA managesieve port
148
+ var DEFAULT_MAX_LINE_BYTES = 8192; // allow:raw-byte-literal — matches guardManageSieveCommand strict cap
149
+ var DEFAULT_IDLE_TIMEOUT_MS = C.TIME.minutes(5);
150
+ var DEFAULT_GREETING_VENDOR = "blamejs ManageSieve";
151
+
152
+ var ERR_CLAMP = 200; // allow:raw-byte-literal — protocol-reply error-message clamp
153
+
154
+ /**
155
+ * @primitive b.mail.server.managesieve.create
156
+ * @signature b.mail.server.managesieve.create(opts)
157
+ * @since 0.9.57
158
+ * @status stable
159
+ * @related b.mail.server.imap.create, b.mail.server.pop3.create, b.safeSieve.parse, b.guardManageSieveCommand.validate
160
+ *
161
+ * Build a ManageSieve listener (RFC 5804). Returns a handle exposing
162
+ * `listen({ port, address })` and `close()`. Composes `b.safeSieve`
163
+ * for PUTSCRIPT pre-validation per RFC 5804 §2.3.
164
+ *
165
+ * @opts
166
+ * tlsContext: SecureContext, // required (no implicit plaintext)
167
+ * allowPlaintext: boolean, // explicit opt-in; emits warning audit
168
+ * greeting: string, // default "blamejs ManageSieve"
169
+ * maxLineBytes: number, // default 8192
170
+ * idleTimeoutMs: number, // default 5 min
171
+ * profile: "strict" | "balanced" | "permissive", // default "strict"
172
+ * auth: {
173
+ * mechanisms: ["SCRAM-SHA-256", "OAUTHBEARER", ...], // SASL mechs to advertise
174
+ * verify: async function (mech, credentials) → { ok, actor },
175
+ * },
176
+ * mailStore: b.mailStore handle, // must expose sieveScripts.*
177
+ * rateLimit: b.mail.server.rateLimit handle | opts | false,
178
+ * audit: b.audit
179
+ *
180
+ * @example
181
+ * var msv = b.mail.server.managesieve.create({
182
+ * tlsContext: b.mail.server.tls.context({ certFile, keyFile }).secureContext,
183
+ * auth: {
184
+ * mechanisms: ["SCRAM-SHA-256", "OAUTHBEARER", "EXTERNAL"],
185
+ * verify: async function (mech, creds) {
186
+ * return { ok: true, actor: { username: creds.authzid, tenantId: "t1" } };
187
+ * },
188
+ * },
189
+ * mailStore: b.mailStore.create({ backend: b.db.handle() }),
190
+ * });
191
+ * await msv.listen({ port: 4190 });
192
+ */
193
+ function create(opts) {
194
+ validateOpts.requireObject(opts, "mail.server.managesieve.create",
195
+ MailServerManageSieveError, "mail-server-managesieve/bad-opts");
196
+ if (!opts.tlsContext && !opts.allowPlaintext) {
197
+ throw new MailServerManageSieveError("mail-server-managesieve/no-tls-context",
198
+ "mail.server.managesieve.create: tlsContext is required (no implicit plaintext mode). " +
199
+ "Use b.mail.server.tls.context({ certFile, keyFile, watch: true }) to load + " +
200
+ "auto-reload a cert/key pair from disk. Operators that genuinely need plaintext " +
201
+ "(intra-rack testing) explicitly pass allowPlaintext: true.");
202
+ }
203
+ if (!opts.mailStore || !opts.mailStore.sieveScripts ||
204
+ typeof opts.mailStore.sieveScripts.put !== "function") {
205
+ throw new MailServerManageSieveError("mail-server-managesieve/no-mail-store",
206
+ "mail.server.managesieve.create: mailStore.sieveScripts is required (must expose " +
207
+ "put/list/get/setActive/delete/rename/haveSpace; compose b.mailStore.create or " +
208
+ "operator-supplied backend)");
209
+ }
210
+ numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
211
+ ["maxLineBytes", "idleTimeoutMs"],
212
+ "mail.server.managesieve.", MailServerManageSieveError, "mail-server-managesieve/bad-bound");
213
+
214
+ var greeting = opts.greeting || DEFAULT_GREETING_VENDOR;
215
+ var maxLineBytes = opts.maxLineBytes || DEFAULT_MAX_LINE_BYTES;
216
+ var idleTimeoutMs = opts.idleTimeoutMs || DEFAULT_IDLE_TIMEOUT_MS;
217
+ var profile = opts.profile || "strict";
218
+ var authConfig = opts.auth || null;
219
+ var mailStore = opts.mailStore;
220
+ var allowPlaintext = opts.allowPlaintext === true;
221
+ var tlsContext = opts.tlsContext || null;
222
+
223
+ // safeSieve cap matches the guard's per-profile script cap (the
224
+ // guard caps the literal-byte announcement; safeSieve.parse caps
225
+ // the actual script bytes — same cap on both sides).
226
+ var safeSieveProfile = profile;
227
+
228
+ var rateLimit;
229
+ if (opts.rateLimit === false) {
230
+ rateLimit = mailServerRateLimit.create({ disabled: true });
231
+ } else if (opts.rateLimit && typeof opts.rateLimit.admitConnection === "function") {
232
+ rateLimit = opts.rateLimit;
233
+ } else {
234
+ rateLimit = mailServerRateLimit.create(opts.rateLimit || {});
235
+ }
236
+
237
+ var tcpServer = null;
238
+ var listening = false;
239
+ var connections = new Set();
240
+
241
+ function _emit(action, metadata, outcome) {
242
+ try {
243
+ audit().safeEmit({
244
+ action: action,
245
+ outcome: outcome || "success",
246
+ metadata: metadata || {},
247
+ });
248
+ } catch (_e) { /* drop-silent — audit best-effort */ }
249
+ }
250
+
251
+ function _handleConnection(rawSocket) {
252
+ var remoteAddress = rawSocket.remoteAddress || "0.0.0.0";
253
+ var admit = rateLimit.admitConnection(remoteAddress);
254
+ if (!admit.ok) {
255
+ _emit("mail.server.managesieve.rate_limit_refused",
256
+ { remoteAddress: remoteAddress, reason: admit.reason }, "denied");
257
+ try { rawSocket.write('NO "Too many connections from your IP"\r\n'); }
258
+ catch (_e) { /* socket may be down */ }
259
+ try { rawSocket.destroy(); } catch (_e2) { /* idempotent */ }
260
+ return;
261
+ }
262
+ var connectionId = "msvconn-" + bCrypto.generateToken(8); // allow:raw-byte-literal — connection-id length
263
+ var socket = rawSocket;
264
+ connections.add(socket);
265
+ // Single close handler covers BOTH operator-driven `_close(socket)`
266
+ // and client-initiated disconnects (TCP FIN / RST). Releases the
267
+ // rate-limit slot AND removes the socket from the tracking set so
268
+ // long-lived deployments don't accumulate stale entries.
269
+ rawSocket.once("close", function () {
270
+ rateLimit.releaseConnection(remoteAddress);
271
+ connections.delete(socket);
272
+ });
273
+
274
+ var state = {
275
+ id: connectionId,
276
+ remoteAddress: remoteAddress,
277
+ tls: false,
278
+ stage: "not-authenticated",
279
+ actor: null,
280
+ pendingLiteral: null, // { verb, name, size, body, plus }
281
+ pendingAuth: null, // { mech, irBytes, irPlus, irBody }
282
+ lineBuffer: Buffer.alloc(0),
283
+ };
284
+
285
+ _emit("mail.server.managesieve.connect",
286
+ { connectionId: connectionId, remoteAddress: remoteAddress });
287
+
288
+ socket.setTimeout(idleTimeoutMs);
289
+ socket.on("timeout", function () {
290
+ _writeBye(socket, "Idle timeout");
291
+ _close(socket);
292
+ });
293
+ socket.on("error", function (err) {
294
+ _emit("mail.server.managesieve.socket_error",
295
+ { connectionId: connectionId, error: (err && err.message) || String(err) }, "failure");
296
+ });
297
+
298
+ // Unsolicited capability banner per RFC 5804 §1.7 — capabilities
299
+ // first, then `OK "<greeting>"`.
300
+ _emitCapabilityBanner(state, socket);
301
+ _writeOk(socket, greeting + " ready");
302
+
303
+ if (allowPlaintext && !tlsContext) {
304
+ _emit("mail.server.managesieve.plaintext_warning",
305
+ { connectionId: connectionId,
306
+ remark: "allowPlaintext=true; no STARTTLS available — operators MUST gate at network layer" },
307
+ "warning");
308
+ }
309
+
310
+ socket.on("data", function (chunk) {
311
+ state.lineBuffer = Buffer.concat([state.lineBuffer, chunk]);
312
+ _drainBuffer(state, socket);
313
+ });
314
+ }
315
+
316
+ // _drainBuffer — line-oriented dispatch with literal-payload windows.
317
+ // When the previous command opened a literal (PUTSCRIPT, or
318
+ // AUTHENTICATE with a non-synchronizing initial-response), the next
319
+ // N bytes are the literal-payload; accumulate them before resuming
320
+ // line-mode dispatch.
321
+ function _drainBuffer(state, socket) {
322
+ while (true) {
323
+ if (state.pendingLiteral) {
324
+ var pl = state.pendingLiteral;
325
+ var need = pl.size - pl.body.length;
326
+ if (state.lineBuffer.length < need) {
327
+ pl.body = Buffer.concat([pl.body, state.lineBuffer]);
328
+ state.lineBuffer = Buffer.alloc(0);
329
+ return;
330
+ }
331
+ pl.body = Buffer.concat([pl.body, state.lineBuffer.subarray(0, need)]);
332
+ state.lineBuffer = state.lineBuffer.subarray(need);
333
+ state.pendingLiteral = null;
334
+ _completePutscript(state, socket, pl);
335
+ if (state.stage === "closed") return;
336
+ continue;
337
+ }
338
+ if (state.pendingAuth && state.pendingAuth.irBytes !== null) {
339
+ var pa = state.pendingAuth;
340
+ var needA = pa.irBytes - pa.irBody.length;
341
+ if (state.lineBuffer.length < needA) {
342
+ pa.irBody = Buffer.concat([pa.irBody, state.lineBuffer]);
343
+ state.lineBuffer = Buffer.alloc(0);
344
+ return;
345
+ }
346
+ pa.irBody = Buffer.concat([pa.irBody, state.lineBuffer.subarray(0, needA)]);
347
+ state.lineBuffer = state.lineBuffer.subarray(needA);
348
+ // After literal-IR is gathered, expect CRLF terminator.
349
+ pa.irBytes = null;
350
+ _completeAuthenticate(state, socket);
351
+ if (state.stage === "closed") return;
352
+ continue;
353
+ }
354
+ var crlf = state.lineBuffer.indexOf("\r\n");
355
+ if (crlf === -1) {
356
+ if (state.lineBuffer.length > maxLineBytes) {
357
+ _writeNo(socket, "Line too long (cap " + maxLineBytes + ")");
358
+ _close(socket);
359
+ }
360
+ return;
361
+ }
362
+ var rawLine = state.lineBuffer.subarray(0, crlf).toString("utf8");
363
+ state.lineBuffer = state.lineBuffer.subarray(crlf + 2);
364
+ _handleLine(state, socket, rawLine);
365
+ if (state.stage === "closed") return;
366
+ }
367
+ }
368
+
369
+ function _handleLine(state, socket, line) {
370
+ var parsed;
371
+ try {
372
+ parsed = guardManageSieveCommand.validate(line, {
373
+ profile: profile,
374
+ tls: state.tls,
375
+ });
376
+ } catch (e) {
377
+ _writeNo(socket, (e && e.message ? e.message.slice(0, ERR_CLAMP) : "syntax"));
378
+ return;
379
+ }
380
+ try {
381
+ _dispatch(state, socket, parsed);
382
+ } catch (e) {
383
+ _emit("mail.server.managesieve.handler_threw",
384
+ { connectionId: state.id, verb: parsed && parsed.verb,
385
+ error: (e && e.message) || String(e) }, "failure");
386
+ _writeNo(socket, "Internal error");
387
+ }
388
+ }
389
+
390
+ function _dispatch(state, socket, parsed) {
391
+ var verb = parsed.verb;
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
+ }
407
+ }
408
+
409
+ // _emitCapabilityBanner — RFC 5804 §1.7 capability banner. Lines
410
+ // are quoted-string identifiers, optionally followed by a quoted-
411
+ // string value. The framework emits IMPLEMENTATION / SASL / SIEVE /
412
+ // VERSION / STARTTLS (when pre-TLS) — no terminator line; the
413
+ // listener emits a closing `OK` per §1.7.
414
+ function _emitCapabilityBanner(state, socket) {
415
+ socket.write('"IMPLEMENTATION" "blamejs"\r\n');
416
+ socket.write('"VERSION" "1.0"\r\n');
417
+ // Advertise the Sieve extensions safeSieve currently implements.
418
+ // Keep in lockstep with safeSieve.KNOWN_CAPABILITIES — any entry
419
+ // with `true` is exposed.
420
+ var sieveCaps = [];
421
+ var known = safeSieve.KNOWN_CAPABILITIES;
422
+ var names = Object.keys(known);
423
+ for (var i = 0; i < names.length; i += 1) {
424
+ if (known[names[i]] === true && names[i].indexOf("comparator-") !== 0) {
425
+ sieveCaps.push(names[i]);
426
+ }
427
+ }
428
+ socket.write('"SIEVE" "' + sieveCaps.join(" ") + '"\r\n');
429
+ // Advertise SASL mechanisms — ONLY the mechs the operator wired
430
+ // in opts.auth.mechanisms (Codex P2 IMAP lesson: don't hardcode).
431
+ if (authConfig && Array.isArray(authConfig.mechanisms) && authConfig.mechanisms.length > 0) {
432
+ var mechs = authConfig.mechanisms.map(function (m) {
433
+ return String(m).toUpperCase();
434
+ }).join(" ");
435
+ socket.write('"SASL" "' + mechs + '"\r\n');
436
+ } else {
437
+ socket.write('"SASL" ""\r\n');
438
+ }
439
+ if (!state.tls && tlsContext) {
440
+ socket.write('"STARTTLS"\r\n');
441
+ }
442
+ }
443
+
444
+ function _handleCapability(state, socket) {
445
+ _emitCapabilityBanner(state, socket);
446
+ _writeOk(socket, "Capability completed");
447
+ }
448
+
449
+ function _handleNoop(state, socket, parsed) {
450
+ void state;
451
+ if (parsed.args.length > 0) {
452
+ _writeOkWithTag(socket, parsed.args[0], "NOOP completed");
453
+ } else {
454
+ _writeOk(socket, "NOOP completed");
455
+ }
456
+ }
457
+
458
+ function _handleStartTls(state, socket) {
459
+ if (state.tls) {
460
+ _writeNo(socket, "STARTTLS already negotiated");
461
+ return;
462
+ }
463
+ if (state.stage !== "not-authenticated") {
464
+ _writeNo(socket, "STARTTLS only valid pre-AUTH (RFC 5804 §2.2)");
465
+ return;
466
+ }
467
+ if (!tlsContext) {
468
+ _writeNo(socket, "STARTTLS unavailable (listener configured with allowPlaintext=true and no tlsContext)");
469
+ return;
470
+ }
471
+ _writeOk(socket, "Begin TLS negotiation now");
472
+ // RFC 5804 §2.2 — discard any bytes the client queued before the
473
+ // upgrade so pre-handshake injection can't survive into the post-
474
+ // TLS session. The shared upgradeSocket helper handles the
475
+ // CVE-2021-33515 / CVE-2021-38371 listener-strip + pause; the
476
+ // pending-protocol state still belongs to managesieve.
477
+ state.lineBuffer = Buffer.alloc(0);
478
+ state.pendingLiteral = null;
479
+ state.pendingAuth = null;
480
+ mailServerTls.upgradeSocket({
481
+ plainSocket: socket,
482
+ secureContext: tlsContext,
483
+ idleTimeoutMs: idleTimeoutMs,
484
+ onSecure: function (tlsSocket) {
485
+ state.tls = true;
486
+ _emit("mail.server.managesieve.starttls_upgraded",
487
+ { connectionId: state.id });
488
+ // RFC 5804 §2.2 — server MUST re-emit capabilities on the
489
+ // post-TLS socket so the client sees the post-TLS mechanism
490
+ // list (which may now include PLAIN, etc.).
491
+ _emitCapabilityBanner(state, tlsSocket);
492
+ _writeOk(tlsSocket, "TLS negotiation successful");
493
+ },
494
+ onData: function (tlsSocket, chunk) {
495
+ state.lineBuffer = Buffer.concat([state.lineBuffer, chunk]);
496
+ _drainBuffer(state, tlsSocket);
497
+ },
498
+ onError: function (err) {
499
+ _emit("mail.server.managesieve.starttls_handshake_failed",
500
+ { connectionId: state.id, error: (err && err.message) || String(err) }, "failure");
501
+ _close(socket);
502
+ },
503
+ });
504
+ }
505
+
506
+ function _handleLogout(state, socket) {
507
+ _emit("mail.server.managesieve.logout",
508
+ { connectionId: state.id });
509
+ _writeOk(socket, "Logout completed");
510
+ _close(socket);
511
+ }
512
+
513
+ function _handleAuthenticate(state, socket, parsed) {
514
+ if (state.stage !== "not-authenticated") {
515
+ _writeNo(socket, "AUTHENTICATE only valid in NOT-AUTHENTICATED");
516
+ return;
517
+ }
518
+ if (!authConfig || typeof authConfig.verify !== "function") {
519
+ _writeNo(socket, "AUTHENTICATE not configured on this listener");
520
+ return;
521
+ }
522
+ var mech = parsed.args[0];
523
+ var advertised = (authConfig.mechanisms || []).map(function (m) {
524
+ return String(m).toUpperCase();
525
+ });
526
+ if (advertised.indexOf(mech) === -1) {
527
+ _writeNo(socket, "Mechanism '" + mech + "' not advertised");
528
+ return;
529
+ }
530
+ // Defense-in-depth: re-check cleartext refusal at the dispatch
531
+ // boundary even though the validator already gated it (operator
532
+ // could have configured a relaxed profile but the listener still
533
+ // wants to enforce strict at AUTH).
534
+ if (!state.tls && profile === "strict" && mech !== "EXTERNAL") {
535
+ _emit("mail.server.managesieve.auth_refused_cleartext",
536
+ { connectionId: state.id, mech: mech }, "denied");
537
+ _writeNo(socket, "AUTHENTICATE " + mech +
538
+ " refused over cleartext (use STARTTLS first; RFC 5804 §1.1 + RFC 4954 §4)");
539
+ return;
540
+ }
541
+ var authAdmit = rateLimit.checkAuthAdmit(state.remoteAddress);
542
+ if (!authAdmit.ok) {
543
+ _emit("mail.server.managesieve.auth_rate_limit_refused",
544
+ { connectionId: state.id, remoteAddress: state.remoteAddress, reason: authAdmit.reason },
545
+ "denied");
546
+ _writeNo(socket, "Too many AUTH failures from your IP");
547
+ _close(socket);
548
+ return;
549
+ }
550
+ state.pendingAuth = {
551
+ mech: mech,
552
+ irBytes: parsed.literalBytes,
553
+ irPlus: parsed.literalPlus,
554
+ irBody: Buffer.alloc(0),
555
+ };
556
+ if (parsed.literalBytes === null) {
557
+ // No initial-response — call verify with empty client response.
558
+ _completeAuthenticate(state, socket);
559
+ return;
560
+ }
561
+ if (!parsed.literalPlus) {
562
+ // Synchronizing literal — server sends continuation request
563
+ // before client transmits the bytes.
564
+ socket.write("OK\r\n");
565
+ }
566
+ // Loop drains the literal-IR bytes; _completeAuthenticate runs
567
+ // after.
568
+ }
569
+
570
+ function _completeAuthenticate(state, socket) {
571
+ var pa = state.pendingAuth;
572
+ state.pendingAuth = null;
573
+ if (!pa) return;
574
+ _emit("mail.server.managesieve.auth_attempt",
575
+ { connectionId: state.id, mech: pa.mech, remoteAddress: state.remoteAddress });
576
+ Promise.resolve()
577
+ .then(function () {
578
+ return authConfig.verify(pa.mech, {
579
+ clientResponse: pa.irBody.length > 0 ? pa.irBody.toString("utf8") : null,
580
+ tls: state.tls,
581
+ remoteAddress: state.remoteAddress,
582
+ });
583
+ })
584
+ .then(function (result) {
585
+ if (result && result.ok && result.actor) {
586
+ state.actor = result.actor;
587
+ state.stage = "authenticated";
588
+ _emit("mail.server.managesieve.auth_success",
589
+ { connectionId: state.id, mech: pa.mech, tenantId: state.actor.tenantId || null });
590
+ _writeOk(socket, "Authenticated");
591
+ return;
592
+ }
593
+ rateLimit.noteAuthFailure(state.remoteAddress);
594
+ _emit("mail.server.managesieve.auth_failed",
595
+ { connectionId: state.id, mech: pa.mech, reason: "verify-returned-fail" }, "denied");
596
+ _writeNo(socket, "Authentication failed");
597
+ })
598
+ .catch(function () {
599
+ rateLimit.noteAuthFailure(state.remoteAddress);
600
+ _writeNo(socket, "Authentication failed");
601
+ });
602
+ }
603
+
604
+ function _requireAuth(state, socket) {
605
+ if (state.stage !== "authenticated") {
606
+ _writeNo(socket, "AUTHENTICATE first");
607
+ return false;
608
+ }
609
+ return true;
610
+ }
611
+
612
+ function _handleHaveSpace(state, socket, parsed) {
613
+ if (!_requireAuth(state, socket)) return;
614
+ var name = parsed.args[0];
615
+ var size = parsed.args[1];
616
+ Promise.resolve()
617
+ .then(function () { return mailStore.sieveScripts.haveSpace(state.actor, name, size); })
618
+ .then(function (result) {
619
+ var ok = result && result.ok !== false;
620
+ _emit("mail.server.managesieve.havespace",
621
+ { connectionId: state.id, name: name, size: size, ok: ok });
622
+ if (ok) {
623
+ _writeOk(socket, "Have space");
624
+ } else {
625
+ _writeNo(socket, "(QUOTA/MAXSIZE) " + ((result && result.reason) || "no space"));
626
+ }
627
+ })
628
+ .catch(function (err) {
629
+ _writeNo(socket, ((err && err.message) || "haveSpace failed").slice(0, ERR_CLAMP));
630
+ });
631
+ }
632
+
633
+ function _handlePutScript(state, socket, parsed) {
634
+ if (!_requireAuth(state, socket)) return;
635
+ var name = parsed.args[0];
636
+ var size = parsed.literalBytes;
637
+ var plus = parsed.literalPlus;
638
+ state.pendingLiteral = {
639
+ verb: "PUTSCRIPT",
640
+ name: name,
641
+ size: size,
642
+ plus: plus,
643
+ body: Buffer.alloc(0),
644
+ };
645
+ if (!plus) {
646
+ // Synchronizing literal — RFC 5804 §2.3: server sends
647
+ // continuation before client transmits payload.
648
+ socket.write("OK\r\n");
649
+ }
650
+ // _completePutscript runs after the literal is fully drained.
651
+ }
652
+
653
+ function _completePutscript(state, socket, literal) {
654
+ var bodyText = literal.body.toString("utf8");
655
+ // RFC 5804 §2.3 — implementation MUST verify script validity
656
+ // before accepting it. Refuse with the safeSieve issue code so
657
+ // operators can correlate refusals to specific parse errors.
658
+ var v;
659
+ try {
660
+ v = safeSieve.validate(bodyText, { profile: safeSieveProfile });
661
+ } catch (e) {
662
+ _emit("mail.server.managesieve.putscript_refused",
663
+ { connectionId: state.id, name: literal.name,
664
+ reason: (e && e.code) || "safe-sieve/parse-error" }, "denied");
665
+ _writeNo(socket, "(QUOTA/MAXSIZE) " + ((e && e.message) || "validation failed").slice(0, ERR_CLAMP));
666
+ return;
667
+ }
668
+ if (!v.ok) {
669
+ var issue = (v.issues && v.issues[0]) || { ruleId: "safe-sieve/parse-error", snippet: "invalid" };
670
+ _emit("mail.server.managesieve.putscript_refused",
671
+ { connectionId: state.id, name: literal.name, reason: issue.ruleId }, "denied");
672
+ _writeNo(socket, "Script validation failed: " + (issue.snippet || issue.ruleId).slice(0, ERR_CLAMP));
673
+ return;
674
+ }
675
+ Promise.resolve()
676
+ .then(function () {
677
+ return mailStore.sieveScripts.put(state.actor, literal.name, bodyText, {
678
+ requiredCaps: v.requiredCaps,
679
+ });
680
+ })
681
+ .then(function () {
682
+ _emit("mail.server.managesieve.putscript",
683
+ { connectionId: state.id, name: literal.name, bytes: literal.size,
684
+ requiredCaps: v.requiredCaps });
685
+ _writeOk(socket, "PUTSCRIPT completed");
686
+ })
687
+ .catch(function (err) {
688
+ _writeNo(socket, ((err && err.message) || "PUTSCRIPT failed").slice(0, ERR_CLAMP));
689
+ });
690
+ }
691
+
692
+ function _handleListScripts(state, socket) {
693
+ if (!_requireAuth(state, socket)) return;
694
+ Promise.resolve()
695
+ .then(function () { return mailStore.sieveScripts.list(state.actor); })
696
+ .then(function (scripts) {
697
+ var list = Array.isArray(scripts) ? scripts : [];
698
+ for (var i = 0; i < list.length; i += 1) {
699
+ var s = list[i];
700
+ var nm = String(s.name || "");
701
+ var active = s.active === true ? " ACTIVE" : "";
702
+ socket.write('"' + _quoteEscape(nm) + '"' + active + "\r\n");
703
+ }
704
+ _emit("mail.server.managesieve.listscripts",
705
+ { connectionId: state.id, count: list.length });
706
+ _writeOk(socket, "LISTSCRIPTS completed");
707
+ })
708
+ .catch(function (err) {
709
+ _writeNo(socket, ((err && err.message) || "LISTSCRIPTS failed").slice(0, ERR_CLAMP));
710
+ });
711
+ }
712
+
713
+ function _handleSetActive(state, socket, parsed) {
714
+ if (!_requireAuth(state, socket)) return;
715
+ var name = parsed.args[0];
716
+ Promise.resolve()
717
+ .then(function () { return mailStore.sieveScripts.setActive(state.actor, name); })
718
+ .then(function () {
719
+ _emit("mail.server.managesieve.setactive",
720
+ { connectionId: state.id, name: name });
721
+ _writeOk(socket, "SETACTIVE completed");
722
+ })
723
+ .catch(function (err) {
724
+ _writeNo(socket, ((err && err.message) || "SETACTIVE failed").slice(0, ERR_CLAMP));
725
+ });
726
+ }
727
+
728
+ function _handleGetScript(state, socket, parsed) {
729
+ if (!_requireAuth(state, socket)) return;
730
+ var name = parsed.args[0];
731
+ Promise.resolve()
732
+ .then(function () { return mailStore.sieveScripts.get(state.actor, name); })
733
+ .then(function (script) {
734
+ if (!script) {
735
+ _writeNo(socket, "(NONEXISTENT) Script not found");
736
+ return;
737
+ }
738
+ var body = String(script.body || "");
739
+ var bytes = Buffer.byteLength(body, "utf8");
740
+ // RFC 5804 §2.9 — return the script as a synchronizing
741
+ // literal followed by the body and CRLF, then OK.
742
+ socket.write("{" + bytes + "}\r\n");
743
+ socket.write(body);
744
+ if (!body.endsWith("\r\n")) socket.write("\r\n");
745
+ _emit("mail.server.managesieve.getscript",
746
+ { connectionId: state.id, name: name, bytes: bytes });
747
+ _writeOk(socket, "GETSCRIPT completed");
748
+ })
749
+ .catch(function (err) {
750
+ _writeNo(socket, ((err && err.message) || "GETSCRIPT failed").slice(0, ERR_CLAMP));
751
+ });
752
+ }
753
+
754
+ function _handleDeleteScript(state, socket, parsed) {
755
+ if (!_requireAuth(state, socket)) return;
756
+ var name = parsed.args[0];
757
+ Promise.resolve()
758
+ .then(function () { return mailStore.sieveScripts.delete(state.actor, name); })
759
+ .then(function () {
760
+ _emit("mail.server.managesieve.delete",
761
+ { connectionId: state.id, name: name });
762
+ _writeOk(socket, "DELETESCRIPT completed");
763
+ })
764
+ .catch(function (err) {
765
+ _writeNo(socket, ((err && err.message) || "DELETESCRIPT failed").slice(0, ERR_CLAMP));
766
+ });
767
+ }
768
+
769
+ function _handleRenameScript(state, socket, parsed) {
770
+ if (!_requireAuth(state, socket)) return;
771
+ var oldName = parsed.args[0];
772
+ var newName = parsed.args[1];
773
+ Promise.resolve()
774
+ .then(function () { return mailStore.sieveScripts.rename(state.actor, oldName, newName); })
775
+ .then(function () {
776
+ _emit("mail.server.managesieve.rename",
777
+ { connectionId: state.id, old: oldName, "new": newName });
778
+ _writeOk(socket, "RENAMESCRIPT completed");
779
+ })
780
+ .catch(function (err) {
781
+ _writeNo(socket, ((err && err.message) || "RENAMESCRIPT failed").slice(0, ERR_CLAMP));
782
+ });
783
+ }
784
+
785
+ // RFC 5804 §1.2 quoted-string escaping: backslash + DQUOTE inside
786
+ // the value get escaped with a leading backslash.
787
+ function _quoteEscape(s) {
788
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); // allow:regex-no-length-cap — backslash + DQUOTE escape on bounded-input
789
+ }
790
+
791
+ function _writeOk(socket, msg) {
792
+ try { socket.write('OK "' + _quoteEscape(msg) + '"\r\n'); } catch (_e) { /* socket down */ }
793
+ }
794
+ function _writeOkWithTag(socket, tag, msg) {
795
+ try { socket.write('OK (TAG "' + _quoteEscape(tag) + '") "' + _quoteEscape(msg) + '"\r\n'); }
796
+ catch (_e) { /* socket down */ }
797
+ }
798
+ function _writeNo(socket, msg) {
799
+ try { socket.write('NO "' + _quoteEscape(msg) + '"\r\n'); } catch (_e) { /* socket down */ }
800
+ }
801
+ function _writeBye(socket, msg) {
802
+ try { socket.write('BYE "' + _quoteEscape(msg) + '"\r\n'); } catch (_e) { /* socket down */ }
803
+ }
804
+ function _close(socket) {
805
+ try { socket.end(); } catch (_e) { /* idempotent */ }
806
+ try { socket.destroy(); } catch (_e2) { /* idempotent */ }
807
+ connections.delete(socket);
808
+ }
809
+
810
+ // ---- Lifecycle ----------------------------------------------------------
811
+ async function listen(listenOpts) {
812
+ listenOpts = listenOpts || {};
813
+ if (listening) {
814
+ throw new MailServerManageSieveError("mail-server-managesieve/already-listening",
815
+ "listen: already listening");
816
+ }
817
+ var port = listenOpts.port === undefined ? DEFAULT_PORT : listenOpts.port;
818
+ var address = listenOpts.address || "0.0.0.0";
819
+ tcpServer = net.createServer(function (socket) { _handleConnection(socket); });
820
+ return new Promise(function (resolve, reject) {
821
+ tcpServer.once("error", reject);
822
+ tcpServer.listen(port, address, function () {
823
+ listening = true;
824
+ tcpServer.removeListener("error", reject);
825
+ _emit("mail.server.managesieve.listening", { port: port, address: address });
826
+ resolve({ port: tcpServer.address().port, address: address });
827
+ });
828
+ });
829
+ }
830
+
831
+ async function close() {
832
+ if (!listening) return;
833
+ listening = false;
834
+ for (var s of connections) { try { s.destroy(); } catch (_e) { /* idempotent */ } }
835
+ connections.clear();
836
+ return new Promise(function (resolve) {
837
+ tcpServer.close(function () {
838
+ _emit("mail.server.managesieve.closed", {});
839
+ resolve();
840
+ });
841
+ });
842
+ }
843
+
844
+ return {
845
+ listen: listen,
846
+ close: close,
847
+ };
848
+ }
849
+
850
+ module.exports = {
851
+ create: create,
852
+ MailServerManageSieveError: MailServerManageSieveError,
853
+ };