@blamejs/core 0.9.46 → 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.
Files changed (78) hide show
  1. package/CHANGELOG.md +951 -893
  2. package/index.js +30 -0
  3. package/lib/_test/crypto-fixtures.js +67 -0
  4. package/lib/agent-event-bus.js +52 -6
  5. package/lib/agent-idempotency.js +169 -16
  6. package/lib/agent-orchestrator.js +263 -9
  7. package/lib/agent-posture-chain.js +163 -5
  8. package/lib/agent-saga.js +146 -16
  9. package/lib/agent-snapshot.js +349 -19
  10. package/lib/agent-stream.js +34 -2
  11. package/lib/agent-tenant.js +179 -23
  12. package/lib/agent-trace.js +84 -21
  13. package/lib/auth/aal.js +8 -1
  14. package/lib/auth/ciba.js +6 -1
  15. package/lib/auth/dpop.js +7 -2
  16. package/lib/auth/fal.js +17 -8
  17. package/lib/auth/jwt-external.js +128 -4
  18. package/lib/auth/oauth.js +232 -10
  19. package/lib/auth/oid4vci.js +67 -7
  20. package/lib/auth/openid-federation.js +71 -25
  21. package/lib/auth/passkey.js +140 -6
  22. package/lib/auth/sd-jwt-vc.js +67 -5
  23. package/lib/circuit-breaker.js +10 -2
  24. package/lib/compliance.js +176 -8
  25. package/lib/crypto-field.js +114 -14
  26. package/lib/crypto.js +216 -20
  27. package/lib/db.js +1 -0
  28. package/lib/guard-imap-command.js +335 -0
  29. package/lib/guard-jmap.js +321 -0
  30. package/lib/guard-managesieve-command.js +566 -0
  31. package/lib/guard-pop3-command.js +317 -0
  32. package/lib/guard-smtp-command.js +58 -3
  33. package/lib/mail-agent.js +20 -7
  34. package/lib/mail-arc-sign.js +12 -8
  35. package/lib/mail-auth.js +323 -34
  36. package/lib/mail-crypto-pgp.js +934 -0
  37. package/lib/mail-crypto-smime.js +340 -0
  38. package/lib/mail-crypto.js +108 -0
  39. package/lib/mail-dav.js +1224 -0
  40. package/lib/mail-deploy.js +492 -0
  41. package/lib/mail-dkim.js +431 -26
  42. package/lib/mail-journal.js +435 -0
  43. package/lib/mail-scan.js +502 -0
  44. package/lib/mail-server-imap.js +1102 -0
  45. package/lib/mail-server-jmap.js +488 -0
  46. package/lib/mail-server-managesieve.js +853 -0
  47. package/lib/mail-server-mx.js +164 -34
  48. package/lib/mail-server-pop3.js +836 -0
  49. package/lib/mail-server-rate-limit.js +269 -0
  50. package/lib/mail-server-submission.js +1032 -0
  51. package/lib/mail-server-tls.js +445 -0
  52. package/lib/mail-sieve.js +557 -0
  53. package/lib/mail-spam-score.js +284 -0
  54. package/lib/mail.js +99 -0
  55. package/lib/metrics.js +130 -10
  56. package/lib/middleware/dpop.js +58 -3
  57. package/lib/middleware/idempotency-key.js +255 -42
  58. package/lib/middleware/protected-resource-metadata.js +114 -2
  59. package/lib/network-dns-resolver.js +33 -0
  60. package/lib/network-tls.js +46 -0
  61. package/lib/outbox.js +62 -12
  62. package/lib/pqc-agent.js +13 -5
  63. package/lib/retry.js +23 -9
  64. package/lib/router.js +23 -1
  65. package/lib/safe-ical.js +634 -0
  66. package/lib/safe-icap.js +502 -0
  67. package/lib/safe-mime.js +15 -0
  68. package/lib/safe-sieve.js +684 -0
  69. package/lib/safe-smtp.js +57 -0
  70. package/lib/safe-url.js +37 -0
  71. package/lib/safe-vcard.js +473 -0
  72. package/lib/self-update-standalone-verifier.js +32 -3
  73. package/lib/self-update.js +168 -17
  74. package/lib/vendor/MANIFEST.json +161 -156
  75. package/lib/vendor-data.js +127 -9
  76. package/lib/vex.js +324 -59
  77. package/package.json +1 -1
  78. package/sbom.cdx.json +6 -6
@@ -0,0 +1,1224 @@
1
+ "use strict";
2
+ // codebase-patterns:allow-file raw-byte-literal — DAV is HTTP-shaped; every
3
+ // numeric in this file is an HTTP status code (200 / 201 / 207 / 400 / 401
4
+ // / 403 / 404 / 412 / 415 / 500). These are RFC 4791 / RFC 6352 / RFC 2616
5
+ // wire-protocol constants, not memory or byte-count caps.
6
+ /**
7
+ * @module b.mail.dav
8
+ * @nav Mail
9
+ * @title Mail CalDAV / CardDAV
10
+ * @order 560
11
+ *
12
+ * @intro
13
+ * CalDAV (RFC 4791) + CardDAV (RFC 6352) HTTP route handlers. Where
14
+ * the mail-server primitives mount as TCP listeners, the DAV stack
15
+ * rides the existing HTTP surface: operators mount the returned
16
+ * handlers under `b.router` / `b.createApp` and reuse their auth
17
+ * middleware, TLS termination, and rate-limit posture.
18
+ *
19
+ * The framework owns the wire protocol — method dispatch, XML body
20
+ * parsing (via `b.xmlC14n.parse`), per-tenant URL isolation,
21
+ * `If-Match` / `If-None-Match` ETag invariants, response-shape
22
+ * generation, and (critically) PUT-body validation through
23
+ * `b.safeIcal.parse` (CalDAV) and `b.safeVcard.parse` (CardDAV).
24
+ * Operators wire the storage backend — `listCalendars`,
25
+ * `getComponent`, `listComponents`, `putComponent`,
26
+ * `deleteComponent` for CalDAV; the equivalent contact-shaped set
27
+ * for CardDAV — so the framework never assumes a single DB shape.
28
+ *
29
+ * ## Public surface
30
+ *
31
+ * ```js
32
+ * var dav = b.mail.dav.create({
33
+ * storage: {
34
+ * calendar: {
35
+ * listCalendars: async function (principalId) { ... },
36
+ * getComponent: async function (principalId, calendarId, componentId) { ... },
37
+ * listComponents: async function (principalId, calendarId, filter) { ... },
38
+ * putComponent: async function (principalId, calendarId, componentId, icalBytes, ifMatch) { ... },
39
+ * deleteComponent: async function (principalId, calendarId, componentId, ifMatch) { ... },
40
+ * mkcalendar: async function (principalId, calendarId, props) { ... },
41
+ * },
42
+ * addressbook: {
43
+ * listAddressbooks: async function (principalId) { ... },
44
+ * getCard: async function (principalId, addressbookId, cardId) { ... },
45
+ * listCards: async function (principalId, addressbookId, filter) { ... },
46
+ * putCard: async function (principalId, addressbookId, cardId, vcardBytes, ifMatch) { ... },
47
+ * deleteCard: async function (principalId, addressbookId, cardId, ifMatch) { ... },
48
+ * mkcol: async function (principalId, addressbookId, props) { ... },
49
+ * },
50
+ * },
51
+ * profile: "strict", // safeIcal / safeVcard
52
+ * audit: b.audit,
53
+ * });
54
+ *
55
+ * app.use("/.well-known/caldav", dav.discoveryHandler);
56
+ * app.use("/.well-known/carddav", dav.discoveryHandler);
57
+ * app.use("/caldav", b.middleware.bearerAuth({...}), dav.caldavHandler);
58
+ * app.use("/carddav", b.middleware.bearerAuth({...}), dav.carddavHandler);
59
+ * ```
60
+ *
61
+ * ## URL shape
62
+ *
63
+ * - CalDAV: `/caldav/<principal>/<calendar>/<component>.ics`
64
+ * - CardDAV: `/carddav/<principal>/<addressbook>/<card>.vcf`
65
+ *
66
+ * Every URL carries the principal ID at the first path segment.
67
+ * Cross-principal access is refused at the handler boundary; the
68
+ * storage backend never sees a principal ID it did not authorize.
69
+ *
70
+ * ## Verbs (v1)
71
+ *
72
+ * Common: `OPTIONS`, `PROPFIND`, `REPORT`, `GET`, `PUT`, `DELETE`.
73
+ * CalDAV-specific: `MKCALENDAR` (RFC 4791 §5.2.1).
74
+ * CardDAV-specific: `MKCOL` (RFC 4918 §9.3).
75
+ *
76
+ * PROPFIND responds Multi-Status (207) for `Depth: 0` (resource
77
+ * props) / `Depth: 1` (collection contents). REPORT bodies
78
+ * supported: `calendar-query`, `calendar-multiget`,
79
+ * `addressbook-query`, `addressbook-multiget`. `sync-collection`
80
+ * (RFC 6578) ships when the storage backend declares its sync-token
81
+ * capability.
82
+ *
83
+ * ## Status codes
84
+ *
85
+ * - 200 — GET / OPTIONS success
86
+ * - 201 — PUT created / MKCALENDAR / MKCOL success
87
+ * - 204 — PUT / DELETE success (existing resource)
88
+ * - 207 — PROPFIND / REPORT Multi-Status (RFC 4918 §13)
89
+ * - 401 — auth required (operator middleware did not populate actor)
90
+ * - 403 — cross-principal access / forbidden by storage
91
+ * - 404 — resource not found
92
+ * - 412 — `If-Match` ETag mismatch on PUT / DELETE (RFC 4918 §10.4)
93
+ * - 415 — PUT body failed safeIcal / safeVcard validation
94
+ *
95
+ * ## Explicitly deferred (v1)
96
+ *
97
+ * - **WebDAV ACL (RFC 3744)** — operator wires authorization at
98
+ * their HTTP middleware (per-principal scoping is already
99
+ * enforced by the URL invariant; richer ACE / privilege grammar
100
+ * is opt-in).
101
+ * - **CalDAV scheduling (RFC 6638)** — the iTIP scheduling outbox /
102
+ * inbox routes call back into `b.mail.submission`; ships in a
103
+ * later slice so the cross-protocol contract is settled first.
104
+ * - **Free-busy reports (RFC 4791 §7.10)** — basic free-busy shape
105
+ * parses; the full availability merge across attendees defers to
106
+ * the scheduling slice.
107
+ * - **VTIMEZONE inline composition** — operators reference IANA
108
+ * timezone names; full VTIMEZONE generation lives in JSCalendar.
109
+ * - **iMIP (RFC 6047)** — iTIP-over-mail handler defers to the
110
+ * scheduling slice with its MX hook.
111
+ *
112
+ * ## CVE defense composition
113
+ *
114
+ * - `b.safeIcal` rejects RRULE COUNT > 10000 / BYxxx list > 24 →
115
+ * defends CVE-2024-39687 (ical4j RRULE recursion / Outlook
116
+ * calendar bomb) on the PUT path.
117
+ * - `b.xmlC14n.parse` rejects DOCTYPE / ENTITY in the
118
+ * PROPFIND / REPORT body → defends XXE / billion-laughs on the
119
+ * query path.
120
+ * - URL-encoded path traversal (`..`, `%2e%2e`, null bytes) is
121
+ * refused before the storage backend sees the IDs.
122
+ *
123
+ * @card
124
+ * CalDAV (RFC 4791) + CardDAV (RFC 6352) HTTP handlers — operators
125
+ * mount under their HTTP router. Composes b.safeIcal / b.safeVcard
126
+ * for PUT-body validation, b.xmlC14n.parse for PROPFIND / REPORT
127
+ * bodies. Per-principal URL isolation; operator-supplied storage
128
+ * backend. Defends CVE-2024-39687 at the PUT boundary.
129
+ */
130
+
131
+ var lazyRequire = require("./lazy-require");
132
+ var C = require("./constants");
133
+ var safeIcal = require("./safe-ical");
134
+ var safeVcard = require("./safe-vcard");
135
+ var safeBuffer = require("./safe-buffer");
136
+ var validateOpts = require("./validate-opts");
137
+ var xmlC14n = require("./xml-c14n");
138
+ var { defineClass } = require("./framework-error");
139
+
140
+ var audit = lazyRequire(function () { return require("./audit"); });
141
+
142
+ var MailDavError = defineClass("MailDavError", { alwaysPermanent: true });
143
+
144
+ // HTTP method method-set per RFC 4791 §5.3 (CalDAV) + RFC 6352 §6
145
+ // (CardDAV) + RFC 4918 §9 (base WebDAV).
146
+ var CALDAV_METHODS = ["OPTIONS", "PROPFIND", "REPORT", "GET", "PUT", "DELETE", "MKCALENDAR"];
147
+ var CARDDAV_METHODS = ["OPTIONS", "PROPFIND", "REPORT", "GET", "PUT", "DELETE", "MKCOL"];
148
+
149
+ // DAV-class header values per RFC 4791 §5.1 + RFC 6352 §6.1.
150
+ var CALDAV_DAV_HEADER = "1, 2, 3, calendar-access";
151
+ var CARDDAV_DAV_HEADER = "1, 2, 3, addressbook";
152
+
153
+ // Per-request body cap — applies to PROPFIND / REPORT bodies AND to
154
+ // PUT bodies before they are forwarded to safeIcal / safeVcard. The
155
+ // downstream parsers re-cap per profile; this is the outer envelope.
156
+ var MAX_REQUEST_BODY_BYTES = C.BYTES.mib(8);
157
+
158
+ // Per-request actor scope — every operator middleware populates
159
+ // `req.user.principalId` (or `req.actor.principalId`); the handler
160
+ // refuses on miss.
161
+ function _actorPrincipalId(req) {
162
+ var actor = req.user || req.actor || null;
163
+ if (!actor) return null;
164
+ if (typeof actor.principalId === "string") return actor.principalId;
165
+ if (typeof actor.id === "string") return actor.id;
166
+ if (typeof actor.username === "string") return actor.username;
167
+ return null;
168
+ }
169
+
170
+ /**
171
+ * @primitive b.mail.dav.create
172
+ * @signature b.mail.dav.create(opts)
173
+ * @since 0.9.81
174
+ * @status stable
175
+ * @related b.safeIcal.parse, b.safeVcard.parse, b.xmlC14n.parse
176
+ *
177
+ * Build a CalDAV + CardDAV route-handler bundle. Returns a handle
178
+ * exposing `caldavHandler` / `carddavHandler` / `discoveryHandler`
179
+ * (Express-style `(req, res, next)` functions) plus `dispatchCaldav` /
180
+ * `dispatchCarddav` for operators on a non-Express transport.
181
+ *
182
+ * @opts
183
+ * storage: { calendar, addressbook }, // operator-supplied
184
+ * profile: "strict" | "balanced" | "permissive", // default strict
185
+ * compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2", // optional
186
+ * maxRequestBodyBytes: number, // default 8 MiB
187
+ * audit: b.audit, // optional
188
+ *
189
+ * @example
190
+ * var dav = b.mail.dav.create({
191
+ * storage: {
192
+ * calendar: { listCalendars, getComponent, listComponents,
193
+ * putComponent, deleteComponent, mkcalendar },
194
+ * addressbook: { listAddressbooks, getCard, listCards,
195
+ * putCard, deleteCard, mkcol },
196
+ * },
197
+ * profile: "strict",
198
+ * });
199
+ *
200
+ * app.use("/.well-known/caldav", dav.discoveryHandler);
201
+ * app.use("/.well-known/carddav", dav.discoveryHandler);
202
+ * app.use("/caldav", bearerAuth, dav.caldavHandler);
203
+ * app.use("/carddav", bearerAuth, dav.carddavHandler);
204
+ */
205
+ function create(opts) {
206
+ validateOpts.requireObject(opts, "mail.dav.create", MailDavError, "mail-dav/bad-opts");
207
+ if (!opts.storage || typeof opts.storage !== "object") {
208
+ throw new MailDavError("mail-dav/no-storage",
209
+ "mail.dav.create: opts.storage is required " +
210
+ "({ calendar: { listCalendars, ... }, addressbook: { listAddressbooks, ... } })");
211
+ }
212
+ var profile = opts.profile || "strict";
213
+ var compliancePosture = opts.compliancePosture || null;
214
+ var maxBody = (typeof opts.maxRequestBodyBytes === "number" &&
215
+ isFinite(opts.maxRequestBodyBytes) &&
216
+ opts.maxRequestBodyBytes > 0)
217
+ ? opts.maxRequestBodyBytes
218
+ : MAX_REQUEST_BODY_BYTES;
219
+
220
+ var calStorage = opts.storage.calendar || null;
221
+ var cardStorage = opts.storage.addressbook || null;
222
+
223
+ function _emit(action, metadata, outcome) {
224
+ try {
225
+ audit().safeEmit({
226
+ action: action,
227
+ outcome: outcome || "success",
228
+ metadata: metadata || {},
229
+ });
230
+ } catch (_e) { /* drop-silent — audit best-effort */ }
231
+ }
232
+
233
+ // ---- URL parsing (per-tenant principal isolation) ----------------------
234
+ //
235
+ // CalDAV URLs: /caldav/<principal>/<calendar>/<component>
236
+ // CardDAV URLs: /carddav/<principal>/<addressbook>/<card>
237
+ //
238
+ // The mount prefix (`/caldav` / `/carddav`) is stripped by the
239
+ // operator's router before the handler runs. `req.url` therefore
240
+ // starts with `/<principal>/...`.
241
+ function _parsePath(reqUrl) {
242
+ // Strip query string.
243
+ var qIdx = reqUrl.indexOf("?");
244
+ var path = qIdx >= 0 ? reqUrl.slice(0, qIdx) : reqUrl;
245
+ // Refuse path traversal / null bytes before decoding.
246
+ if (path.indexOf("\0") >= 0 || // allow:raw-byte-literal — NUL byte refusal
247
+ /(?:^|\/)\.\.(?:\/|$)/.test(path) ||
248
+ /%2e%2e/i.test(path) ||
249
+ /%00/i.test(path)) {
250
+ return { principalId: null, parts: [], rejected: "traversal" };
251
+ }
252
+ var segs = path.split("/").filter(function (s) { return s.length > 0; });
253
+ var decoded = [];
254
+ for (var i = 0; i < segs.length; i++) {
255
+ try { decoded.push(decodeURIComponent(segs[i])); }
256
+ catch (_e) { return { principalId: null, parts: [], rejected: "malformed-uri" }; }
257
+ }
258
+ return {
259
+ principalId: decoded[0] || null,
260
+ parts: decoded.slice(1),
261
+ rejected: null,
262
+ };
263
+ }
264
+
265
+ function _refuseStatus(res, code, message) {
266
+ res.statusCode = code;
267
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
268
+ res.end(message || "");
269
+ }
270
+
271
+ function _readBodyBytes(req) {
272
+ return new Promise(function (resolve, reject) {
273
+ if (req.body !== undefined && req.body !== null) {
274
+ // Body parser already ran.
275
+ if (Buffer.isBuffer(req.body)) {
276
+ if (req.body.length > maxBody) {
277
+ reject(new MailDavError("mail-dav/oversize-body",
278
+ "request body exceeds " + maxBody + " bytes"));
279
+ return;
280
+ }
281
+ resolve(req.body);
282
+ return;
283
+ }
284
+ if (typeof req.body === "string") {
285
+ var buf = Buffer.from(req.body, "utf8");
286
+ if (buf.length > maxBody) {
287
+ reject(new MailDavError("mail-dav/oversize-body",
288
+ "request body exceeds " + maxBody + " bytes"));
289
+ return;
290
+ }
291
+ resolve(buf);
292
+ return;
293
+ }
294
+ // Object — body parser already JSON-parsed; treat as a
295
+ // signal that the operator misconfigured a body parser
296
+ // upstream. Re-serialize as JSON bytes (DAV bodies are XML,
297
+ // so this will fail downstream — fine).
298
+ resolve(Buffer.from(JSON.stringify(req.body), "utf8"));
299
+ return;
300
+ }
301
+ // safeBuffer.boundedChunkCollector enforces maxBytes inside
302
+ // push(); any chunk that would overflow throws + we reject the
303
+ // promise. The body is bounded BEFORE the cap is reached so the
304
+ // single allocation in result() is bounded by maxBytes.
305
+ var collector = safeBuffer.boundedChunkCollector({
306
+ maxBytes: maxBody,
307
+ errorClass: MailDavError,
308
+ sizeCode: "mail-dav/oversize-body",
309
+ sizeMessage: "request body exceeds " + maxBody + " bytes",
310
+ });
311
+ req.on("data", function (chunk) {
312
+ try { collector.push(chunk); }
313
+ catch (e) {
314
+ req.destroy();
315
+ reject(e);
316
+ }
317
+ });
318
+ req.on("end", function () {
319
+ try { resolve(collector.result()); }
320
+ catch (e) { reject(e); }
321
+ });
322
+ req.on("error", function (e) { reject(e); });
323
+ });
324
+ }
325
+
326
+ // ---- Response builders ------------------------------------------------
327
+
328
+ function _multiStatus(responses) {
329
+ // RFC 4918 §14.16 — Multi-Status response body.
330
+ var lines = ["<?xml version=\"1.0\" encoding=\"utf-8\"?>"];
331
+ lines.push("<D:multistatus xmlns:D=\"DAV:\" xmlns:C=\"urn:ietf:params:xml:ns:caldav\" xmlns:A=\"urn:ietf:params:xml:ns:carddav\">");
332
+ for (var i = 0; i < responses.length; i++) {
333
+ var r = responses[i];
334
+ lines.push("<D:response>");
335
+ lines.push("<D:href>" + _xmlEscape(r.href) + "</D:href>");
336
+ if (r.status) {
337
+ lines.push("<D:status>HTTP/1.1 " + r.status + " " + _statusText(r.status) + "</D:status>");
338
+ }
339
+ if (r.propstat) {
340
+ for (var j = 0; j < r.propstat.length; j++) {
341
+ var ps = r.propstat[j];
342
+ lines.push("<D:propstat>");
343
+ lines.push("<D:prop>" + (ps.propXml || "") + "</D:prop>");
344
+ lines.push("<D:status>HTTP/1.1 " + ps.status + " " + _statusText(ps.status) + "</D:status>");
345
+ lines.push("</D:propstat>");
346
+ }
347
+ }
348
+ lines.push("</D:response>");
349
+ }
350
+ lines.push("</D:multistatus>");
351
+ return lines.join("\n");
352
+ }
353
+
354
+ function _statusText(code) {
355
+ switch (code) {
356
+ case 200: return "OK";
357
+ case 201: return "Created";
358
+ case 204: return "No Content";
359
+ case 207: return "Multi-Status";
360
+ case 401: return "Unauthorized";
361
+ case 403: return "Forbidden";
362
+ case 404: return "Not Found";
363
+ case 412: return "Precondition Failed";
364
+ case 415: return "Unsupported Media Type";
365
+ case 500: return "Internal Server Error";
366
+ default: return "Status";
367
+ }
368
+ }
369
+
370
+ function _xmlEscape(s) {
371
+ if (s === null || s === undefined) return "";
372
+ return String(s)
373
+ .replace(/&/g, "&amp;")
374
+ .replace(/</g, "&lt;")
375
+ .replace(/>/g, "&gt;")
376
+ .replace(/"/g, "&quot;")
377
+ .replace(/'/g, "&apos;");
378
+ }
379
+
380
+ // Render a small subset of well-known DAV / CalDAV / CardDAV
381
+ // properties for PROPFIND responses. Operator-supplied storage
382
+ // resources can carry an `etag` / `ctag` / `displayName` /
383
+ // `resourcetype` field which this picker turns into XML.
384
+ function _renderProps(resource, requestedProps) {
385
+ if (!resource) return "";
386
+ var out = [];
387
+ function maybe(propName, value) {
388
+ if (value === null || value === undefined) return;
389
+ if (requestedProps && requestedProps.length > 0 &&
390
+ requestedProps.indexOf(propName) < 0) return;
391
+ out.push(value);
392
+ }
393
+ maybe("displayname",
394
+ "<D:displayname>" + _xmlEscape(resource.displayName || resource.id || "") + "</D:displayname>");
395
+ maybe("resourcetype",
396
+ "<D:resourcetype>" + (resource.resourcetype || "") + "</D:resourcetype>");
397
+ maybe("getetag",
398
+ resource.etag ? "<D:getetag>" + _xmlEscape(resource.etag) + "</D:getetag>" : null);
399
+ maybe("getcontenttype",
400
+ resource.contentType ? "<D:getcontenttype>" + _xmlEscape(resource.contentType) + "</D:getcontenttype>" : null);
401
+ maybe("getcontentlength",
402
+ typeof resource.size === "number" ?
403
+ "<D:getcontentlength>" + resource.size + "</D:getcontentlength>" : null);
404
+ maybe("getlastmodified",
405
+ resource.lastModified ? "<D:getlastmodified>" + _xmlEscape(resource.lastModified) + "</D:getlastmodified>" : null);
406
+ maybe("calendar-data",
407
+ resource.icalBytes ? "<C:calendar-data>" + _xmlEscape(_bufToText(resource.icalBytes)) + "</C:calendar-data>" : null);
408
+ maybe("address-data",
409
+ resource.vcardBytes ? "<A:address-data>" + _xmlEscape(_bufToText(resource.vcardBytes)) + "</A:address-data>" : null);
410
+ return out.join("");
411
+ }
412
+
413
+ function _bufToText(b) {
414
+ if (typeof b === "string") return b;
415
+ if (Buffer.isBuffer(b)) return b.toString("utf8");
416
+ return String(b);
417
+ }
418
+
419
+ // ---- PROPFIND body parsing -------------------------------------------
420
+ //
421
+ // Returns `{ allprop: boolean, propname: boolean, props: string[] }`.
422
+ // Empty body == allprop per RFC 4918 §9.1.
423
+
424
+ function _parsePropfindBody(bodyBuf) {
425
+ var s = bodyBuf.toString("utf8").trim();
426
+ if (s.length === 0) return { allprop: true, propname: false, props: [] };
427
+ var tree;
428
+ try { tree = xmlC14n.parse(s); }
429
+ catch (e) {
430
+ throw new MailDavError("mail-dav/bad-propfind-body",
431
+ "PROPFIND body XML refused: " + (e && e.message));
432
+ }
433
+ var props = [];
434
+ var allprop = false;
435
+ var propname = false;
436
+ _walkXml(tree, function (node) {
437
+ var local = _localName(node.name);
438
+ if (local === "allprop") allprop = true;
439
+ else if (local === "propname") propname = true;
440
+ else if (local === "prop" && node.children) {
441
+ for (var i = 0; i < node.children.length; i++) {
442
+ var c = node.children[i];
443
+ if (c.type === "element") props.push(_localName(c.name));
444
+ }
445
+ }
446
+ });
447
+ return { allprop: allprop, propname: propname, props: props };
448
+ }
449
+
450
+ function _localName(n) {
451
+ if (typeof n !== "string") return "";
452
+ var colonIdx = n.indexOf(":");
453
+ return (colonIdx >= 0 ? n.slice(colonIdx + 1) : n).toLowerCase();
454
+ }
455
+
456
+ function _walkXml(node, visitor) {
457
+ if (!node) return;
458
+ if (node.type === "element") visitor(node);
459
+ if (node.children) {
460
+ for (var i = 0; i < node.children.length; i++) {
461
+ _walkXml(node.children[i], visitor);
462
+ }
463
+ }
464
+ }
465
+
466
+ // ---- REPORT body parsing ---------------------------------------------
467
+ //
468
+ // Returns `{ kind, props, hrefs, filter }`. Recognized kinds:
469
+ // calendar-query / calendar-multiget / addressbook-query /
470
+ // addressbook-multiget.
471
+
472
+ function _parseReportBody(bodyBuf) {
473
+ var s = bodyBuf.toString("utf8").trim();
474
+ if (s.length === 0) {
475
+ throw new MailDavError("mail-dav/bad-report-body",
476
+ "REPORT body is empty");
477
+ }
478
+ var tree;
479
+ try { tree = xmlC14n.parse(s); }
480
+ catch (e) {
481
+ throw new MailDavError("mail-dav/bad-report-body",
482
+ "REPORT body XML refused: " + (e && e.message));
483
+ }
484
+ var kind = _localName(tree.name);
485
+ var props = [];
486
+ var hrefs = [];
487
+ var filter = null;
488
+ _walkXml(tree, function (node) {
489
+ var local = _localName(node.name);
490
+ if (local === "prop" && node.children) {
491
+ for (var i = 0; i < node.children.length; i++) {
492
+ var c = node.children[i];
493
+ if (c.type === "element") props.push(_localName(c.name));
494
+ }
495
+ } else if (local === "href" && node.children) {
496
+ for (var j = 0; j < node.children.length; j++) {
497
+ if (node.children[j].type === "text") {
498
+ hrefs.push(node.children[j].value || node.children[j].text || "");
499
+ }
500
+ }
501
+ } else if (local === "filter") {
502
+ filter = node;
503
+ }
504
+ });
505
+ return { kind: kind, props: props, hrefs: hrefs, filter: filter };
506
+ }
507
+
508
+ // ---- CalDAV dispatch -------------------------------------------------
509
+
510
+ async function dispatchCaldav(actorPrincipalId, req, res) {
511
+ var method = (req.method || "GET").toUpperCase();
512
+ if (CALDAV_METHODS.indexOf(method) < 0) {
513
+ _emit("mail.dav.refused",
514
+ { method: method, kind: "caldav" }, "denied");
515
+ return _refuseStatus(res, 405, "Method not allowed: " + method);
516
+ }
517
+ if (!actorPrincipalId) {
518
+ _emit("mail.dav.refused",
519
+ { method: method, kind: "caldav", reason: "no-actor" }, "denied");
520
+ return _refuseStatus(res, 401, "Authentication required");
521
+ }
522
+ if (!calStorage) {
523
+ return _refuseStatus(res, 501,
524
+ "CalDAV not configured (opts.storage.calendar required)");
525
+ }
526
+
527
+ if (method === "OPTIONS") {
528
+ res.statusCode = 200;
529
+ res.setHeader("DAV", CALDAV_DAV_HEADER);
530
+ res.setHeader("Allow", CALDAV_METHODS.join(", "));
531
+ res.setHeader("MS-Author-Via", "DAV");
532
+ res.end();
533
+ _emit("mail.dav.options", { kind: "caldav" });
534
+ return;
535
+ }
536
+
537
+ var parsed = _parsePath(req.url || "");
538
+ if (parsed.rejected) {
539
+ _emit("mail.dav.refused",
540
+ { method: method, reason: parsed.rejected }, "denied");
541
+ return _refuseStatus(res, 400, "Bad URL: " + parsed.rejected);
542
+ }
543
+ if (!parsed.principalId) {
544
+ // Allow PROPFIND at the root for principal-discovery patterns.
545
+ if (method !== "PROPFIND") {
546
+ return _refuseStatus(res, 400, "Principal segment required");
547
+ }
548
+ } else if (parsed.principalId !== actorPrincipalId) {
549
+ _emit("mail.dav.refused",
550
+ { method: method, kind: "caldav",
551
+ urlPrincipal: parsed.principalId,
552
+ actorPrincipal: actorPrincipalId,
553
+ reason: "cross-principal" }, "denied");
554
+ return _refuseStatus(res, 403,
555
+ "Cross-principal access refused");
556
+ }
557
+
558
+ var calendarId = parsed.parts[0] || null;
559
+ var componentId = parsed.parts[1] || null;
560
+
561
+ try {
562
+ switch (method) {
563
+ case "PROPFIND": return await _handleCaldavPropfind(req, res, actorPrincipalId, calendarId, componentId);
564
+ case "REPORT": return await _handleCaldavReport(req, res, actorPrincipalId, calendarId);
565
+ case "GET": return await _handleCaldavGet(req, res, actorPrincipalId, calendarId, componentId);
566
+ case "PUT": return await _handleCaldavPut(req, res, actorPrincipalId, calendarId, componentId);
567
+ case "DELETE": return await _handleCaldavDelete(req, res, actorPrincipalId, calendarId, componentId);
568
+ case "MKCALENDAR": return await _handleMkcalendar(req, res, actorPrincipalId, calendarId);
569
+ default:
570
+ // CALDAV_METHODS allowlist gate is enforced above; this
571
+ // branch is unreachable but eslint requires it.
572
+ return _refuseStatus(res, 405, "Method not allowed: " + method);
573
+ }
574
+ } catch (e) {
575
+ _emit("mail.dav.handler_threw",
576
+ { method: method, kind: "caldav",
577
+ error: (e && e.message) || String(e) }, "failure");
578
+ return _refuseStatus(res, 500, "Server error");
579
+ }
580
+ }
581
+
582
+ async function _handleCaldavPropfind(req, res, principalId, calendarId, componentId) {
583
+ var depth = (req.headers && (req.headers.depth || req.headers.Depth)) || "0";
584
+ var bodyBuf = await _readBodyBytes(req);
585
+ var body = _parsePropfindBody(bodyBuf);
586
+ var responses = [];
587
+
588
+ if (!calendarId) {
589
+ // Principal-level: list calendars (Depth: 1) or report principal (Depth: 0).
590
+ if (depth === "0") {
591
+ responses.push({
592
+ href: "/caldav/" + principalId + "/",
593
+ propstat: [{
594
+ status: 200,
595
+ propXml: _renderProps({
596
+ displayName: principalId,
597
+ resourcetype: "<D:collection/>",
598
+ }, body.props),
599
+ }],
600
+ });
601
+ } else {
602
+ var cals = await calStorage.listCalendars(principalId);
603
+ responses.push({
604
+ href: "/caldav/" + principalId + "/",
605
+ propstat: [{ status: 200,
606
+ propXml: _renderProps({ displayName: principalId,
607
+ resourcetype: "<D:collection/>" }, body.props) }],
608
+ });
609
+ for (var i = 0; i < (cals || []).length; i++) {
610
+ var cal = cals[i];
611
+ responses.push({
612
+ href: "/caldav/" + principalId + "/" + cal.id + "/",
613
+ propstat: [{ status: 200,
614
+ propXml: _renderProps({
615
+ displayName: cal.displayName || cal.id,
616
+ resourcetype: "<D:collection/><C:calendar/>",
617
+ etag: cal.etag,
618
+ }, body.props) }],
619
+ });
620
+ }
621
+ }
622
+ } else if (!componentId) {
623
+ // Calendar collection: list components.
624
+ var components = await calStorage.listComponents(principalId, calendarId, null);
625
+ responses.push({
626
+ href: "/caldav/" + principalId + "/" + calendarId + "/",
627
+ propstat: [{ status: 200,
628
+ propXml: _renderProps({ displayName: calendarId,
629
+ resourcetype: "<D:collection/><C:calendar/>" }, body.props) }],
630
+ });
631
+ if (depth !== "0") {
632
+ for (var j = 0; j < (components || []).length; j++) {
633
+ var c = components[j];
634
+ responses.push({
635
+ href: "/caldav/" + principalId + "/" + calendarId + "/" + c.id,
636
+ propstat: [{ status: 200,
637
+ propXml: _renderProps({
638
+ displayName: c.id,
639
+ resourcetype: "",
640
+ etag: c.etag,
641
+ contentType: "text/calendar; charset=utf-8",
642
+ size: c.size,
643
+ icalBytes: body.props.indexOf("calendar-data") >= 0 ? c.icalBytes : null,
644
+ }, body.props) }],
645
+ });
646
+ }
647
+ }
648
+ } else {
649
+ // Single component.
650
+ var comp = await calStorage.getComponent(principalId, calendarId, componentId);
651
+ if (!comp) {
652
+ responses.push({ href: req.url, status: 404 });
653
+ } else {
654
+ responses.push({
655
+ href: req.url,
656
+ propstat: [{ status: 200,
657
+ propXml: _renderProps({
658
+ displayName: comp.id,
659
+ etag: comp.etag,
660
+ contentType: "text/calendar; charset=utf-8",
661
+ size: comp.size,
662
+ icalBytes: body.props.indexOf("calendar-data") >= 0 ? comp.icalBytes : null,
663
+ }, body.props) }],
664
+ });
665
+ }
666
+ }
667
+ res.statusCode = 207;
668
+ res.setHeader("Content-Type", "application/xml; charset=utf-8");
669
+ res.setHeader("DAV", CALDAV_DAV_HEADER);
670
+ res.end(_multiStatus(responses));
671
+ _emit("mail.dav.propfind",
672
+ { kind: "caldav", principalId: principalId, calendarId: calendarId,
673
+ depth: depth, responseCount: responses.length });
674
+ }
675
+
676
+ async function _handleCaldavReport(req, res, principalId, calendarId) {
677
+ var bodyBuf = await _readBodyBytes(req);
678
+ var report = _parseReportBody(bodyBuf);
679
+ var responses = [];
680
+ if (report.kind === "calendar-multiget") {
681
+ for (var i = 0; i < report.hrefs.length; i++) {
682
+ var href = report.hrefs[i];
683
+ var hrefParsed = _parsePath(href);
684
+ if (hrefParsed.rejected || hrefParsed.principalId !== principalId) {
685
+ responses.push({ href: href, status: 403 });
686
+ continue;
687
+ }
688
+ var hrefCalId = hrefParsed.parts[0];
689
+ var hrefCompId = hrefParsed.parts[1];
690
+ var comp = await calStorage.getComponent(principalId, hrefCalId, hrefCompId);
691
+ if (!comp) {
692
+ responses.push({ href: href, status: 404 });
693
+ } else {
694
+ responses.push({
695
+ href: href,
696
+ propstat: [{ status: 200,
697
+ propXml: _renderProps({
698
+ etag: comp.etag,
699
+ contentType: "text/calendar; charset=utf-8",
700
+ size: comp.size,
701
+ icalBytes: comp.icalBytes,
702
+ }, report.props.length > 0 ? report.props : ["getetag", "calendar-data"]) }],
703
+ });
704
+ }
705
+ }
706
+ } else if (report.kind === "calendar-query") {
707
+ var rows = await calStorage.listComponents(principalId, calendarId, report.filter);
708
+ for (var j = 0; j < (rows || []).length; j++) {
709
+ var r = rows[j];
710
+ responses.push({
711
+ href: "/caldav/" + principalId + "/" + calendarId + "/" + r.id,
712
+ propstat: [{ status: 200,
713
+ propXml: _renderProps({
714
+ etag: r.etag,
715
+ contentType: "text/calendar; charset=utf-8",
716
+ size: r.size,
717
+ icalBytes: r.icalBytes,
718
+ }, report.props.length > 0 ? report.props : ["getetag", "calendar-data"]) }],
719
+ });
720
+ }
721
+ } else {
722
+ return _refuseStatus(res, 422, "Unsupported REPORT kind: " + report.kind);
723
+ }
724
+ res.statusCode = 207;
725
+ res.setHeader("Content-Type", "application/xml; charset=utf-8");
726
+ res.setHeader("DAV", CALDAV_DAV_HEADER);
727
+ res.end(_multiStatus(responses));
728
+ _emit("mail.dav.report",
729
+ { kind: "caldav", reportKind: report.kind, responseCount: responses.length });
730
+ }
731
+
732
+ async function _handleCaldavGet(req, res, principalId, calendarId, componentId) {
733
+ if (!componentId) {
734
+ return _refuseStatus(res, 404, "GET requires a component path");
735
+ }
736
+ var comp = await calStorage.getComponent(principalId, calendarId, componentId);
737
+ if (!comp) return _refuseStatus(res, 404, "Component not found");
738
+ res.statusCode = 200;
739
+ res.setHeader("Content-Type", "text/calendar; charset=utf-8");
740
+ if (comp.etag) res.setHeader("ETag", comp.etag);
741
+ res.end(comp.icalBytes);
742
+ _emit("mail.dav.get",
743
+ { kind: "caldav", principalId: principalId, calendarId: calendarId,
744
+ componentId: componentId });
745
+ }
746
+
747
+ async function _handleCaldavPut(req, res, principalId, calendarId, componentId) {
748
+ if (!componentId) {
749
+ return _refuseStatus(res, 400, "PUT requires a component path");
750
+ }
751
+ var bodyBuf = await _readBodyBytes(req);
752
+ // Validate iCal body via safeIcal — defends CVE-2024-39687 at the
753
+ // ingest boundary.
754
+ try {
755
+ safeIcal.parse(bodyBuf, {
756
+ profile: profile,
757
+ compliancePosture: compliancePosture,
758
+ });
759
+ } catch (e) {
760
+ _emit("mail.dav.refused",
761
+ { kind: "caldav", method: "PUT", reason: "ical-refused",
762
+ error: (e && e.code) || (e && e.message) }, "denied");
763
+ return _refuseStatus(res, 415,
764
+ "iCalendar body refused: " + ((e && e.message) || String(e)));
765
+ }
766
+ var ifMatch = (req.headers && (req.headers["if-match"] || req.headers["If-Match"])) || null;
767
+ var ifNoneMatch = (req.headers && (req.headers["if-none-match"] || req.headers["If-None-Match"])) || null;
768
+ var result;
769
+ try {
770
+ result = await calStorage.putComponent(principalId, calendarId, componentId,
771
+ bodyBuf, { ifMatch: ifMatch, ifNoneMatch: ifNoneMatch });
772
+ } catch (e) {
773
+ if (e && (e.code === "etag-mismatch" || e.statusCode === 412)) {
774
+ return _refuseStatus(res, 412, "ETag precondition failed");
775
+ }
776
+ throw e;
777
+ }
778
+ res.statusCode = result && result.created ? 201 : 204;
779
+ if (result && result.etag) res.setHeader("ETag", result.etag);
780
+ res.end();
781
+ _emit("mail.dav.put",
782
+ { kind: "caldav", principalId: principalId, calendarId: calendarId,
783
+ componentId: componentId, created: !!(result && result.created) });
784
+ }
785
+
786
+ async function _handleCaldavDelete(req, res, principalId, calendarId, componentId) {
787
+ if (!componentId) {
788
+ return _refuseStatus(res, 400, "DELETE requires a component path");
789
+ }
790
+ var ifMatch = (req.headers && (req.headers["if-match"] || req.headers["If-Match"])) || null;
791
+ try {
792
+ var r = await calStorage.deleteComponent(principalId, calendarId, componentId,
793
+ { ifMatch: ifMatch });
794
+ if (!r || r.notFound) return _refuseStatus(res, 404, "Component not found");
795
+ } catch (e) {
796
+ if (e && (e.code === "etag-mismatch" || e.statusCode === 412)) {
797
+ return _refuseStatus(res, 412, "ETag precondition failed");
798
+ }
799
+ throw e;
800
+ }
801
+ res.statusCode = 204;
802
+ res.end();
803
+ _emit("mail.dav.delete",
804
+ { kind: "caldav", principalId: principalId, calendarId: calendarId,
805
+ componentId: componentId });
806
+ }
807
+
808
+ async function _handleMkcalendar(req, res, principalId, calendarId) {
809
+ if (!calendarId) {
810
+ return _refuseStatus(res, 400, "MKCALENDAR requires a calendar path");
811
+ }
812
+ if (typeof calStorage.mkcalendar !== "function") {
813
+ return _refuseStatus(res, 501,
814
+ "MKCALENDAR not supported (storage.calendar.mkcalendar undefined)");
815
+ }
816
+ var bodyBuf = await _readBodyBytes(req);
817
+ var props = Object.create(null);
818
+ if (bodyBuf.length > 0) {
819
+ try {
820
+ var tree = xmlC14n.parse(bodyBuf.toString("utf8"));
821
+ _walkXml(tree, function (node) {
822
+ if (_localName(node.name) === "prop" && node.children) {
823
+ for (var i = 0; i < node.children.length; i++) {
824
+ var c = node.children[i];
825
+ if (c.type === "element" && c.children) {
826
+ var text = "";
827
+ for (var j = 0; j < c.children.length; j++) {
828
+ if (c.children[j].type === "text") {
829
+ text += c.children[j].value || c.children[j].text || "";
830
+ }
831
+ }
832
+ props[_localName(c.name)] = text;
833
+ }
834
+ }
835
+ }
836
+ });
837
+ } catch (_e) {
838
+ return _refuseStatus(res, 400, "MKCALENDAR body XML refused");
839
+ }
840
+ }
841
+ var r = await calStorage.mkcalendar(principalId, calendarId, props);
842
+ res.statusCode = (r && r.created) ? 201 : 200;
843
+ res.end();
844
+ _emit("mail.dav.mkcalendar",
845
+ { principalId: principalId, calendarId: calendarId });
846
+ }
847
+
848
+ // ---- CardDAV dispatch ------------------------------------------------
849
+
850
+ async function dispatchCarddav(actorPrincipalId, req, res) {
851
+ var method = (req.method || "GET").toUpperCase();
852
+ if (CARDDAV_METHODS.indexOf(method) < 0) {
853
+ _emit("mail.dav.refused",
854
+ { method: method, kind: "carddav" }, "denied");
855
+ return _refuseStatus(res, 405, "Method not allowed: " + method);
856
+ }
857
+ if (!actorPrincipalId) {
858
+ _emit("mail.dav.refused",
859
+ { method: method, kind: "carddav", reason: "no-actor" }, "denied");
860
+ return _refuseStatus(res, 401, "Authentication required");
861
+ }
862
+ if (!cardStorage) {
863
+ return _refuseStatus(res, 501,
864
+ "CardDAV not configured (opts.storage.addressbook required)");
865
+ }
866
+
867
+ if (method === "OPTIONS") {
868
+ res.statusCode = 200;
869
+ res.setHeader("DAV", CARDDAV_DAV_HEADER);
870
+ res.setHeader("Allow", CARDDAV_METHODS.join(", "));
871
+ res.setHeader("MS-Author-Via", "DAV");
872
+ res.end();
873
+ _emit("mail.dav.options", { kind: "carddav" });
874
+ return;
875
+ }
876
+
877
+ var parsed = _parsePath(req.url || "");
878
+ if (parsed.rejected) {
879
+ _emit("mail.dav.refused",
880
+ { method: method, reason: parsed.rejected }, "denied");
881
+ return _refuseStatus(res, 400, "Bad URL: " + parsed.rejected);
882
+ }
883
+ if (!parsed.principalId) {
884
+ if (method !== "PROPFIND") {
885
+ return _refuseStatus(res, 400, "Principal segment required");
886
+ }
887
+ } else if (parsed.principalId !== actorPrincipalId) {
888
+ _emit("mail.dav.refused",
889
+ { method: method, kind: "carddav",
890
+ urlPrincipal: parsed.principalId,
891
+ actorPrincipal: actorPrincipalId,
892
+ reason: "cross-principal" }, "denied");
893
+ return _refuseStatus(res, 403, "Cross-principal access refused");
894
+ }
895
+
896
+ var addressbookId = parsed.parts[0] || null;
897
+ var cardId = parsed.parts[1] || null;
898
+
899
+ try {
900
+ switch (method) {
901
+ case "PROPFIND": return await _handleCarddavPropfind(req, res, actorPrincipalId, addressbookId, cardId);
902
+ case "REPORT": return await _handleCarddavReport(req, res, actorPrincipalId, addressbookId);
903
+ case "GET": return await _handleCarddavGet(req, res, actorPrincipalId, addressbookId, cardId);
904
+ case "PUT": return await _handleCarddavPut(req, res, actorPrincipalId, addressbookId, cardId);
905
+ case "DELETE": return await _handleCarddavDelete(req, res, actorPrincipalId, addressbookId, cardId);
906
+ case "MKCOL": return await _handleMkcol(req, res, actorPrincipalId, addressbookId);
907
+ default:
908
+ // CARDDAV_METHODS allowlist gate is enforced above; this
909
+ // branch is unreachable but eslint requires it.
910
+ return _refuseStatus(res, 405, "Method not allowed: " + method);
911
+ }
912
+ } catch (e) {
913
+ _emit("mail.dav.handler_threw",
914
+ { method: method, kind: "carddav",
915
+ error: (e && e.message) || String(e) }, "failure");
916
+ return _refuseStatus(res, 500, "Server error");
917
+ }
918
+ }
919
+
920
+ async function _handleCarddavPropfind(req, res, principalId, addressbookId, cardId) {
921
+ var depth = (req.headers && (req.headers.depth || req.headers.Depth)) || "0";
922
+ var bodyBuf = await _readBodyBytes(req);
923
+ var body = _parsePropfindBody(bodyBuf);
924
+ var responses = [];
925
+
926
+ if (!addressbookId) {
927
+ if (depth === "0") {
928
+ responses.push({
929
+ href: "/carddav/" + principalId + "/",
930
+ propstat: [{ status: 200,
931
+ propXml: _renderProps({ displayName: principalId,
932
+ resourcetype: "<D:collection/>" }, body.props) }],
933
+ });
934
+ } else {
935
+ var books = await cardStorage.listAddressbooks(principalId);
936
+ responses.push({
937
+ href: "/carddav/" + principalId + "/",
938
+ propstat: [{ status: 200,
939
+ propXml: _renderProps({ displayName: principalId,
940
+ resourcetype: "<D:collection/>" }, body.props) }],
941
+ });
942
+ for (var i = 0; i < (books || []).length; i++) {
943
+ var bk = books[i];
944
+ responses.push({
945
+ href: "/carddav/" + principalId + "/" + bk.id + "/",
946
+ propstat: [{ status: 200,
947
+ propXml: _renderProps({
948
+ displayName: bk.displayName || bk.id,
949
+ resourcetype: "<D:collection/><A:addressbook/>",
950
+ etag: bk.etag,
951
+ }, body.props) }],
952
+ });
953
+ }
954
+ }
955
+ } else if (!cardId) {
956
+ var cards = await cardStorage.listCards(principalId, addressbookId, null);
957
+ responses.push({
958
+ href: "/carddav/" + principalId + "/" + addressbookId + "/",
959
+ propstat: [{ status: 200,
960
+ propXml: _renderProps({ displayName: addressbookId,
961
+ resourcetype: "<D:collection/><A:addressbook/>" }, body.props) }],
962
+ });
963
+ if (depth !== "0") {
964
+ for (var j = 0; j < (cards || []).length; j++) {
965
+ var card = cards[j];
966
+ responses.push({
967
+ href: "/carddav/" + principalId + "/" + addressbookId + "/" + card.id,
968
+ propstat: [{ status: 200,
969
+ propXml: _renderProps({
970
+ displayName: card.id,
971
+ etag: card.etag,
972
+ contentType: "text/vcard; charset=utf-8",
973
+ size: card.size,
974
+ vcardBytes: body.props.indexOf("address-data") >= 0 ? card.vcardBytes : null,
975
+ }, body.props) }],
976
+ });
977
+ }
978
+ }
979
+ } else {
980
+ var single = await cardStorage.getCard(principalId, addressbookId, cardId);
981
+ if (!single) {
982
+ responses.push({ href: req.url, status: 404 });
983
+ } else {
984
+ responses.push({
985
+ href: req.url,
986
+ propstat: [{ status: 200,
987
+ propXml: _renderProps({
988
+ displayName: single.id,
989
+ etag: single.etag,
990
+ contentType: "text/vcard; charset=utf-8",
991
+ size: single.size,
992
+ vcardBytes: body.props.indexOf("address-data") >= 0 ? single.vcardBytes : null,
993
+ }, body.props) }],
994
+ });
995
+ }
996
+ }
997
+ res.statusCode = 207;
998
+ res.setHeader("Content-Type", "application/xml; charset=utf-8");
999
+ res.setHeader("DAV", CARDDAV_DAV_HEADER);
1000
+ res.end(_multiStatus(responses));
1001
+ _emit("mail.dav.propfind",
1002
+ { kind: "carddav", principalId: principalId,
1003
+ addressbookId: addressbookId, depth: depth,
1004
+ responseCount: responses.length });
1005
+ }
1006
+
1007
+ async function _handleCarddavReport(req, res, principalId, addressbookId) {
1008
+ var bodyBuf = await _readBodyBytes(req);
1009
+ var report = _parseReportBody(bodyBuf);
1010
+ var responses = [];
1011
+ if (report.kind === "addressbook-multiget") {
1012
+ for (var i = 0; i < report.hrefs.length; i++) {
1013
+ var href = report.hrefs[i];
1014
+ var hp = _parsePath(href);
1015
+ if (hp.rejected || hp.principalId !== principalId) {
1016
+ responses.push({ href: href, status: 403 });
1017
+ continue;
1018
+ }
1019
+ var hpBookId = hp.parts[0];
1020
+ var hpCardId = hp.parts[1];
1021
+ var card = await cardStorage.getCard(principalId, hpBookId, hpCardId);
1022
+ if (!card) {
1023
+ responses.push({ href: href, status: 404 });
1024
+ } else {
1025
+ responses.push({
1026
+ href: href,
1027
+ propstat: [{ status: 200,
1028
+ propXml: _renderProps({
1029
+ etag: card.etag,
1030
+ contentType: "text/vcard; charset=utf-8",
1031
+ size: card.size,
1032
+ vcardBytes: card.vcardBytes,
1033
+ }, report.props.length > 0 ? report.props : ["getetag", "address-data"]) }],
1034
+ });
1035
+ }
1036
+ }
1037
+ } else if (report.kind === "addressbook-query") {
1038
+ var rows = await cardStorage.listCards(principalId, addressbookId, report.filter);
1039
+ for (var j = 0; j < (rows || []).length; j++) {
1040
+ var r = rows[j];
1041
+ responses.push({
1042
+ href: "/carddav/" + principalId + "/" + addressbookId + "/" + r.id,
1043
+ propstat: [{ status: 200,
1044
+ propXml: _renderProps({
1045
+ etag: r.etag,
1046
+ contentType: "text/vcard; charset=utf-8",
1047
+ size: r.size,
1048
+ vcardBytes: r.vcardBytes,
1049
+ }, report.props.length > 0 ? report.props : ["getetag", "address-data"]) }],
1050
+ });
1051
+ }
1052
+ } else {
1053
+ return _refuseStatus(res, 422, "Unsupported REPORT kind: " + report.kind);
1054
+ }
1055
+ res.statusCode = 207;
1056
+ res.setHeader("Content-Type", "application/xml; charset=utf-8");
1057
+ res.setHeader("DAV", CARDDAV_DAV_HEADER);
1058
+ res.end(_multiStatus(responses));
1059
+ _emit("mail.dav.report",
1060
+ { kind: "carddav", reportKind: report.kind, responseCount: responses.length });
1061
+ }
1062
+
1063
+ async function _handleCarddavGet(req, res, principalId, addressbookId, cardId) {
1064
+ if (!cardId) return _refuseStatus(res, 404, "GET requires a card path");
1065
+ var card = await cardStorage.getCard(principalId, addressbookId, cardId);
1066
+ if (!card) return _refuseStatus(res, 404, "Card not found");
1067
+ res.statusCode = 200;
1068
+ res.setHeader("Content-Type", "text/vcard; charset=utf-8");
1069
+ if (card.etag) res.setHeader("ETag", card.etag);
1070
+ res.end(card.vcardBytes);
1071
+ _emit("mail.dav.get",
1072
+ { kind: "carddav", principalId: principalId,
1073
+ addressbookId: addressbookId, cardId: cardId });
1074
+ }
1075
+
1076
+ async function _handleCarddavPut(req, res, principalId, addressbookId, cardId) {
1077
+ if (!cardId) return _refuseStatus(res, 400, "PUT requires a card path");
1078
+ var bodyBuf = await _readBodyBytes(req);
1079
+ try {
1080
+ safeVcard.parse(bodyBuf, {
1081
+ profile: profile,
1082
+ compliancePosture: compliancePosture,
1083
+ });
1084
+ } catch (e) {
1085
+ _emit("mail.dav.refused",
1086
+ { kind: "carddav", method: "PUT", reason: "vcard-refused",
1087
+ error: (e && e.code) || (e && e.message) }, "denied");
1088
+ return _refuseStatus(res, 415,
1089
+ "vCard body refused: " + ((e && e.message) || String(e)));
1090
+ }
1091
+ var ifMatch = (req.headers && (req.headers["if-match"] || req.headers["If-Match"])) || null;
1092
+ var ifNoneMatch = (req.headers && (req.headers["if-none-match"] || req.headers["If-None-Match"])) || null;
1093
+ var result;
1094
+ try {
1095
+ result = await cardStorage.putCard(principalId, addressbookId, cardId,
1096
+ bodyBuf, { ifMatch: ifMatch, ifNoneMatch: ifNoneMatch });
1097
+ } catch (e) {
1098
+ if (e && (e.code === "etag-mismatch" || e.statusCode === 412)) {
1099
+ return _refuseStatus(res, 412, "ETag precondition failed");
1100
+ }
1101
+ throw e;
1102
+ }
1103
+ res.statusCode = result && result.created ? 201 : 204;
1104
+ if (result && result.etag) res.setHeader("ETag", result.etag);
1105
+ res.end();
1106
+ _emit("mail.dav.put",
1107
+ { kind: "carddav", principalId: principalId,
1108
+ addressbookId: addressbookId, cardId: cardId,
1109
+ created: !!(result && result.created) });
1110
+ }
1111
+
1112
+ async function _handleCarddavDelete(req, res, principalId, addressbookId, cardId) {
1113
+ if (!cardId) return _refuseStatus(res, 400, "DELETE requires a card path");
1114
+ var ifMatch = (req.headers && (req.headers["if-match"] || req.headers["If-Match"])) || null;
1115
+ try {
1116
+ var r = await cardStorage.deleteCard(principalId, addressbookId, cardId,
1117
+ { ifMatch: ifMatch });
1118
+ if (!r || r.notFound) return _refuseStatus(res, 404, "Card not found");
1119
+ } catch (e) {
1120
+ if (e && (e.code === "etag-mismatch" || e.statusCode === 412)) {
1121
+ return _refuseStatus(res, 412, "ETag precondition failed");
1122
+ }
1123
+ throw e;
1124
+ }
1125
+ res.statusCode = 204;
1126
+ res.end();
1127
+ _emit("mail.dav.delete",
1128
+ { kind: "carddav", principalId: principalId,
1129
+ addressbookId: addressbookId, cardId: cardId });
1130
+ }
1131
+
1132
+ async function _handleMkcol(req, res, principalId, addressbookId) {
1133
+ if (!addressbookId) {
1134
+ return _refuseStatus(res, 400, "MKCOL requires an addressbook path");
1135
+ }
1136
+ if (typeof cardStorage.mkcol !== "function") {
1137
+ return _refuseStatus(res, 501,
1138
+ "MKCOL not supported (storage.addressbook.mkcol undefined)");
1139
+ }
1140
+ var bodyBuf = await _readBodyBytes(req);
1141
+ var props = Object.create(null);
1142
+ if (bodyBuf.length > 0) {
1143
+ try {
1144
+ var tree = xmlC14n.parse(bodyBuf.toString("utf8"));
1145
+ _walkXml(tree, function (node) {
1146
+ if (_localName(node.name) === "prop" && node.children) {
1147
+ for (var i = 0; i < node.children.length; i++) {
1148
+ var c = node.children[i];
1149
+ if (c.type === "element" && c.children) {
1150
+ var text = "";
1151
+ for (var j = 0; j < c.children.length; j++) {
1152
+ if (c.children[j].type === "text") {
1153
+ text += c.children[j].value || c.children[j].text || "";
1154
+ }
1155
+ }
1156
+ props[_localName(c.name)] = text;
1157
+ }
1158
+ }
1159
+ }
1160
+ });
1161
+ } catch (_e) {
1162
+ return _refuseStatus(res, 400, "MKCOL body XML refused");
1163
+ }
1164
+ }
1165
+ var r = await cardStorage.mkcol(principalId, addressbookId, props);
1166
+ res.statusCode = (r && r.created) ? 201 : 200;
1167
+ res.end();
1168
+ _emit("mail.dav.mkcol",
1169
+ { principalId: principalId, addressbookId: addressbookId });
1170
+ }
1171
+
1172
+ // ---- HTTP handlers (Express-style) -----------------------------------
1173
+
1174
+ function caldavHandler(req, res) {
1175
+ var actorPrincipalId = _actorPrincipalId(req);
1176
+ dispatchCaldav(actorPrincipalId, req, res).catch(function (err) {
1177
+ _emit("mail.dav.handler_threw",
1178
+ { kind: "caldav", error: (err && err.message) || String(err) }, "failure");
1179
+ try { _refuseStatus(res, 500, "Server error"); } catch (_e) { /* response already sent */ }
1180
+ });
1181
+ }
1182
+
1183
+ function carddavHandler(req, res) {
1184
+ var actorPrincipalId = _actorPrincipalId(req);
1185
+ dispatchCarddav(actorPrincipalId, req, res).catch(function (err) {
1186
+ _emit("mail.dav.handler_threw",
1187
+ { kind: "carddav", error: (err && err.message) || String(err) }, "failure");
1188
+ try { _refuseStatus(res, 500, "Server error"); } catch (_e) { /* response already sent */ }
1189
+ });
1190
+ }
1191
+
1192
+ // RFC 6764 — .well-known/caldav + .well-known/carddav. SHOULD return
1193
+ // a 301 redirect to the principal URL (the operator's auth layer
1194
+ // resolves the principal from the bearer token first). For the
1195
+ // framework default, redirect to the static collection root and let
1196
+ // the client follow current-user-principal.
1197
+ function discoveryHandler(req, res) {
1198
+ var path = (req.url || "/").toLowerCase();
1199
+ var target;
1200
+ if (path.indexOf("carddav") >= 0) {
1201
+ target = opts.carddavBaseUrl || "/carddav/";
1202
+ } else {
1203
+ target = opts.caldavBaseUrl || "/caldav/";
1204
+ }
1205
+ res.statusCode = 301;
1206
+ res.setHeader("Location", target);
1207
+ res.end();
1208
+ _emit("mail.dav.discovery", { target: target });
1209
+ }
1210
+
1211
+ return {
1212
+ caldavHandler: caldavHandler,
1213
+ carddavHandler: carddavHandler,
1214
+ discoveryHandler: discoveryHandler,
1215
+ dispatchCaldav: dispatchCaldav,
1216
+ dispatchCarddav: dispatchCarddav,
1217
+ MailDavError: MailDavError,
1218
+ };
1219
+ }
1220
+
1221
+ module.exports = {
1222
+ create: create,
1223
+ MailDavError: MailDavError,
1224
+ };