@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,488 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.mail.server.jmap
|
|
4
|
+
* @nav Mail
|
|
5
|
+
* @title Mail JMAP Server
|
|
6
|
+
* @order 548
|
|
7
|
+
*
|
|
8
|
+
* @intro
|
|
9
|
+
* JMAP Core (RFC 8620) + JMAP Mail (RFC 8621) listener. Where IMAP
|
|
10
|
+
* is a TCP text-protocol with a connection state-machine, JMAP is
|
|
11
|
+
* HTTP-mounted JSON-RPC — operators mount the handler under their
|
|
12
|
+
* existing `b.router` / `b.createApp` and the JMAP semantics ride
|
|
13
|
+
* the HTTP request lifecycle (auth → body parse → handler →
|
|
14
|
+
* response).
|
|
15
|
+
*
|
|
16
|
+
* ## Public surface
|
|
17
|
+
*
|
|
18
|
+
* ```js
|
|
19
|
+
* var jmap = b.mail.server.jmap.create({
|
|
20
|
+
* mailStore: b.mailStore.create({ backend: b.db.handle() }),
|
|
21
|
+
* methods: {
|
|
22
|
+
* "Mailbox/get": async function (actor, args) {...},
|
|
23
|
+
* "Email/query": async function (actor, args) {...},
|
|
24
|
+
* "Email/get": async function (actor, args) {...},
|
|
25
|
+
* },
|
|
26
|
+
* serverCapabilities: {
|
|
27
|
+
* "urn:ietf:params:jmap:mail": { maxMailboxesPerEmail: null },
|
|
28
|
+
* "urn:ietf:params:jmap:submission": null,
|
|
29
|
+
* },
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* // Mount on the framework's router:
|
|
33
|
+
* app.use("/.well-known/jmap", jmap.discoveryHandler);
|
|
34
|
+
* app.use("/jmap/session", b.middleware.bearerAuth(...), jmap.sessionHandler);
|
|
35
|
+
* app.use("/jmap/api", b.middleware.bearerAuth(...), jmap.apiHandler);
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* The listener owns the request envelope (`b.guardJmap.validate`),
|
|
39
|
+
* back-reference resolution (RFC 8620 §3.7), the per-call dispatch,
|
|
40
|
+
* and the standard error mapping (RFC 8620 §3.6.1). Operators wire
|
|
41
|
+
* the actual method implementations — JMAP semantics are too varied
|
|
42
|
+
* (Mailbox / Email / Thread / SearchSnippet / Identity /
|
|
43
|
+
* EmailSubmission) to enshrine in v1.
|
|
44
|
+
*
|
|
45
|
+
* ## Capability discovery (RFC 8620 §2)
|
|
46
|
+
*
|
|
47
|
+
* GET `/.well-known/jmap` redirects to the session resource per
|
|
48
|
+
* §2.2. GET `/jmap/session` returns the session object with the
|
|
49
|
+
* server's capabilities, account list (operator-supplied via
|
|
50
|
+
* `opts.accountsFor(actor)`), and endpoint URLs.
|
|
51
|
+
*
|
|
52
|
+
* ## Request shape (RFC 8620 §3.3)
|
|
53
|
+
*
|
|
54
|
+
* POST `/jmap/api` with body:
|
|
55
|
+
*
|
|
56
|
+
* ```json
|
|
57
|
+
* {
|
|
58
|
+
* "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
|
|
59
|
+
* "methodCalls": [
|
|
60
|
+
* ["Mailbox/get", { "accountId": "A1" }, "c0"],
|
|
61
|
+
* ["Email/query", { "filter": { "inMailbox": "#c0/list/0/id" } }, "c1"]
|
|
62
|
+
* ]
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* Response shape:
|
|
67
|
+
*
|
|
68
|
+
* ```json
|
|
69
|
+
* {
|
|
70
|
+
* "methodResponses": [
|
|
71
|
+
* ["Mailbox/get", { ... }, "c0"],
|
|
72
|
+
* ["Email/query", { ... }, "c1"]
|
|
73
|
+
* ],
|
|
74
|
+
* "sessionState": "<opaque-token>"
|
|
75
|
+
* }
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* ## Caps (RFC 8620 §3.6)
|
|
79
|
+
*
|
|
80
|
+
* Enforced via `b.guardJmap.validate` — `maxCallsInRequest`,
|
|
81
|
+
* `maxSizeRequest`, `maxObjectsInGet/Set`, `maxBackRefDepth`. Per-
|
|
82
|
+
* account method-call concurrent cap via `b.mail.server.rateLimit`
|
|
83
|
+
* when wired.
|
|
84
|
+
*
|
|
85
|
+
* ## Error vocabulary (RFC 8620 §3.6.1)
|
|
86
|
+
*
|
|
87
|
+
* Standard errors emitted as the methodResponse object:
|
|
88
|
+
*
|
|
89
|
+
* - `urn:ietf:params:jmap:error:requestTooLarge`
|
|
90
|
+
* - `urn:ietf:params:jmap:error:invalidArguments`
|
|
91
|
+
* - `urn:ietf:params:jmap:error:invalidResultReference`
|
|
92
|
+
* - `urn:ietf:params:jmap:error:unknownCapability`
|
|
93
|
+
* - `urn:ietf:params:jmap:error:limit/<name>`
|
|
94
|
+
* - `urn:ietf:params:jmap:error:forbidden`
|
|
95
|
+
* - `urn:ietf:params:jmap:error:accountNotFound`
|
|
96
|
+
* - `urn:ietf:params:jmap:error:serverFail` (opaque last-resort)
|
|
97
|
+
*
|
|
98
|
+
* ## What v1 does NOT ship
|
|
99
|
+
*
|
|
100
|
+
* - **Push channel (SSE + WebSocket per RFC 8887)** — operator wires
|
|
101
|
+
* `b.sse` or `b.websocket` to the `pushSubscribe` hook. v1.5
|
|
102
|
+
* bundles a turnkey push handler.
|
|
103
|
+
* - **Blob upload/download endpoints** — operator wires their own
|
|
104
|
+
* `/jmap/upload` / `/jmap/download` handlers; the framework
|
|
105
|
+
* supplies `b.storage` + `b.objectStore` + the guard-* family
|
|
106
|
+
* for the actual upload path.
|
|
107
|
+
* - **EmailSubmission (RFC 8621 §7)** — operator wires the bridge
|
|
108
|
+
* to `b.mail.server.submission`'s outbound agent.
|
|
109
|
+
* - **Calendars / Contacts (RFC 9610)**, **Sieve (RFC 9404)**,
|
|
110
|
+
* **MDN (RFC 9007)** — opt-in capabilities.
|
|
111
|
+
*
|
|
112
|
+
* @card
|
|
113
|
+
* JMAP Core (RFC 8620) + JMAP Mail (RFC 8621) listener. HTTP-mounted
|
|
114
|
+
* JSON-RPC. Composes b.guardJmap (request-envelope validator) +
|
|
115
|
+
* operator-supplied method handlers + b.mailStore. Per-account back-
|
|
116
|
+
* reference resolution (RFC 8620 §3.7) + standard error vocabulary
|
|
117
|
+
* (RFC 8620 §3.6.1) handled at the listener boundary.
|
|
118
|
+
*/
|
|
119
|
+
|
|
120
|
+
var lazyRequire = require("./lazy-require");
|
|
121
|
+
var C = require("./constants");
|
|
122
|
+
var bCrypto = require("./crypto");
|
|
123
|
+
var safeJson = require("./safe-json");
|
|
124
|
+
var validateOpts = require("./validate-opts");
|
|
125
|
+
var guardJmap = require("./guard-jmap");
|
|
126
|
+
var { defineClass } = require("./framework-error");
|
|
127
|
+
|
|
128
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
129
|
+
|
|
130
|
+
var MailServerJmapError = defineClass("MailServerJmapError", { alwaysPermanent: true });
|
|
131
|
+
|
|
132
|
+
var DEFAULT_PROFILE = "strict";
|
|
133
|
+
var WELL_KNOWN_PATH = "/.well-known/jmap";
|
|
134
|
+
void C; // reserved for future cap constants
|
|
135
|
+
void WELL_KNOWN_PATH;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @primitive b.mail.server.jmap.create
|
|
139
|
+
* @signature b.mail.server.jmap.create(opts)
|
|
140
|
+
* @since 0.9.50
|
|
141
|
+
* @status stable
|
|
142
|
+
* @related b.mail.server.imap.create, b.guardJmap.validate, b.mailStore.create
|
|
143
|
+
*
|
|
144
|
+
* Build a JMAP Core + JMAP Mail listener. Returns a handle exposing
|
|
145
|
+
* `apiHandler` / `sessionHandler` / `discoveryHandler` (Express-style
|
|
146
|
+
* `(req, res, next)` functions) and `dispatch(actor, body)` for
|
|
147
|
+
* operators with a non-Express transport.
|
|
148
|
+
*
|
|
149
|
+
* @opts
|
|
150
|
+
* mailStore: b.mailStore handle (operator-supplied backend),
|
|
151
|
+
* methods: { "<Type>/<verb>": async fn(actor, args, ctx) },
|
|
152
|
+
* // operator-supplied JMAP method handlers
|
|
153
|
+
* serverCapabilities: { "<URI>": <capability-record> },
|
|
154
|
+
* // capabilities the server advertises beyond core
|
|
155
|
+
* accountsFor: async function (actor) → { primaryAccounts, accounts },
|
|
156
|
+
* // operator-supplied accountId enumeration
|
|
157
|
+
* profile: "strict" | "balanced" | "permissive",
|
|
158
|
+
* posture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
|
|
159
|
+
* audit: b.audit // optional
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* var jmap = b.mail.server.jmap.create({
|
|
163
|
+
* mailStore: b.mailStore.create({ backend: b.db.handle() }),
|
|
164
|
+
* methods: {
|
|
165
|
+
* "Mailbox/get": async function (actor, args) {
|
|
166
|
+
* return { accountId: args.accountId, list: [], notFound: [] };
|
|
167
|
+
* },
|
|
168
|
+
* },
|
|
169
|
+
* serverCapabilities: { "urn:ietf:params:jmap:mail": {} },
|
|
170
|
+
* accountsFor: async function (actor) {
|
|
171
|
+
* return {
|
|
172
|
+
* primaryAccounts: { "urn:ietf:params:jmap:mail": "A1" },
|
|
173
|
+
* accounts: { A1: { name: actor.username } },
|
|
174
|
+
* };
|
|
175
|
+
* },
|
|
176
|
+
* });
|
|
177
|
+
*
|
|
178
|
+
* app.post("/jmap/api", b.middleware.bearerAuth({ verify: verify }), jmap.apiHandler);
|
|
179
|
+
*/
|
|
180
|
+
function create(opts) {
|
|
181
|
+
validateOpts.requireObject(opts, "mail.server.jmap.create",
|
|
182
|
+
MailServerJmapError, "mail-server-jmap/bad-opts");
|
|
183
|
+
if (!opts.mailStore) {
|
|
184
|
+
throw new MailServerJmapError("mail-server-jmap/no-mail-store",
|
|
185
|
+
"mail.server.jmap.create: mailStore is required (compose b.mailStore.create({ backend: ... }))");
|
|
186
|
+
}
|
|
187
|
+
if (typeof opts.methods !== "object" || opts.methods === null || Array.isArray(opts.methods)) {
|
|
188
|
+
throw new MailServerJmapError("mail-server-jmap/no-methods",
|
|
189
|
+
"mail.server.jmap.create: opts.methods must be an object mapping method-name → async fn(actor, args, ctx)");
|
|
190
|
+
}
|
|
191
|
+
if (typeof opts.accountsFor !== "function") {
|
|
192
|
+
throw new MailServerJmapError("mail-server-jmap/no-accounts-for",
|
|
193
|
+
"mail.server.jmap.create: opts.accountsFor(actor) async function is required for the session resource");
|
|
194
|
+
}
|
|
195
|
+
var profile = opts.profile || DEFAULT_PROFILE;
|
|
196
|
+
var posture = opts.posture || null;
|
|
197
|
+
var serverCapabilities = opts.serverCapabilities || {};
|
|
198
|
+
var methods = opts.methods;
|
|
199
|
+
var sessionState = bCrypto.generateToken(16); // allow:raw-byte-literal — opaque session-state token length
|
|
200
|
+
|
|
201
|
+
function _emit(action, metadata, outcome) {
|
|
202
|
+
try {
|
|
203
|
+
audit().safeEmit({
|
|
204
|
+
action: action,
|
|
205
|
+
outcome: outcome || "success",
|
|
206
|
+
metadata: metadata || {},
|
|
207
|
+
});
|
|
208
|
+
} catch (_e) { /* drop-silent — audit best-effort */ }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---- Back-reference resolution (RFC 8620 §3.7) -------------------------
|
|
212
|
+
//
|
|
213
|
+
// Walks the args tree replacing `#<refClientId>/<jsonPointer>` shapes
|
|
214
|
+
// with the resolved value from the prior method-response. Returns the
|
|
215
|
+
// resolved args object on success or throws an
|
|
216
|
+
// `urn:ietf:params:jmap:error:invalidResultReference`.
|
|
217
|
+
function _resolveBackRefs(args, priorResponses) {
|
|
218
|
+
if (args === null || typeof args !== "object") return args;
|
|
219
|
+
if (Array.isArray(args)) {
|
|
220
|
+
var out = [];
|
|
221
|
+
for (var i = 0; i < args.length; i += 1) out.push(_resolveBackRefs(args[i], priorResponses));
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
var obj = {};
|
|
225
|
+
var keys = Object.keys(args);
|
|
226
|
+
for (var k = 0; k < keys.length; k += 1) {
|
|
227
|
+
var key = keys[k];
|
|
228
|
+
var val = args[key];
|
|
229
|
+
if (key.charCodeAt(0) === 0x23) { // allow:raw-byte-literal — `#` (0x23) is the JMAP back-ref-key prefix
|
|
230
|
+
// `#<srcClientId>` → key strips the `#`; value is { resultOf, name, path }
|
|
231
|
+
var targetKey = key.slice(1);
|
|
232
|
+
if (!val || typeof val !== "object" || Array.isArray(val) ||
|
|
233
|
+
typeof val.resultOf !== "string" || typeof val.name !== "string" ||
|
|
234
|
+
typeof val.path !== "string") {
|
|
235
|
+
throw new MailServerJmapError("urn:ietf:params:jmap:error:invalidResultReference",
|
|
236
|
+
"back-ref `#" + targetKey + "` malformed (expected { resultOf, name, path })");
|
|
237
|
+
}
|
|
238
|
+
var src = priorResponses[val.resultOf];
|
|
239
|
+
if (!src || src.name !== val.name) {
|
|
240
|
+
throw new MailServerJmapError("urn:ietf:params:jmap:error:invalidResultReference",
|
|
241
|
+
"back-ref `#" + targetKey + "` → no prior response with clientId='" + val.resultOf +
|
|
242
|
+
"' and name='" + val.name + "'");
|
|
243
|
+
}
|
|
244
|
+
var resolved = _pointerLookup(src.result, val.path);
|
|
245
|
+
if (resolved === undefined) {
|
|
246
|
+
throw new MailServerJmapError("urn:ietf:params:jmap:error:invalidResultReference",
|
|
247
|
+
"back-ref `#" + targetKey + "` → path '" + val.path + "' resolved to undefined");
|
|
248
|
+
}
|
|
249
|
+
obj[targetKey] = resolved;
|
|
250
|
+
} else {
|
|
251
|
+
obj[key] = _resolveBackRefs(val, priorResponses);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return obj;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Minimal JSON Pointer (RFC 6901 §3) — `/foo/bar/0` traversal. JMAP
|
|
258
|
+
// back-references constrain the shape (single object → single array
|
|
259
|
+
// → single field) so we don't need the full RFC 6901 escape grammar
|
|
260
|
+
// here. Bounded recursion via the back-ref-depth cap upstream.
|
|
261
|
+
function _pointerLookup(node, path) {
|
|
262
|
+
if (typeof path !== "string") return undefined;
|
|
263
|
+
if (path === "" || path === "/") return node;
|
|
264
|
+
var parts = path.split("/");
|
|
265
|
+
var cur = node;
|
|
266
|
+
for (var i = 0; i < parts.length; i += 1) {
|
|
267
|
+
var seg = parts[i];
|
|
268
|
+
if (seg === "" && i === 0) continue;
|
|
269
|
+
if (cur === null || typeof cur !== "object") return undefined;
|
|
270
|
+
// RFC 6901 ~1 / ~0 escapes — minimal grammar.
|
|
271
|
+
seg = seg.replace(/~1/g, "/").replace(/~0/g, "~"); // allow:regex-no-length-cap — seg length bounded by path which is bounded by maxLineBytes upstream
|
|
272
|
+
if (Array.isArray(cur)) {
|
|
273
|
+
if (seg === "*") return cur;
|
|
274
|
+
var idx = parseInt(seg, 10);
|
|
275
|
+
if (!isFinite(idx) || idx < 0 || idx >= cur.length) return undefined;
|
|
276
|
+
cur = cur[idx];
|
|
277
|
+
} else {
|
|
278
|
+
if (!Object.prototype.hasOwnProperty.call(cur, seg)) return undefined;
|
|
279
|
+
cur = cur[seg];
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return cur;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ---- Dispatch ------------------------------------------------------------
|
|
286
|
+
//
|
|
287
|
+
// `dispatch(actor, body)` is the operator-callable form — accepts a
|
|
288
|
+
// pre-parsed request body + an authenticated actor, returns a
|
|
289
|
+
// response object suitable for JSON-serialization to the client.
|
|
290
|
+
async function dispatch(actor, body) {
|
|
291
|
+
if (!actor) {
|
|
292
|
+
return _refusalResponse("urn:ietf:params:jmap:error:forbidden",
|
|
293
|
+
"actor is required (operator must wire b.middleware.bearerAuth before this handler)");
|
|
294
|
+
}
|
|
295
|
+
var parsed;
|
|
296
|
+
try {
|
|
297
|
+
parsed = guardJmap.validate(body, {
|
|
298
|
+
profile: profile,
|
|
299
|
+
posture: posture,
|
|
300
|
+
serverCapabilities: serverCapabilities,
|
|
301
|
+
});
|
|
302
|
+
} catch (e) {
|
|
303
|
+
var errType = (e && e.code) || "urn:ietf:params:jmap:error:invalidArguments";
|
|
304
|
+
_emit("mail.server.jmap.request_refused",
|
|
305
|
+
{ type: errType, reason: (e && e.message) || "" }, "denied");
|
|
306
|
+
return _refusalResponse(errType, (e && e.message) || "request refused");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
var methodResponses = [];
|
|
310
|
+
var byClientId = Object.create(null);
|
|
311
|
+
for (var i = 0; i < parsed.methodCalls.length; i += 1) {
|
|
312
|
+
var call = parsed.methodCalls[i];
|
|
313
|
+
var methodName = call[0];
|
|
314
|
+
var rawArgs = call[1];
|
|
315
|
+
var clientId = call[2];
|
|
316
|
+
var resolvedArgs;
|
|
317
|
+
try {
|
|
318
|
+
resolvedArgs = _resolveBackRefs(rawArgs, byClientId);
|
|
319
|
+
} catch (e) {
|
|
320
|
+
var refType = (e && e.code) || "urn:ietf:params:jmap:error:invalidResultReference";
|
|
321
|
+
methodResponses.push(["error", { type: refType, description: (e && e.message) || "" }, clientId]);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
var handler = methods[methodName];
|
|
325
|
+
if (typeof handler !== "function") {
|
|
326
|
+
methodResponses.push(["error",
|
|
327
|
+
{ type: "urn:ietf:params:jmap:error:unknownMethod",
|
|
328
|
+
description: "Method '" + methodName + "' not implemented on this server" }, clientId]);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
// JMAP methodCalls execute sequentially by spec (RFC 8620 §3.7 —
|
|
333
|
+
// back-references require strict ordering). The await-in-loop
|
|
334
|
+
// pattern is intentional here.
|
|
335
|
+
var result = await handler(actor, resolvedArgs, {
|
|
336
|
+
using: parsed.using,
|
|
337
|
+
createdIds: parsed.createdIds,
|
|
338
|
+
methodName: methodName,
|
|
339
|
+
clientId: clientId,
|
|
340
|
+
});
|
|
341
|
+
if (result && typeof result === "object" && result.type &&
|
|
342
|
+
typeof result.type === "string" && result.type.indexOf("urn:ietf:params:jmap:error:") === 0) {
|
|
343
|
+
// Operator-emitted error shape — preserve as-is.
|
|
344
|
+
methodResponses.push(["error", result, clientId]);
|
|
345
|
+
byClientId[clientId] = { name: "error", result: result };
|
|
346
|
+
} else {
|
|
347
|
+
methodResponses.push([methodName, result || {}, clientId]);
|
|
348
|
+
byClientId[clientId] = { name: methodName, result: result || {} };
|
|
349
|
+
}
|
|
350
|
+
} catch (e) {
|
|
351
|
+
_emit("mail.server.jmap.method_threw",
|
|
352
|
+
{ method: methodName, clientId: clientId,
|
|
353
|
+
error: (e && e.message) || String(e) }, "failure");
|
|
354
|
+
methodResponses.push(["error",
|
|
355
|
+
{ type: "urn:ietf:params:jmap:error:serverFail",
|
|
356
|
+
description: "Method threw" }, clientId]);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
_emit("mail.server.jmap.request",
|
|
361
|
+
{ methodCallCount: parsed.methodCalls.length, using: parsed.using });
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
methodResponses: methodResponses,
|
|
365
|
+
sessionState: sessionState,
|
|
366
|
+
createdIds: parsed.createdIds,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function _refusalResponse(type, description) {
|
|
371
|
+
return {
|
|
372
|
+
type: type,
|
|
373
|
+
description: description,
|
|
374
|
+
methodResponses: [],
|
|
375
|
+
sessionState: sessionState,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ---- HTTP handlers -----------------------------------------------------
|
|
380
|
+
|
|
381
|
+
function apiHandler(req, res) {
|
|
382
|
+
// Operator wires b.middleware.bearerAuth before this handler, so
|
|
383
|
+
// `req.user` is the authenticated actor. If req.user is missing,
|
|
384
|
+
// refuse with 401 + Problem Details body.
|
|
385
|
+
var actor = req.user || (req.actor || null);
|
|
386
|
+
var rawBody = req.body;
|
|
387
|
+
if (rawBody === undefined) {
|
|
388
|
+
// b.middleware.bodyParser may not have run. Refuse cleanly.
|
|
389
|
+
res.statusCode = 400;
|
|
390
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
391
|
+
res.end(JSON.stringify({
|
|
392
|
+
type: "urn:ietf:params:jmap:error:invalidArguments",
|
|
393
|
+
description: "request body missing (wire b.middleware.bodyParser before this handler)",
|
|
394
|
+
}));
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
dispatch(actor, rawBody).then(function (response) {
|
|
398
|
+
// Map typed JMAP refusals to the right HTTP status. `forbidden`
|
|
399
|
+
// means the dispatcher refused because actor was missing /
|
|
400
|
+
// wrong-tenant — clients + proxies need 401 to trigger their
|
|
401
|
+
// re-auth flow (a 400 looks like a malformed request, which it
|
|
402
|
+
// isn't). Everything else stays 400 per RFC 8620 §3.6.1.
|
|
403
|
+
if (response && response.type === "urn:ietf:params:jmap:error:forbidden") {
|
|
404
|
+
res.statusCode = 401; // allow:raw-byte-literal — HTTP status codes
|
|
405
|
+
} else if (response && response.type) {
|
|
406
|
+
res.statusCode = 400; // allow:raw-byte-literal — HTTP status codes
|
|
407
|
+
} else {
|
|
408
|
+
res.statusCode = 200; // allow:raw-byte-literal — HTTP status codes
|
|
409
|
+
}
|
|
410
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
411
|
+
res.end(JSON.stringify(response));
|
|
412
|
+
}, function (err) {
|
|
413
|
+
_emit("mail.server.jmap.handler_threw",
|
|
414
|
+
{ error: (err && err.message) || String(err) }, "failure");
|
|
415
|
+
res.statusCode = 500;
|
|
416
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
417
|
+
res.end(JSON.stringify({
|
|
418
|
+
type: "urn:ietf:params:jmap:error:serverFail",
|
|
419
|
+
description: "Server error",
|
|
420
|
+
}));
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function sessionHandler(req, res) {
|
|
425
|
+
var actor = req.user || (req.actor || null);
|
|
426
|
+
if (!actor) {
|
|
427
|
+
res.statusCode = 401;
|
|
428
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
429
|
+
res.end(JSON.stringify({
|
|
430
|
+
type: "urn:ietf:params:jmap:error:forbidden",
|
|
431
|
+
description: "Authentication required",
|
|
432
|
+
}));
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
Promise.resolve().then(function () { return opts.accountsFor(actor); })
|
|
436
|
+
.then(function (accountInfo) {
|
|
437
|
+
var info = accountInfo || { primaryAccounts: {}, accounts: {} };
|
|
438
|
+
var session = {
|
|
439
|
+
capabilities: Object.assign({}, { "urn:ietf:params:jmap:core": {} }, serverCapabilities),
|
|
440
|
+
accounts: info.accounts || {},
|
|
441
|
+
primaryAccounts: info.primaryAccounts || {},
|
|
442
|
+
username: actor.username || actor.id || "unknown",
|
|
443
|
+
apiUrl: opts.apiUrl || "/jmap/api",
|
|
444
|
+
downloadUrl: opts.downloadUrl || "/jmap/download/{accountId}/{blobId}/{name}?accept={type}",
|
|
445
|
+
uploadUrl: opts.uploadUrl || "/jmap/upload/{accountId}",
|
|
446
|
+
eventSourceUrl: opts.eventSourceUrl || "/jmap/eventsource?types={types}&closeafter={closeafter}&ping={ping}",
|
|
447
|
+
state: sessionState,
|
|
448
|
+
};
|
|
449
|
+
res.statusCode = 200;
|
|
450
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
451
|
+
res.end(safeJson.stringify ? safeJson.stringify(session) : JSON.stringify(session)); // allow:bare-canonicalize-walk — JSON response, not signed payload
|
|
452
|
+
})
|
|
453
|
+
.catch(function (err) {
|
|
454
|
+
_emit("mail.server.jmap.session_threw",
|
|
455
|
+
{ error: (err && err.message) || String(err) }, "failure");
|
|
456
|
+
res.statusCode = 500;
|
|
457
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
458
|
+
res.end(JSON.stringify({
|
|
459
|
+
type: "urn:ietf:params:jmap:error:serverFail",
|
|
460
|
+
description: "Session resource failed",
|
|
461
|
+
}));
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function discoveryHandler(req, res) {
|
|
466
|
+
// RFC 8620 §2.2 — well-known endpoint redirects (or directly returns)
|
|
467
|
+
// the session URL. We redirect to /jmap/session per the most common
|
|
468
|
+
// pattern; operators with a non-root mount path override via
|
|
469
|
+
// opts.sessionUrl.
|
|
470
|
+
res.statusCode = 302;
|
|
471
|
+
res.setHeader("Location", opts.sessionUrl || "/jmap/session");
|
|
472
|
+
res.end();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
create: create,
|
|
477
|
+
dispatch: dispatch,
|
|
478
|
+
apiHandler: apiHandler,
|
|
479
|
+
sessionHandler: sessionHandler,
|
|
480
|
+
discoveryHandler: discoveryHandler,
|
|
481
|
+
MailServerJmapError: MailServerJmapError,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
module.exports = {
|
|
486
|
+
create: create,
|
|
487
|
+
MailServerJmapError: MailServerJmapError,
|
|
488
|
+
};
|