@blamejs/core 0.18.53 → 0.18.55
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 +228 -0
- package/NOTICE +1 -1
- package/README.md +5 -5
- package/lib/agent-audit.js +27 -2
- package/lib/ai-adverse-decision.js +18 -2
- package/lib/audit-sign.js +24 -5
- package/lib/auth/passkey.js +4 -1
- package/lib/codepoint-class.js +72 -0
- package/lib/cookies.js +7 -10
- package/lib/credential-hash.js +8 -1
- package/lib/crypto.js +7 -5
- package/lib/db-file-lifecycle.js +14 -3
- package/lib/db.js +505 -49
- package/lib/guard-auth.js +34 -11
- package/lib/guard-filename.js +41 -33
- package/lib/guard-html.js +10 -2
- package/lib/guard-list-unsubscribe.js +6 -1
- package/lib/guard-managesieve-command.js +73 -12
- package/lib/guard-regex.js +3 -5
- package/lib/guard-smtp-command.js +20 -4
- package/lib/guard-svg.js +6 -1
- package/lib/guard-yaml.js +60 -15
- package/lib/http-client.js +17 -3
- package/lib/mail-agent.js +29 -13
- package/lib/mail-arc-sign.js +40 -7
- package/lib/mail-auth.js +134 -22
- package/lib/mail-crypto-pgp.js +1 -1
- package/lib/mail-dkim.js +80 -11
- package/lib/mail-helo.js +10 -0
- package/lib/mail-rbl.js +10 -3
- package/lib/mail-send-deliver.js +151 -32
- package/lib/mail-server-imap.js +186 -89
- package/lib/mail-server-jmap.js +31 -4
- package/lib/mail-server-managesieve.js +198 -42
- package/lib/mail-server-mx.js +191 -38
- package/lib/mail-server-net.js +281 -1
- package/lib/mail-server-pop3.js +89 -41
- package/lib/mail-server-rate-limit.js +104 -6
- package/lib/mail-server-submission.js +183 -35
- package/lib/mail-server-tls.js +48 -3
- package/lib/mail-store.js +33 -11
- package/lib/mail.js +355 -17
- package/lib/mcp.js +11 -3
- package/lib/middleware/bearer-auth.js +6 -1
- package/lib/middleware/fetch-metadata.js +5 -1
- package/lib/middleware/headers.js +7 -10
- package/lib/middleware/require-mtls.js +8 -1
- package/lib/network-dns-resolver.js +71 -8
- package/lib/network-dns.js +26 -0
- package/lib/network-smtp-policy.js +42 -10
- package/lib/network-tls.js +18 -0
- package/lib/redact.js +13 -3
- package/lib/retention.js +22 -2
- package/lib/safe-mount-info.js +39 -6
- package/lib/safe-smtp.js +96 -1
- package/lib/safe-url.js +8 -2
- package/lib/self-update.js +4 -1
- package/lib/vendor/MANIFEST.json +12 -12
- package/lib/vendor/blamejs-pki.cjs +672 -75
- package/lib/watcher.js +31 -6
- package/lib/ws-client.js +17 -2
- package/lib/yaml-lex.js +55 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/mail-server-imap.js
CHANGED
|
@@ -98,10 +98,18 @@
|
|
|
98
98
|
*
|
|
99
99
|
* ## What v1 does NOT ship
|
|
100
100
|
*
|
|
101
|
-
* - **SEARCH** — operator
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
101
|
+
* - **SEARCH / COPY / MOVE** — operator-domain logic against the
|
|
102
|
+
* mailStore index, supplied through `opts.overrides`; until then
|
|
103
|
+
* the listener answers `NO <verb> not configured`. A handler
|
|
104
|
+
* supplied once serves BOTH the plain and the `UID` form: the UID
|
|
105
|
+
* verb dispatches back through the same registry entry and sets
|
|
106
|
+
* `parsed.useUid`, which the handler reads to answer in unique
|
|
107
|
+
* identifiers per RFC 9051 §6.4.9.
|
|
108
|
+
* - **UID EXPUNGE (RFC 4315)** — refused unless the operator
|
|
109
|
+
* supplies an EXPUNGE handler that reads the uid-set. The shipped
|
|
110
|
+
* EXPUNGE takes no set and expunges every message flagged
|
|
111
|
+
* `\Deleted`, so serving the UID form from it would delete
|
|
112
|
+
* messages the client did not name.
|
|
105
113
|
* - **NOTIFY (RFC 5465)**, **METADATA (RFC 5464)**, **CATENATE
|
|
106
114
|
* (RFC 4469)**, **URLAUTH (RFC 4467)**, **IMAPSIEVE (RFC 6785)**,
|
|
107
115
|
* **COMPRESS=DEFLATE (RFC 4978)** — opt-in / refused.
|
|
@@ -121,7 +129,6 @@
|
|
|
121
129
|
var net = require("node:net");
|
|
122
130
|
var safeBuffer = require("./safe-buffer");
|
|
123
131
|
var C = require("./constants");
|
|
124
|
-
var bCrypto = require("./crypto");
|
|
125
132
|
var numericBounds = require("./numeric-bounds");
|
|
126
133
|
var validateOpts = require("./validate-opts");
|
|
127
134
|
var guardImapCommand = require("./guard-imap-command");
|
|
@@ -147,6 +154,7 @@ var codepointClass = require("./codepoint-class");
|
|
|
147
154
|
// Centralized so the marker lives in one place
|
|
148
155
|
// and the per-call sites read cleanly.
|
|
149
156
|
var ERR_CLAMP = 200; // protocol-reply error-message clamp
|
|
157
|
+
var CRLF_BYTES = Buffer.from("\r\n", "latin1"); // RFC 9051 §2.2 line terminator, as octets
|
|
150
158
|
var LINE_PREVIEW = 80; // audit-line preview clamp
|
|
151
159
|
|
|
152
160
|
// RFC 9051 §6.3.12 + RFC 5322 §3.3 date-time parser for IMAP APPEND.
|
|
@@ -228,6 +236,7 @@ function _validateMailboxName(name, opts) {
|
|
|
228
236
|
* maxLineBytes: number, // default 8192
|
|
229
237
|
* maxLiteralBytes: number, // default 64 MiB
|
|
230
238
|
* idleTimeoutMs: number, // default 30 min
|
|
239
|
+
* maxConnections: number, // default 1024 — listener-wide ceiling
|
|
231
240
|
* profile: "strict" | "balanced" | "permissive",
|
|
232
241
|
* auth: {
|
|
233
242
|
* mechanisms: ["PLAIN", "LOGIN", "SCRAM-SHA-256", "EXTERNAL", "XOAUTH2"],
|
|
@@ -237,6 +246,15 @@ function _validateMailboxName(name, opts) {
|
|
|
237
246
|
* rateLimit: b.mail.server.rateLimit handle | opts | false,
|
|
238
247
|
* audit: b.audit // optional
|
|
239
248
|
*
|
|
249
|
+
* `fetchRange` may return each row's `payload` as a Buffer, and should whenever
|
|
250
|
+
* the row carries message content. RFC 9051 §4.3 makes a literal a counted
|
|
251
|
+
* sequence of octets, and a string cannot hold one: a message octet that is not
|
|
252
|
+
* valid UTF-8 does not survive being encoded on the way to the socket, and the
|
|
253
|
+
* count the response announced stops matching the number of octets it wrote —
|
|
254
|
+
* which is what tells a client where the response ends. A Buffer payload is
|
|
255
|
+
* framed and written as the octets it holds. A string payload is unchanged and
|
|
256
|
+
* remains right for the rows that carry only attributes, such as `FLAGS (\Seen)`.
|
|
257
|
+
*
|
|
240
258
|
* @example
|
|
241
259
|
* var imap = b.mail.server.imap.create({
|
|
242
260
|
* tlsContext: b.mail.server.tls.context({ certFile, keyFile }).secureContext,
|
|
@@ -264,7 +282,7 @@ function create(opts) {
|
|
|
264
282
|
"mail.server.imap.create: mailStore is required (compose b.mailStore.create({ backend: ... }))");
|
|
265
283
|
}
|
|
266
284
|
numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
|
|
267
|
-
["maxLineBytes", "maxLiteralBytes", "idleTimeoutMs"],
|
|
285
|
+
["maxLineBytes", "maxLiteralBytes", "idleTimeoutMs", "maxConnections"],
|
|
268
286
|
"mail.server.imap.", MailServerImapError, "mail-server-imap/bad-bound");
|
|
269
287
|
|
|
270
288
|
var greeting = opts.greeting || DEFAULT_GREETING_VENDOR;
|
|
@@ -283,16 +301,18 @@ function create(opts) {
|
|
|
283
301
|
var _emit = auditEmit.emit;
|
|
284
302
|
|
|
285
303
|
function _handleConnection(rawSocket) {
|
|
286
|
-
var
|
|
304
|
+
var accepted = mailServerNet.acceptConnection(rawSocket, {
|
|
305
|
+
rateLimit: rateLimit,
|
|
306
|
+
connections: connections,
|
|
307
|
+
emit: _emit,
|
|
287
308
|
refusedEvent: "mail.server.imap.rate_limit_refused",
|
|
288
309
|
refusalLine: "* BAD Too many connections from your IP\r\n",
|
|
310
|
+
idPrefix: "imapconn-",
|
|
289
311
|
});
|
|
290
|
-
if (
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
var
|
|
294
|
-
var socket = rawSocket;
|
|
295
|
-
connections.add(socket);
|
|
312
|
+
if (accepted === null) return;
|
|
313
|
+
var remoteAddress = accepted.remoteAddress;
|
|
314
|
+
var connectionId = accepted.connectionId;
|
|
315
|
+
var socket = accepted.socket;
|
|
296
316
|
|
|
297
317
|
var state = {
|
|
298
318
|
id: connectionId,
|
|
@@ -450,7 +470,14 @@ function create(opts) {
|
|
|
450
470
|
synchronizing: !parsed.literalNonSync,
|
|
451
471
|
};
|
|
452
472
|
if (!parsed.literalNonSync) {
|
|
453
|
-
|
|
473
|
+
// RFC 9051 §7.5 — a synchronizing literal is answered with a command
|
|
474
|
+
// continuation request: a line that BEGINS with `+`. Written as an
|
|
475
|
+
// untagged response, the wire carried `* + Ready for literal data`,
|
|
476
|
+
// which is not one, so a conforming client waited for a `+` line that
|
|
477
|
+
// never came and APPEND could not complete. The non-synchronizing
|
|
478
|
+
// route is no escape either: the strict profile's guard refuses
|
|
479
|
+
// LITERAL+ and CAPABILITY does not advertise it.
|
|
480
|
+
_writeContinuation(socket, "Ready for literal data");
|
|
454
481
|
}
|
|
455
482
|
return;
|
|
456
483
|
}
|
|
@@ -731,7 +758,7 @@ function create(opts) {
|
|
|
731
758
|
} else if (event.kind === "LIST") {
|
|
732
759
|
_writeUntagged(socket, "LIST " + event.payload);
|
|
733
760
|
} else if (event.kind === "FETCH") {
|
|
734
|
-
_writeUntagged(socket, (event.seq || ""
|
|
761
|
+
_writeUntagged(socket, _fetchResponse(event.seq || "", event.payload));
|
|
735
762
|
}
|
|
736
763
|
} catch (_e) { /* drop-silent — socket may already be closed */ }
|
|
737
764
|
});
|
|
@@ -967,56 +994,49 @@ function create(opts) {
|
|
|
967
994
|
}
|
|
968
995
|
_emit("mail.server.imap.auth_attempt",
|
|
969
996
|
{ connectionId: state.id, mechanism: mechName, remoteAddress: state.remoteAddress });
|
|
970
|
-
state.authPending = {
|
|
997
|
+
state.authPending = { mech: mechName, tag: tag, step: 0 };
|
|
971
998
|
_runAuthStep(state, socket, initialResp);
|
|
972
999
|
}
|
|
973
1000
|
|
|
974
1001
|
function _runAuthStep(state, socket, clientResp) {
|
|
975
1002
|
var pending = state.authPending;
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
{ connectionId: state.id, mechanism: pending.mechanism,
|
|
999
|
-
tenantId: result.actor.tenantId || null });
|
|
1000
|
-
_writeTagged(socket, savedTag, "OK [CAPABILITY " + _capabilityLine(state) + "] AUTHENTICATE completed");
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
|
-
var failTag = pending.tag;
|
|
1004
|
-
state.authPending = null;
|
|
1005
|
-
rateLimit.noteAuthFailure(state.remoteAddress);
|
|
1006
|
-
_emit("mail.server.imap.auth_failed",
|
|
1007
|
-
{ connectionId: state.id, mechanism: pending.mechanism,
|
|
1008
|
-
reason: (result && result.reason) || "verify-returned-fail" }, "denied");
|
|
1009
|
-
_writeTagged(socket, failTag, "NO Authentication credentials invalid");
|
|
1010
|
-
})
|
|
1011
|
-
.catch(function (err) {
|
|
1012
|
-
var failTag = pending.tag;
|
|
1003
|
+
function _fail(reason, outcome, reply) {
|
|
1004
|
+
var failTag = pending.tag;
|
|
1005
|
+
state.authPending = null;
|
|
1006
|
+
rateLimit.noteAuthFailure(state.remoteAddress);
|
|
1007
|
+
_emit("mail.server.imap.auth_failed",
|
|
1008
|
+
{ connectionId: state.id, mechanism: pending.mech, reason: reason }, outcome);
|
|
1009
|
+
_writeTagged(socket, failTag, reply);
|
|
1010
|
+
}
|
|
1011
|
+
mailServerNet.runSaslStep({
|
|
1012
|
+
exchange: pending,
|
|
1013
|
+
verify: authConfig.verify,
|
|
1014
|
+
credentials: { tls: state.tls, remoteAddress: state.remoteAddress },
|
|
1015
|
+
clientResponse: clientResp,
|
|
1016
|
+
// Server-side challenge — `+ <base64>` per RFC 9051 §6.2.2.
|
|
1017
|
+
writeChallenge: function (ch) { return _writeContinuation(socket, ch); },
|
|
1018
|
+
onChallengeUnsafe: function () {
|
|
1019
|
+
_fail("challenge-contains-line-terminator", "denied", "NO Authentication failed");
|
|
1020
|
+
},
|
|
1021
|
+
onSuccess: function (result) {
|
|
1022
|
+
state.actor = result.actor;
|
|
1023
|
+
state.stage = "authenticated";
|
|
1024
|
+
var savedTag = pending.tag;
|
|
1013
1025
|
state.authPending = null;
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
}
|
|
1026
|
+
_emit("mail.server.imap.auth_success",
|
|
1027
|
+
{ connectionId: state.id, mechanism: pending.mech,
|
|
1028
|
+
tenantId: result.actor.tenantId || null });
|
|
1029
|
+
_writeTagged(socket, savedTag,
|
|
1030
|
+
"OK [CAPABILITY " + _capabilityLine(state) + "] AUTHENTICATE completed");
|
|
1031
|
+
},
|
|
1032
|
+
onFailure: function (result) {
|
|
1033
|
+
_fail((result && result.reason) || "verify-returned-fail", "denied",
|
|
1034
|
+
"NO Authentication credentials invalid");
|
|
1035
|
+
},
|
|
1036
|
+
onError: function (err) {
|
|
1037
|
+
_fail((err && err.message) || String(err), "failure", "NO Authentication failed");
|
|
1038
|
+
},
|
|
1039
|
+
});
|
|
1020
1040
|
}
|
|
1021
1041
|
|
|
1022
1042
|
function _handleLogin(state, socket, tag, args) {
|
|
@@ -1582,10 +1602,20 @@ function create(opts) {
|
|
|
1582
1602
|
for (var i = 0; i < rs.length; i += 1) {
|
|
1583
1603
|
var r = rs[i];
|
|
1584
1604
|
var payload = r.payload || "";
|
|
1585
|
-
|
|
1586
|
-
|
|
1605
|
+
// A Buffer payload is the backend saying "these are the message's
|
|
1606
|
+
// own octets". It is assembled and written as octets end to end, so
|
|
1607
|
+
// the count in the literal header is the count that reaches the wire.
|
|
1608
|
+
// Concatenating it into a string instead re-encoded it as UTF-8, and
|
|
1609
|
+
// the response then announced one length and sent another.
|
|
1610
|
+
var octets = Buffer.isBuffer(payload);
|
|
1611
|
+
var asText = octets ? payload.toString("latin1") : String(payload);
|
|
1612
|
+
if (includeModseq && r.modseq !== undefined && !/MODSEQ\s*\(/.test(asText)) {
|
|
1613
|
+
var modseqAttr = (asText ? " " : "") + "MODSEQ (" + r.modseq + ")";
|
|
1614
|
+
payload = octets
|
|
1615
|
+
? Buffer.concat([payload, Buffer.from(modseqAttr, "latin1")])
|
|
1616
|
+
: asText + modseqAttr;
|
|
1587
1617
|
}
|
|
1588
|
-
_writeUntagged(socket, r.seq
|
|
1618
|
+
_writeUntagged(socket, _fetchResponse(r.seq, payload));
|
|
1589
1619
|
}
|
|
1590
1620
|
_writeTagged(socket, tag, "OK FETCH completed");
|
|
1591
1621
|
})
|
|
@@ -1713,29 +1743,49 @@ function create(opts) {
|
|
|
1713
1743
|
var subArgs = sub[2];
|
|
1714
1744
|
if (subVerb === "FETCH") return _handleFetch(state, socket, tag, subArgs, true);
|
|
1715
1745
|
if (subVerb === "STORE") return _handleStore(state, socket, tag, subArgs, true);
|
|
1716
|
-
// RFC 9051 §6.4.9
|
|
1717
|
-
//
|
|
1746
|
+
// Every other sub-command RFC 9051 §6.4.9 defines — SEARCH, COPY, MOVE,
|
|
1747
|
+
// EXPUNGE — goes back through the registry to the SAME entry the
|
|
1748
|
+
// unprefixed verb dispatches to, carrying `useUid` so the handler answers
|
|
1749
|
+
// in unique identifiers.
|
|
1718
1750
|
//
|
|
1719
|
-
//
|
|
1720
|
-
//
|
|
1721
|
-
//
|
|
1722
|
-
//
|
|
1723
|
-
//
|
|
1724
|
-
//
|
|
1725
|
-
//
|
|
1726
|
-
//
|
|
1751
|
+
// The listener ships no search, copy or move of its own: those are
|
|
1752
|
+
// operator-domain, and the registry holds a "not configured" default that
|
|
1753
|
+
// a consumer replaces through `opts.overrides`. Dispatching the UID forms
|
|
1754
|
+
// here instead of through the registry made that seam reachable from one
|
|
1755
|
+
// side only — a consumer who supplied SEARCH got the sequence form served
|
|
1756
|
+
// and the UID form refused, and the only way to supply the UID form was to
|
|
1757
|
+
// replace the whole UID verb, taking the working UID FETCH and UID STORE
|
|
1758
|
+
// with it. A client that keeps a cross-session cache asks for the UID form
|
|
1759
|
+
// precisely because a sequence number does not survive the session, so the
|
|
1760
|
+
// refusal fell on the clients doing the durable thing.
|
|
1727
1761
|
//
|
|
1728
|
-
//
|
|
1729
|
-
//
|
|
1730
|
-
//
|
|
1731
|
-
//
|
|
1732
|
-
//
|
|
1733
|
-
//
|
|
1734
|
-
//
|
|
1735
|
-
//
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1762
|
+
// Going through the registry also keeps the tenant check, the guard
|
|
1763
|
+
// validation and the audit emission on the UID path, which a direct call
|
|
1764
|
+
// from here would have skipped.
|
|
1765
|
+
// An allowlist, not "whatever the registry holds". UID takes the five
|
|
1766
|
+
// sub-commands §6.4.9 names plus EXPUNGE from RFC 4315; sending anything
|
|
1767
|
+
// else through would dispatch a real handler that knows nothing about UIDs
|
|
1768
|
+
// — `UID SELECT INBOX` would select a mailbox, and `UID UID …` would
|
|
1769
|
+
// recurse.
|
|
1770
|
+
if (subVerb !== "SEARCH" && subVerb !== "COPY" &&
|
|
1771
|
+
subVerb !== "MOVE" && subVerb !== "EXPUNGE") {
|
|
1772
|
+
_writeTagged(socket, tag, "BAD UID " + subVerb + " is not a UID sub-command");
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
// EXPUNGE is the one that cannot be forwarded blind. RFC 4315 §2.1 makes
|
|
1776
|
+
// `UID EXPUNGE <uid-set>` expunge ONLY the named messages, while the
|
|
1777
|
+
// listener's own EXPUNGE expunges every message flagged \Deleted and takes
|
|
1778
|
+
// no set at all. Forwarding to it would delete messages the client did not
|
|
1779
|
+
// name, which is worse than not answering. A consumer who supplies their
|
|
1780
|
+
// own EXPUNGE reads `useUid` and the set, so theirs can serve it.
|
|
1781
|
+
if (subVerb === "EXPUNGE" && _registry.source("EXPUNGE") !== "operator-override") {
|
|
1782
|
+
_writeTagged(socket, tag,
|
|
1783
|
+
"NO UID EXPUNGE needs a uid-set-aware EXPUNGE handler; " +
|
|
1784
|
+
"the default expunges by \\Deleted flag and would exceed the set");
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
return _registry.dispatch(subVerb, state, socket,
|
|
1788
|
+
{ tag: tag, args: subArgs, useUid: true });
|
|
1739
1789
|
}
|
|
1740
1790
|
|
|
1741
1791
|
function _handleIdle(state, socket, tag) {
|
|
@@ -1754,17 +1804,63 @@ function create(opts) {
|
|
|
1754
1804
|
state.idle = { tag: tag, timer: timer };
|
|
1755
1805
|
}
|
|
1756
1806
|
|
|
1807
|
+
// One line onto the socket, as OCTETS.
|
|
1808
|
+
//
|
|
1809
|
+
// RFC 9051 §4.3 makes a literal a counted sequence of octets, so a response
|
|
1810
|
+
// carrying message content has to reach the wire as the octets the backend
|
|
1811
|
+
// holds. `socket.write(string)` encodes as UTF-8, which replaces every
|
|
1812
|
+
// sequence that is not valid UTF-8 and changes the length — so the count the
|
|
1813
|
+
// response announced and the number of octets it wrote disagreed, and a
|
|
1814
|
+
// client, which uses that count to find the end of the response, read the
|
|
1815
|
+
// next response as part of this one.
|
|
1816
|
+
//
|
|
1817
|
+
// A Buffer `msg` is written through untouched. A string keeps its UTF-8
|
|
1818
|
+
// encoding, which is what RFC 9051 §5.1 asks for in the one place a response
|
|
1819
|
+
// string is not ASCII: a mailbox name, once the client has enabled UTF8=ACCEPT.
|
|
1820
|
+
// Encoding those latin1 instead would keep the low byte of each character and
|
|
1821
|
+
// corrupt every name outside Latin-1 — the same defect this fixes, moved.
|
|
1822
|
+
function _writeLine(socket, prefix, msg) {
|
|
1823
|
+
try {
|
|
1824
|
+
if (Buffer.isBuffer(msg)) {
|
|
1825
|
+
socket.write(Buffer.concat([Buffer.from(prefix, "latin1"), msg, CRLF_BYTES]));
|
|
1826
|
+
} else {
|
|
1827
|
+
socket.write(prefix + msg + "\r\n");
|
|
1828
|
+
}
|
|
1829
|
+
} catch (_e) { /* socket may be down */ }
|
|
1830
|
+
}
|
|
1831
|
+
// One untagged FETCH response. A Buffer payload is the backend saying "these
|
|
1832
|
+
// are the message's own octets", and the response is assembled as octets so
|
|
1833
|
+
// the count in the literal header is the count that reaches the wire; a
|
|
1834
|
+
// string payload is attributes and stays a string.
|
|
1835
|
+
//
|
|
1836
|
+
// Two places build one of these — the FETCH command and a NOTIFY push — and
|
|
1837
|
+
// a second copy of this is how the octet handling ends up right in one and
|
|
1838
|
+
// wrong in the other, which is what it was.
|
|
1839
|
+
function _fetchResponse(seq, payload) {
|
|
1840
|
+
if (Buffer.isBuffer(payload)) {
|
|
1841
|
+
return Buffer.concat([
|
|
1842
|
+
Buffer.from(seq + " FETCH (", "latin1"), payload, Buffer.from(")", "latin1"),
|
|
1843
|
+
]);
|
|
1844
|
+
}
|
|
1845
|
+
return seq + " FETCH (" + (payload || "") + ")";
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1757
1848
|
function _writeTagged(socket, tag, msg) {
|
|
1758
|
-
|
|
1759
|
-
catch (_e) { /* socket may be down */ }
|
|
1849
|
+
_writeLine(socket, tag + " ", msg);
|
|
1760
1850
|
}
|
|
1761
1851
|
function _writeUntagged(socket, msg) {
|
|
1762
|
-
|
|
1763
|
-
catch (_e) { /* socket may be down */ }
|
|
1852
|
+
_writeLine(socket, "* ", msg);
|
|
1764
1853
|
}
|
|
1854
|
+
// RFC 9051 §7.5 continuation. Returns false when the operator's challenge
|
|
1855
|
+
// carries CR / LF / NUL: those end the line early and the remainder is read
|
|
1856
|
+
// by the client as a second server response. Not hypothetical for a SCRAM or
|
|
1857
|
+
// CRAM mechanism, whose challenge is composed partly from the client's own
|
|
1858
|
+
// nonce, so client bytes reach this write.
|
|
1765
1859
|
function _writeContinuation(socket, msg) {
|
|
1766
|
-
|
|
1767
|
-
|
|
1860
|
+
var safe = mailServerNet.saslChallengeOrNull(msg);
|
|
1861
|
+
if (safe === null) return false;
|
|
1862
|
+
_writeLine(socket, "+ ", safe);
|
|
1863
|
+
return true;
|
|
1768
1864
|
}
|
|
1769
1865
|
function _close(socket, state) {
|
|
1770
1866
|
// The drain loop's `if (state.stage === "closed") return;` guard
|
|
@@ -1789,6 +1885,7 @@ function create(opts) {
|
|
|
1789
1885
|
// ---- Lifecycle ----------------------------------------------------------
|
|
1790
1886
|
return mailServerNet.createStoreServer(net, {
|
|
1791
1887
|
defaultPort: 143, // RFC 9051 IMAP port (IANA)
|
|
1888
|
+
maxConnections: opts.maxConnections,
|
|
1792
1889
|
handleConnection: _handleConnection,
|
|
1793
1890
|
errorClass: MailServerImapError,
|
|
1794
1891
|
errorCodePrefix: "mail-server-imap/",
|
package/lib/mail-server-jmap.js
CHANGED
|
@@ -158,6 +158,11 @@ void C;
|
|
|
158
158
|
* // capabilities the server advertises beyond core
|
|
159
159
|
* accountsFor: async function (actor) → { primaryAccounts, accounts },
|
|
160
160
|
* // operator-supplied accountId enumeration
|
|
161
|
+
* webSocket: boolean, // default true — false stops advertising the
|
|
162
|
+
* // RFC 8887 WebSocket transport (capability and the
|
|
163
|
+
* // top-level webSocketUrl alias), for a deployment
|
|
164
|
+
* // that has not wired the upgrade handler
|
|
165
|
+
* webSocketUrl: string, // default "/jmap/ws" — where the upgrade lives
|
|
161
166
|
* profile: "strict" | "balanced" | "permissive",
|
|
162
167
|
* posture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
|
|
163
168
|
* audit: b.audit // optional
|
|
@@ -199,6 +204,20 @@ function create(opts) {
|
|
|
199
204
|
var profile = opts.profile || DEFAULT_PROFILE;
|
|
200
205
|
var posture = opts.posture || null;
|
|
201
206
|
var serverCapabilities = opts.serverCapabilities || {};
|
|
207
|
+
// RFC 8887 §3 — whether this deployment offers the WebSocket transport. It
|
|
208
|
+
// was advertised unconditionally: the capability was injected when the
|
|
209
|
+
// consumer omitted the key, and merged back in when they supplied one, so a
|
|
210
|
+
// deployment that had not wired the upgrade handler still sent conforming
|
|
211
|
+
// clients to an endpoint that could not upgrade. The only value that
|
|
212
|
+
// suppressed it was `undefined`, and only because JSON.stringify drops such
|
|
213
|
+
// keys — an accident of serialization rather than an answer. `false` is now
|
|
214
|
+
// that answer, and a value that is neither true nor false is a
|
|
215
|
+
// misconfiguration rather than a silent default back to advertising.
|
|
216
|
+
validateOpts.optionalBoolean(opts.webSocket,
|
|
217
|
+
"mail.server.jmap.create: opts.webSocket (false stops advertising the RFC 8887 " +
|
|
218
|
+
"WebSocket transport, for a deployment that has not wired the upgrade handler)",
|
|
219
|
+
MailServerJmapError, "mail-server-jmap/bad-websocket");
|
|
220
|
+
var webSocketEnabled = opts.webSocket !== false;
|
|
202
221
|
|
|
203
222
|
// JMAP method registry. Wrap operator-supplied `opts.methods` map
|
|
204
223
|
// through `b.mail.serverRegistry` so per-handler resource budgets
|
|
@@ -569,14 +588,19 @@ function create(opts) {
|
|
|
569
588
|
var defaultCaps = { "urn:ietf:params:jmap:core": {} };
|
|
570
589
|
var hasOperatorWsCap = Object.prototype.hasOwnProperty.call(
|
|
571
590
|
serverCapabilities, "urn:ietf:params:jmap:websocket");
|
|
572
|
-
if (!hasOperatorWsCap) {
|
|
591
|
+
if (webSocketEnabled && !hasOperatorWsCap) {
|
|
573
592
|
defaultCaps["urn:ietf:params:jmap:websocket"] = {
|
|
574
593
|
url: opts.webSocketUrl || "/jmap/ws",
|
|
575
594
|
supportsPush: true,
|
|
576
595
|
};
|
|
577
596
|
}
|
|
597
|
+
var caps = Object.assign({}, defaultCaps, serverCapabilities);
|
|
598
|
+
// `webSocket: false` is the deployment's answer, so it outranks a
|
|
599
|
+
// capability the consumer also listed in serverCapabilities — otherwise
|
|
600
|
+
// the merge above would put back exactly what was declined.
|
|
601
|
+
if (!webSocketEnabled) delete caps["urn:ietf:params:jmap:websocket"];
|
|
578
602
|
var session = {
|
|
579
|
-
capabilities:
|
|
603
|
+
capabilities: caps,
|
|
580
604
|
accounts: info.accounts || {},
|
|
581
605
|
primaryAccounts: info.primaryAccounts || {},
|
|
582
606
|
username: actor.username || actor.id || "unknown",
|
|
@@ -587,10 +611,13 @@ function create(opts) {
|
|
|
587
611
|
// RFC 8887 §3 — `webSocketUrl` advertises the JMAP WS
|
|
588
612
|
// endpoint. Operator overrides via opts.webSocketUrl; default
|
|
589
613
|
// mounts at `/jmap/ws`.
|
|
590
|
-
urlEndpointResolution: serverCapabilities["urn:ietf:params:jmap:websocket"]
|
|
614
|
+
urlEndpointResolution: (webSocketEnabled && serverCapabilities["urn:ietf:params:jmap:websocket"])
|
|
591
615
|
? { useEndpoint: opts.webSocketUrl || "/jmap/ws", urlPrefix: "" }
|
|
592
616
|
: undefined,
|
|
593
|
-
|
|
617
|
+
// The top-level alias goes with the capability. Leaving it behind
|
|
618
|
+
// would still point a client at an endpoint that cannot upgrade,
|
|
619
|
+
// which is the whole thing `webSocket: false` is declining.
|
|
620
|
+
webSocketUrl: webSocketEnabled ? (opts.webSocketUrl || "/jmap/ws") : undefined,
|
|
594
621
|
state: sessionState,
|
|
595
622
|
};
|
|
596
623
|
res.statusCode = 200;
|