@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.
- package/CHANGELOG.md +951 -893
- package/index.js +30 -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-imap-command.js +335 -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 +1102 -0
- package/lib/mail-server-jmap.js +488 -0
- package/lib/mail-server-managesieve.js +853 -0
- package/lib/mail-server-mx.js +164 -34
- package/lib/mail-server-pop3.js +836 -0
- package/lib/mail-server-rate-limit.js +269 -0
- package/lib/mail-server-submission.js +1032 -0
- 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 +130 -10
- 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 +168 -17
- 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
package/lib/mail-dav.js
ADDED
|
@@ -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, "&")
|
|
374
|
+
.replace(/</g, "<")
|
|
375
|
+
.replace(/>/g, ">")
|
|
376
|
+
.replace(/"/g, """)
|
|
377
|
+
.replace(/'/g, "'");
|
|
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
|
+
};
|