@blamejs/core 0.9.49 → 0.10.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.
- package/CHANGELOG.md +951 -908
- package/index.js +25 -0
- package/lib/_test/crypto-fixtures.js +67 -0
- package/lib/agent-event-bus.js +52 -6
- package/lib/agent-idempotency.js +169 -16
- package/lib/agent-orchestrator.js +263 -9
- package/lib/agent-posture-chain.js +163 -5
- package/lib/agent-saga.js +146 -16
- package/lib/agent-snapshot.js +349 -19
- package/lib/agent-stream.js +34 -2
- package/lib/agent-tenant.js +179 -23
- package/lib/agent-trace.js +84 -21
- package/lib/auth/aal.js +8 -1
- package/lib/auth/ciba.js +6 -1
- package/lib/auth/dpop.js +7 -2
- package/lib/auth/fal.js +17 -8
- package/lib/auth/jwt-external.js +128 -4
- package/lib/auth/oauth.js +232 -10
- package/lib/auth/oid4vci.js +67 -7
- package/lib/auth/openid-federation.js +71 -25
- package/lib/auth/passkey.js +140 -6
- package/lib/auth/sd-jwt-vc.js +67 -5
- package/lib/circuit-breaker.js +10 -2
- package/lib/compliance.js +176 -8
- package/lib/crypto-field.js +114 -14
- package/lib/crypto.js +216 -20
- package/lib/db.js +1 -0
- package/lib/guard-jmap.js +321 -0
- package/lib/guard-managesieve-command.js +566 -0
- package/lib/guard-pop3-command.js +317 -0
- package/lib/guard-smtp-command.js +58 -3
- package/lib/mail-agent.js +20 -7
- package/lib/mail-arc-sign.js +12 -8
- package/lib/mail-auth.js +323 -34
- package/lib/mail-crypto-pgp.js +934 -0
- package/lib/mail-crypto-smime.js +340 -0
- package/lib/mail-crypto.js +108 -0
- package/lib/mail-dav.js +1224 -0
- package/lib/mail-deploy.js +492 -0
- package/lib/mail-dkim.js +431 -26
- package/lib/mail-journal.js +435 -0
- package/lib/mail-scan.js +502 -0
- package/lib/mail-server-imap.js +64 -26
- package/lib/mail-server-jmap.js +488 -0
- package/lib/mail-server-managesieve.js +853 -0
- package/lib/mail-server-mx.js +40 -30
- package/lib/mail-server-pop3.js +836 -0
- package/lib/mail-server-rate-limit.js +13 -0
- package/lib/mail-server-submission.js +70 -24
- package/lib/mail-server-tls.js +445 -0
- package/lib/mail-sieve.js +557 -0
- package/lib/mail-spam-score.js +284 -0
- package/lib/mail.js +99 -0
- package/lib/metrics.js +80 -3
- package/lib/middleware/dpop.js +58 -3
- package/lib/middleware/idempotency-key.js +255 -42
- package/lib/middleware/protected-resource-metadata.js +114 -2
- package/lib/network-dns-resolver.js +33 -0
- package/lib/network-tls.js +46 -0
- package/lib/outbox.js +62 -12
- package/lib/pqc-agent.js +13 -5
- package/lib/retry.js +23 -9
- package/lib/router.js +23 -1
- package/lib/safe-ical.js +634 -0
- package/lib/safe-icap.js +502 -0
- package/lib/safe-mime.js +15 -0
- package/lib/safe-sieve.js +684 -0
- package/lib/safe-smtp.js +57 -0
- package/lib/safe-url.js +37 -0
- package/lib/safe-vcard.js +473 -0
- package/lib/self-update-standalone-verifier.js +32 -3
- package/lib/self-update.js +153 -33
- package/lib/vendor/MANIFEST.json +161 -156
- package/lib/vendor-data.js +127 -9
- package/lib/vex.js +324 -59
- package/package.json +1 -1
- 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
|
+
};
|