1 /* jmap_calendar.c -- Routines for handling JMAP calendar messages
2 *
3 * Copyright (c) 1994-2014 Carnegie Mellon University. All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 *
9 * 1. Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 *
12 * 2. Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in
14 * the documentation and/or other materials provided with the
15 * distribution.
16 *
17 * 3. The name "Carnegie Mellon University" must not be used to
18 * endorse or promote products derived from this software without
19 * prior written permission. For permission or any legal
20 * details, please contact
21 * Carnegie Mellon University
22 * Center for Technology Transfer and Enterprise Creation
23 * 4615 Forbes Avenue
24 * Suite 302
25 * Pittsburgh, PA 15213
26 * (412) 268-7393, fax: (412) 268-7395
27 * innovation@andrew.cmu.edu
28 *
29 * 4. Redistributions of any form whatsoever must retain the following
30 * acknowledgment:
31 * "This product includes software developed by Computing Services
32 * at Carnegie Mellon University (http://www.cmu.edu/computing/)."
33 *
34 * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
35 * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
36 * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
37 * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
38 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
39 * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
40 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
41 *
42 */
43
44 #include <config.h>
45
46 #include <ctype.h>
47 #include <errno.h>
48 #include <assert.h>
49 #include <string.h>
50 #include <syslog.h>
51 #ifdef HAVE_UNISTD_H
52 #include <unistd.h>
53 #endif
54
55 #include "acl.h"
56 #include "annotate.h"
57 #include "caldav_db.h"
58 #include "cyr_qsort_r.h"
59 #include "global.h"
60 #include "hash.h"
61 #include "httpd.h"
62 #include "http_caldav.h"
63 #include "http_caldav_sched.h"
64 #include "http_dav.h"
65 #include "http_jmap.h"
66 #include "http_proxy.h"
67 #include "ical_support.h"
68 #include "json_support.h"
69 #include "jmap_ical.h"
70 #include "search_query.h"
71 #include "times.h"
72 #include "user.h"
73 #include "util.h"
74 #include "xmalloc.h"
75
76 /* generated headers are not necessarily in current directory */
77 #include "imap/http_err.h"
78 #include "imap/imap_err.h"
79
80 static int jmap_calendar_get(struct jmap_req *req);
81 static int jmap_calendar_changes(struct jmap_req *req);
82 static int jmap_calendar_set(struct jmap_req *req);
83 static int jmap_calendarevent_get(struct jmap_req *req);
84 static int jmap_calendarevent_changes(struct jmap_req *req);
85 static int jmap_calendarevent_query(struct jmap_req *req);
86 static int jmap_calendarevent_set(struct jmap_req *req);
87 static int jmap_calendarevent_copy(struct jmap_req *req);
88
89 static int jmap_calendarevent_getblob(jmap_req_t *req, const char *blobid,
90 const char *accept);
91
92 #define JMAPCACHE_CALVERSION 19
93
94 jmap_method_t jmap_calendar_methods_standard[] = {
95 // we have no standard for JMAP calendars yet!
96 { NULL, NULL, NULL, 0}
97 };
98
99 jmap_method_t jmap_calendar_methods_nonstandard[] = {
100 {
101 "Calendar/get",
102 JMAP_CALENDARS_EXTENSION,
103 &jmap_calendar_get,
104 JMAP_NEED_CSTATE
105 },
106 {
107 "Calendar/changes",
108 JMAP_CALENDARS_EXTENSION,
109 &jmap_calendar_changes,
110 JMAP_NEED_CSTATE
111 },
112 {
113 "Calendar/set",
114 JMAP_CALENDARS_EXTENSION,
115 &jmap_calendar_set,
116 JMAP_NEED_CSTATE | JMAP_READ_WRITE
117 },
118 {
119 "CalendarEvent/get",
120 JMAP_CALENDARS_EXTENSION,
121 &jmap_calendarevent_get,
122 JMAP_NEED_CSTATE
123 },
124 {
125 "CalendarEvent/changes",
126 JMAP_CALENDARS_EXTENSION,
127 &jmap_calendarevent_changes,
128 JMAP_NEED_CSTATE
129 },
130 {
131 "CalendarEvent/query",
132 JMAP_CALENDARS_EXTENSION,
133 &jmap_calendarevent_query,
134 JMAP_NEED_CSTATE
135 },
136 {
137 "CalendarEvent/set",
138 JMAP_CALENDARS_EXTENSION,
139 &jmap_calendarevent_set,
140 JMAP_NEED_CSTATE | JMAP_READ_WRITE
141 },
142 {
143 "CalendarEvent/copy",
144 JMAP_CALENDARS_EXTENSION,
145 &jmap_calendarevent_copy,
146 JMAP_NEED_CSTATE | JMAP_READ_WRITE
147 },
148 { NULL, NULL, NULL, 0}
149 };
150
jmap_calendar_init(jmap_settings_t * settings)151 HIDDEN void jmap_calendar_init(jmap_settings_t *settings)
152 {
153 jmap_method_t *mp;
154
155 for (mp = jmap_calendar_methods_standard; mp->name; mp++) {
156 hash_insert(mp->name, mp, &settings->methods);
157 }
158
159 if (config_getswitch(IMAPOPT_JMAP_NONSTANDARD_EXTENSIONS)) {
160 json_object_set_new(settings->server_capabilities,
161 JMAP_CALENDARS_EXTENSION, json_object());
162
163 for (mp = jmap_calendar_methods_nonstandard; mp->name; mp++) {
164 hash_insert(mp->name, mp, &settings->methods);
165 }
166 }
167
168 ptrarray_append(&settings->getblob_handlers, jmap_calendarevent_getblob);
169 }
170
jmap_calendar_capabilities(json_t * account_capabilities)171 HIDDEN void jmap_calendar_capabilities(json_t *account_capabilities)
172 {
173 if (config_getswitch(IMAPOPT_JMAP_NONSTANDARD_EXTENSIONS)) {
174 json_object_set_new(account_capabilities, JMAP_CALENDARS_EXTENSION, json_object());
175 }
176 }
177
178 /* Helper flags for CalendarEvent/set */
179 #define JMAP_CREATE (1<<0) /* Current request is a create. */
180 #define JMAP_UPDATE (1<<1) /* Current request is an update. */
181 #define JMAP_DESTROY (1<<2) /* Current request is a destroy. */
182
183 /* Return a non-zero value if uid maps to a special-purpose calendar mailbox,
184 * that may not be read or modified by the user. */
jmap_calendar_isspecial(mbname_t * mbname)185 static int jmap_calendar_isspecial(mbname_t *mbname) {
186 if (!mboxname_iscalendarmailbox(mbname_intname(mbname), 0)) return 1;
187
188 const strarray_t *boxes = mbname_boxes(mbname);
189 const char *lastname = strarray_nth(boxes, boxes->count - 1);
190
191 /* Don't return user.foo.#calendars */
192 if (!strcmp(lastname, config_getstring(IMAPOPT_CALENDARPREFIX))) {
193 return 1;
194 }
195
196 /* SCHED_INBOX and SCHED_OUTBOX end in "/", so trim them */
197 if (!strncmp(lastname, SCHED_INBOX, strlen(SCHED_INBOX)-1)) return 1;
198 if (!strncmp(lastname, SCHED_OUTBOX, strlen(SCHED_OUTBOX)-1)) return 1;
199 if (!strncmp(lastname, MANAGED_ATTACH, strlen(MANAGED_ATTACH)-1)) return 1;
200 return 0;
201 }
202
203 struct getcalendars_rock {
204 struct jmap_req *req;
205 struct jmap_get *get;
206 int skip_hidden;
207 };
208
get_schedule_address_set(const char * userid,const char * mboxname)209 static json_t *get_schedule_address_set(const char *userid,
210 const char *mboxname)
211 {
212 struct buf attrib = BUF_INITIALIZER;
213 json_t *val = json_array();
214 static const char *annot =
215 DAV_ANNOT_NS "<" XML_NS_CALDAV ">calendar-user-address-set";
216 int r = annotatemore_lookupmask(mboxname, annot, httpd_userid, &attrib);
217 if (r || !attrib.len) {
218 // fetch from my own principal
219 char *prinmbox = mboxname_user_mbox(httpd_userid, "#calendars");
220 r = annotatemore_lookupmask(prinmbox, annot, httpd_userid, &attrib);
221 free(prinmbox);
222 }
223 if (!r && attrib.len) {
224 strarray_t *values = strarray_split(buf_cstring(&attrib), ",", STRARRAY_TRIM);
225 int i;
226 for (i = 0; i < strarray_size(values); i++) {
227 const char *item = strarray_nth(values, i);
228 if (!strncasecmp(item, "mailto:", 7)) item += 7;
229 char *value = strconcat("mailto:", item, NULL);
230 json_array_append_new(val, json_string(value));
231 free(value);
232 }
233 strarray_free(values);
234 }
235 else if (strchr(userid, '@')) {
236 char *value = strconcat("mailto:", userid, NULL);
237 json_array_append_new(val, json_string(value));
238 free(value);
239 }
240 else {
241 const char *domain = httpd_extradomain ? httpd_extradomain : config_defdomain;
242 char *value = strconcat("mailto:", userid, "@", domain, NULL);
243 json_array_append_new(val, json_string(value));
244 free(value);
245 }
246 buf_free(&attrib);
247 return val;
248 }
249
getcalendars_cb(const mbentry_t * mbentry,void * vrock)250 static int getcalendars_cb(const mbentry_t *mbentry, void *vrock)
251 {
252 struct getcalendars_rock *rock = vrock;
253 mbname_t *mbname = NULL;
254 int r = 0;
255
256 /* Only calendars... */
257 if (!(mbentry->mbtype & MBTYPE_CALENDAR)) return 0;
258
259 /* ...which are at least readable or visible... */
260 if (!jmap_hasrights_mbentry(rock->req, mbentry, JACL_READITEMS))
261 return rock->skip_hidden ? 0 : IMAP_PERMISSION_DENIED;
262
263 // needed for some fields
264 int rights = jmap_myrights_mbentry(rock->req, mbentry);
265
266 /* ...and contain VEVENTs. */
267 struct buf attrib = BUF_INITIALIZER;
268 static const char *calcompset_annot =
269 DAV_ANNOT_NS "<" XML_NS_CALDAV ">supported-calendar-component-set";
270 unsigned long supported_components = -1; /* ALL component types by default. */
271 r = annotatemore_lookupmask(mbentry->name, calcompset_annot,
272 rock->req->accountid, &attrib);
273 if (attrib.len) {
274 supported_components = strtoul(buf_cstring(&attrib), NULL, 10);
275 buf_free(&attrib);
276 }
277 if (!(supported_components & CAL_COMP_VEVENT)) {
278 goto done;
279 }
280
281 /* OK, we want this one... */
282 mbname = mbname_from_intname(mbentry->name);
283 /* ...unless it's one of the special names. */
284 if (jmap_calendar_isspecial(mbname)) {
285 r = 0;
286 goto done;
287 }
288
289 json_t *obj = json_pack("{}");
290
291 const strarray_t *boxes = mbname_boxes(mbname);
292 const char *id = strarray_nth(boxes, boxes->count-1);
293 json_object_set_new(obj, "id", json_string(id));
294
295 if (jmap_wantprop(rock->get->props, "x-href")) {
296 // XXX - should the x-ref for a shared calendar point
297 // to the authenticated user's calendar home?
298 char *xhref = jmap_xhref(mbentry->name, NULL);
299 json_object_set_new(obj, "x-href", json_string(xhref));
300 free(xhref);
301 }
302
303 if (jmap_wantprop(rock->get->props, "name")) {
304 buf_reset(&attrib);
305 static const char *displayname_annot =
306 DAV_ANNOT_NS "<" XML_NS_DAV ">displayname";
307 r = annotatemore_lookupmask(mbentry->name, displayname_annot,
308 httpd_userid, &attrib);
309 /* fall back to last part of mailbox name */
310 if (r || !attrib.len) buf_setcstr(&attrib, id);
311 json_object_set_new(obj, "name", json_string(buf_cstring(&attrib)));
312 buf_free(&attrib);
313 }
314
315 if (jmap_wantprop(rock->get->props, "color")) {
316 struct buf attrib = BUF_INITIALIZER;
317 static const char *color_annot =
318 DAV_ANNOT_NS "<" XML_NS_APPLE ">calendar-color";
319 r = annotatemore_lookupmask(mbentry->name, color_annot,
320 httpd_userid, &attrib);
321 if (buf_len(&attrib))
322 json_object_set_new(obj, "color", json_string(buf_cstring(&attrib)));
323 buf_free(&attrib);
324 }
325
326 if (jmap_wantprop(rock->get->props, "sortOrder")) {
327 long sort_order = 0;
328 buf_reset(&attrib);
329 static const char *order_annot =
330 DAV_ANNOT_NS "<" XML_NS_APPLE ">calendar-order";
331 r = annotatemore_lookupmask(mbentry->name, order_annot,
332 httpd_userid, &attrib);
333 if (!r && attrib.len) {
334 char *ptr;
335 long val = strtol(buf_cstring(&attrib), &ptr, 10);
336 if (ptr && *ptr == '\0') {
337 sort_order = val;
338 }
339 else {
340 /* Ignore, but report non-numeric calendar-order values */
341 syslog(LOG_WARNING, "sortOrder: strtol(%s) failed",
342 buf_cstring(&attrib));
343 }
344 }
345 json_object_set_new(obj, "sortOrder", json_integer(sort_order));
346 buf_free(&attrib);
347 }
348
349 if (jmap_wantprop(rock->get->props, "isVisible")) {
350 int is_visible = 1;
351 buf_reset(&attrib);
352 static const char *visible_annot =
353 DAV_ANNOT_NS "<" XML_NS_CALDAV ">X-FM-isVisible";
354 r = annotatemore_lookupmask(mbentry->name, visible_annot,
355 httpd_userid, &attrib);
356 if (!r && attrib.len) {
357 const char *val = buf_cstring(&attrib);
358 if (!strncmp(val, "true", 4) || !strncmp(val, "1", 1)) {
359 is_visible = 1;
360 } else if (!strncmp(val, "false", 5) || !strncmp(val, "0", 1)) {
361 is_visible = 0;
362 } else {
363 /* Report invalid value and fall back to default. */
364 syslog(LOG_WARNING,
365 "isVisible: invalid annotation value: %s", val);
366 is_visible = 1;
367 }
368 }
369 json_object_set_new(obj, "isVisible", json_boolean(is_visible));
370 buf_free(&attrib);
371 }
372
373 if (jmap_wantprop(rock->get->props, "isSubscribed")) {
374 int is_subscribed;
375 if (mboxname_userownsmailbox(httpd_userid, mbentry->name)) {
376 /* Users always subscribe their own calendars */
377 is_subscribed = 1;
378 }
379 else {
380 /* Lookup mailbox subscriptions */
381 is_subscribed = mboxlist_checksub(mbentry->name, httpd_userid) == 0;
382 }
383 json_object_set_new(obj, "isSubscribed", json_boolean(is_subscribed));
384 }
385
386 int writerights = JACL_ADDITEMS|JACL_SETMETADATA;
387
388 if (jmap_wantprop(rock->get->props, "mayReadFreeBusy")) {
389 json_object_set_new(obj, "mayReadFreeBusy",
390 ((rights & JACL_READFB) == JACL_READFB) ? json_true() : json_false());
391 }
392
393 if (jmap_wantprop(rock->get->props, "mayReadItems")) {
394 json_object_set_new(obj, "mayReadItems",
395 ((rights & JACL_READITEMS) == JACL_READITEMS) ? json_true() : json_false());
396 }
397
398 if (jmap_wantprop(rock->get->props, "mayAddItems")) {
399 json_object_set_new(obj, "mayAddItems",
400 ((rights & writerights) == writerights) ? json_true() : json_false());
401 }
402
403 if (jmap_wantprop(rock->get->props, "mayModifyItems")) {
404 json_object_set_new(obj, "mayModifyItems",
405 ((rights & writerights) == writerights) ? json_true() : json_false());
406 }
407
408 if (jmap_wantprop(rock->get->props, "mayRemoveItems")) {
409 json_object_set_new(obj, "mayRemoveItems",
410 ((rights & JACL_REMOVEITEMS) == JACL_REMOVEITEMS) ? json_true() : json_false());
411 }
412
413 if (jmap_wantprop(rock->get->props, "mayRename")) {
414 json_object_set_new(obj, "mayRename",
415 ((rights & JACL_RENAME) == JACL_RENAME) ? json_true() : json_false());
416 }
417
418 if (jmap_wantprop(rock->get->props, "mayDelete")) {
419 json_object_set_new(obj, "mayDelete",
420 ((rights & JACL_DELETE) == JACL_DELETE) ? json_true() : json_false());
421 }
422
423 if (jmap_wantprop(rock->get->props, "mayAdmin")) {
424 json_object_set_new(obj, "mayAdmin",
425 ((rights & JACL_ADMIN) == JACL_ADMIN) ? json_true() : json_false());
426 }
427
428 if (jmap_wantprop(rock->get->props, "shareWith")) {
429 json_t *sharewith = jmap_get_sharewith(mbentry);
430 json_object_set_new(obj, "shareWith", sharewith);
431 }
432
433 if (jmap_wantprop(rock->get->props, "scheduleAddressSet")) {
434 json_t *set = get_schedule_address_set(rock->req->userid, mbentry->name);
435 json_object_set_new(obj, "scheduleAddressSet", set);
436 }
437
438 json_array_append_new(rock->get->list, obj);
439
440 done:
441 buf_free(&attrib);
442 mbname_free(&mbname);
443 return r;
444 }
445
446 static const jmap_property_t calendar_props[] = {
447 {
448 "id",
449 NULL,
450 JMAP_PROP_SERVER_SET | JMAP_PROP_IMMUTABLE | JMAP_PROP_ALWAYS_GET
451 },
452 {
453 "name",
454 NULL,
455 0
456 },
457 {
458 "color",
459 NULL,
460 0
461 },
462 {
463 "sortOrder",
464 NULL,
465 0
466 },
467 {
468 "isVisible",
469 NULL,
470 0
471 },
472 {
473 "isSubscribed",
474 NULL,
475 0
476 },
477 {
478 "mayReadFreeBusy",
479 NULL,
480 JMAP_PROP_SERVER_SET
481 },
482 {
483 "mayReadItems",
484 NULL,
485 JMAP_PROP_SERVER_SET
486 },
487 {
488 "mayAddItems",
489 NULL,
490 JMAP_PROP_SERVER_SET
491 },
492 {
493 "mayModifyItems",
494 NULL,
495 JMAP_PROP_SERVER_SET
496 },
497 {
498 "mayRemoveItems",
499 NULL,
500 JMAP_PROP_SERVER_SET
501 },
502 {
503 "mayRename",
504 NULL,
505 JMAP_PROP_SERVER_SET
506 },
507 {
508 "mayDelete",
509 NULL,
510 JMAP_PROP_SERVER_SET
511 },
512
513 /* FM extensions (do ALL of these get through to Cyrus?) */
514 {
515 "mayAdmin",
516 JMAP_CALENDARS_EXTENSION,
517 JMAP_PROP_SERVER_SET
518 },
519 {
520 "syncedFrom",
521 JMAP_CALENDARS_EXTENSION,
522 0
523 },
524 {
525 "isEventsPublic",
526 JMAP_CALENDARS_EXTENSION,
527 0
528 },
529 {
530 "isFreeBusyPublic",
531 JMAP_CALENDARS_EXTENSION,
532 0
533 },
534 {
535 "eventsUrl",
536 JMAP_CALENDARS_EXTENSION,
537 JMAP_PROP_SERVER_SET
538 },
539 {
540 "freeBusyUrl",
541 JMAP_CALENDARS_EXTENSION,
542 JMAP_PROP_SERVER_SET
543 },
544 {
545 "calDavUrl",
546 JMAP_CALENDARS_EXTENSION,
547 JMAP_PROP_SERVER_SET
548 },
549 {
550 "shareWith",
551 JMAP_CALENDARS_EXTENSION,
552 0
553 },
554 {
555 "x-href",
556 JMAP_CALENDARS_EXTENSION,
557 JMAP_PROP_SERVER_SET
558 },
559 {
560 "scheduleAddressSet",
561 JMAP_CALENDARS_EXTENSION,
562 0
563 },
564
565 { NULL, NULL, 0 }
566 };
567
has_calendars_cb(const mbentry_t * mbentry,void * rock)568 static int has_calendars_cb(const mbentry_t *mbentry, void *rock)
569 {
570 jmap_req_t *req = rock;
571 if (mbentry->mbtype == MBTYPE_CALENDAR &&
572 jmap_hasrights_mbentry(req, mbentry, JACL_READITEMS)) {
573 return CYRUSDB_DONE;
574 }
575 return 0;
576 }
577
has_calendars(jmap_req_t * req)578 static int has_calendars(jmap_req_t *req)
579 {
580 mbname_t *mbname = mbname_from_userid(req->accountid);
581 mbname_push_boxes(mbname, config_getstring(IMAPOPT_CALENDARPREFIX));
582 int r = mboxlist_mboxtree(mbname_intname(mbname), has_calendars_cb,
583 req, MBOXTREE_SKIP_ROOT);
584 mbname_free(&mbname);
585 return r == CYRUSDB_DONE;
586 }
587
jmap_calendar_get(struct jmap_req * req)588 static int jmap_calendar_get(struct jmap_req *req)
589 {
590 struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
591 struct jmap_get get;
592 json_t *err = NULL;
593 int r = 0;
594
595 if (!has_calendars(req)) {
596 jmap_error(req, json_pack("{s:s}", "type", "accountNoCalendars"));
597 return 0;
598 }
599
600 /* Parse request */
601 jmap_get_parse(req, &parser, calendar_props, /*allow_null_ids*/1,
602 NULL, NULL, &get, &err);
603 if (err) {
604 jmap_error(req, err);
605 goto done;
606 }
607
608 /* Build callback data */
609 struct getcalendars_rock rock = { req, &get, 1 /*skiphidden*/ };
610
611 /* Does the client request specific mailboxes? */
612 if (JNOTNULL(get.ids)) {
613 size_t i;
614 json_t *jval;
615
616 rock.skip_hidden = 0; /* complain about missing ACL rights */
617 json_array_foreach(get.ids, i, jval) {
618 const char *id = json_string_value(jval);
619 char *mboxname = caldav_mboxname(req->accountid, id);
620 mbentry_t *mbentry = NULL;
621
622 r = mboxlist_lookup(mboxname, &mbentry, NULL);
623 if (r == IMAP_NOTFOUND || !mbentry) {
624 json_array_append(get.not_found, jval);
625 r = 0;
626 }
627 else {
628 r = getcalendars_cb(mbentry, &rock);
629 if (r == IMAP_PERMISSION_DENIED) {
630 json_array_append(get.not_found, jval);
631 r = 0;
632 }
633 }
634
635 if (mbentry) mboxlist_entry_free(&mbentry);
636 free(mboxname);
637 if (r) goto done;
638 }
639 }
640 else {
641 // XXX: replace with a function which only looks inside INBOX.#calendars
642 r = mboxlist_usermboxtree(req->accountid, req->authstate, &getcalendars_cb, &rock, MBOXTREE_INTERMEDIATES);
643 if (r) goto done;
644 }
645
646 /* Build response */
647 json_t *jstate = jmap_getstate(req, MBTYPE_CALENDAR, /*refresh*/0);
648 get.state = xstrdup(json_string_value(jstate));
649 json_decref(jstate);
650 jmap_ok(req, jmap_get_reply(&get));
651
652 done:
653 jmap_parser_fini(&parser);
654 jmap_get_fini(&get);
655 return r;
656 }
657
658 struct calendarchanges_rock {
659 jmap_req_t *req;
660 struct jmap_changes *changes;
661 };
662
getcalendarchanges_cb(const mbentry_t * mbentry,void * vrock)663 static int getcalendarchanges_cb(const mbentry_t *mbentry, void *vrock)
664 {
665 struct calendarchanges_rock *rock = (struct calendarchanges_rock *) vrock;
666 mbname_t *mbname = NULL;
667 jmap_req_t *req = rock->req;
668 int r = 0;
669
670 /* Ignore old changes. */
671 if (mbentry->foldermodseq <= rock->changes->since_modseq) {
672 goto done;
673 }
674
675 /* Ignore any mailboxes that aren't (possibly deleted) calendars. */
676 if (!mboxname_iscalendarmailbox(mbentry->name, mbentry->mbtype))
677 return 0;
678
679 /* Ignore mailboxes that are hidden from us. */
680 /* XXX Deleted mailboxes loose their ACL so we can't determine
681 * if they ever could be read by the authenticated user. We
682 * need to leak these deleted entries to not mess up client state. */
683 if (!(mbentry->mbtype & MBTYPE_DELETED) || strcmpsafe(mbentry->acl, "")) {
684 if (!jmap_hasrights_mbentry(req, mbentry, JACL_READITEMS)) return 0;
685 }
686
687 /* Ignore special-purpose calendar mailboxes. */
688 mbname = mbname_from_intname(mbentry->name);
689 if (jmap_calendar_isspecial(mbname)) {
690 goto done;
691 }
692
693 /* Ignore calendars that don't store VEVENTs */
694 struct buf attrib = BUF_INITIALIZER;
695 static const char *calcompset_annot =
696 DAV_ANNOT_NS "<" XML_NS_CALDAV ">supported-calendar-component-set";
697 unsigned long supported_components = -1; /* ALL component types by default. */
698 r = annotatemore_lookupmask(mbentry->name, calcompset_annot,
699 rock->req->accountid, &attrib);
700 if (attrib.len) {
701 supported_components = strtoul(buf_cstring(&attrib), NULL, 10);
702 buf_free(&attrib);
703 }
704 if (!(supported_components & CAL_COMP_VEVENT)) {
705 goto done;
706 }
707
708 const strarray_t *boxes = mbname_boxes(mbname);
709 const char *id = strarray_nth(boxes, boxes->count-1);
710
711 /* Report this calendar as created, updated or destroyed. */
712 if (mbentry->mbtype & MBTYPE_DELETED) {
713 if (mbentry->createdmodseq <= rock->changes->since_modseq)
714 json_array_append_new(rock->changes->destroyed, json_string(id));
715 }
716 else {
717 if (mbentry->createdmodseq <= rock->changes->since_modseq)
718 json_array_append_new(rock->changes->updated, json_string(id));
719 else
720 json_array_append_new(rock->changes->created, json_string(id));
721 }
722
723 done:
724 mbname_free(&mbname);
725 return r;
726 }
727
jmap_calendar_changes(struct jmap_req * req)728 static int jmap_calendar_changes(struct jmap_req *req)
729 {
730 struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
731 struct jmap_changes changes;
732 json_t *err = NULL;
733 int r = 0;
734
735 if (!has_calendars(req)) {
736 jmap_error(req, json_pack("{s:s}", "type", "accountNoCalendars"));
737 return 0;
738 }
739
740 /* Parse request */
741 jmap_changes_parse(req, &parser, req->counters.caldavfoldersdeletedmodseq,
742 NULL, NULL, &changes, &err);
743 if (err) {
744 jmap_error(req, err);
745 goto done;
746 }
747
748 /* Lookup any changes. */
749 char *mboxname = caldav_mboxname(req->accountid, NULL);
750 struct calendarchanges_rock rock = { req, &changes };
751
752 r = mboxlist_mboxtree(mboxname, getcalendarchanges_cb, &rock,
753 MBOXTREE_TOMBSTONES|MBOXTREE_SKIP_ROOT);
754 free(mboxname);
755 if (r) {
756 jmap_error(req, json_pack("{s:s}", "type", "cannotCalculateChanges"));
757 r = 0;
758 goto done;
759 }
760
761 /* Determine new state. XXX what about max_changes? */
762 changes.new_modseq = /*changes.has_more_changes ? rock.highestmodseq :*/
763 jmap_highestmodseq(req, MBTYPE_CALENDAR);
764
765 /* Build response */
766 jmap_ok(req, jmap_changes_reply(&changes));
767
768 done:
769 jmap_changes_fini(&changes);
770 jmap_parser_fini(&parser);
771 if (r) {
772 jmap_error(req, jmap_server_error(r));
773 }
774 return 0;
775 }
776
777 /* jmap calendar APIs */
778
779 struct setcalendar_props {
780 const char *name;
781 const char *color;
782 int sortOrder;
783 int isVisible;
784 int isSubscribed;
785 json_t *scheduleAddressSet;
786 struct {
787 json_t *With;
788 int overwrite_acl;
789 } share;
790 long comp_types;
791 };
792
793 /* Update the calendar properties in the calendar mailbox named mboxname.
794 * NULL values and negative integers are ignored. Return 0 on success. */
setcalendars_update(jmap_req_t * req,const char * mboxname,struct setcalendar_props * props,int ignore_acl)795 static int setcalendars_update(jmap_req_t *req,
796 const char *mboxname,
797 struct setcalendar_props *props,
798 int ignore_acl)
799 {
800 struct mailbox *mbox = NULL;
801 annotate_state_t *astate = NULL;
802 struct buf val = BUF_INITIALIZER;
803 int r;
804
805 if (!jmap_hasrights(req, mboxname, JACL_READITEMS) && !ignore_acl)
806 return IMAP_MAILBOX_NONEXISTENT;
807
808 r = jmap_openmbox(req, mboxname, &mbox, 1);
809 if (r) {
810 syslog(LOG_ERR, "jmap_openmbox(req, %s) failed: %s",
811 mboxname, error_message(r));
812 return r;
813 }
814
815 r = mailbox_get_annotate_state(mbox, 0, &astate);
816 if (r) {
817 syslog(LOG_ERR, "IOERROR: failed to open annotations %s: %s",
818 mbox->name, error_message(r));
819 }
820 /* name */
821 if (!r && props->name) {
822 buf_setcstr(&val, props->name);
823 static const char *displayname_annot =
824 DAV_ANNOT_NS "<" XML_NS_DAV ">displayname";
825 r = annotate_state_writemask(astate, displayname_annot, req->userid, &val);
826 if (r) {
827 syslog(LOG_ERR, "failed to write annotation %s: %s",
828 displayname_annot, error_message(r));
829 }
830 buf_reset(&val);
831 }
832 /* color */
833 if (!r && props->color) {
834 buf_setcstr(&val, props->color);
835 static const char *color_annot =
836 DAV_ANNOT_NS "<" XML_NS_APPLE ">calendar-color";
837 r = annotate_state_writemask(astate, color_annot, req->userid, &val);
838 if (r) {
839 syslog(LOG_ERR, "failed to write annotation %s: %s",
840 color_annot, error_message(r));
841 }
842 buf_reset(&val);
843 }
844 /* sortOrder */
845 if (!r && props->sortOrder >= 0) {
846 buf_printf(&val, "%d", props->sortOrder);
847 static const char *sortOrder_annot =
848 DAV_ANNOT_NS "<" XML_NS_APPLE ">calendar-order";
849 r = annotate_state_writemask(astate, sortOrder_annot, req->userid, &val);
850 if (r) {
851 syslog(LOG_ERR, "failed to write annotation %s: %s",
852 sortOrder_annot, error_message(r));
853 }
854 buf_reset(&val);
855 }
856 /* isVisible */
857 if (!r && props->isVisible >= 0) {
858 buf_setcstr(&val, props->isVisible ? "true" : "false");
859 static const char *visible_annot =
860 DAV_ANNOT_NS "<" XML_NS_CALDAV ">X-FM-isVisible";
861 r = annotate_state_writemask(astate, visible_annot, req->userid, &val);
862 if (r) {
863 syslog(LOG_ERR, "failed to write annotation %s: %s",
864 visible_annot, error_message(r));
865 }
866 buf_reset(&val);
867 }
868 /* scheduleAddressSet */
869 if (!r && json_is_array(props->scheduleAddressSet)) {
870 static const char *annot =
871 DAV_ANNOT_NS "<" XML_NS_CALDAV ">calendar-user-address-set";
872 strarray_t *array = strarray_new();
873 size_t i;
874 json_t *jval;
875 json_array_foreach(props->scheduleAddressSet, i, jval) {
876 strarray_add(array, json_string_value(jval));
877 }
878 char *joined = strarray_join(array, ",");
879 buf_setcstr(&val, joined);
880 r = annotate_state_writemask(astate, annot, req->userid, &val);
881 if (r) {
882 syslog(LOG_ERR, "failed to write annotation %s: %s",
883 annot, error_message(r));
884 }
885 free(joined);
886 strarray_free(array);
887 buf_reset(&val);
888 }
889
890 /* isSubscribed */
891 if (!r && props->isSubscribed >= 0) {
892 /* Update subscription database */
893 r = mboxlist_changesub(mboxname, req->userid, req->authstate,
894 props->isSubscribed, 0, /*notify*/1);
895
896 /* Set invite status for CalDAV */
897 buf_setcstr(&val, props->isSubscribed ? "invite-accepted" : "invite-declined");
898 static const char *invite_annot =
899 DAV_ANNOT_NS "<" XML_NS_DAV ">invite-status";
900 r = annotate_state_writemask(astate, invite_annot, req->userid, &val);
901 if (r) {
902 syslog(LOG_ERR, "failed to write annotation %s: %s",
903 invite_annot, error_message(r));
904 }
905 buf_reset(&val);
906 }
907 /* shareWith */
908 if (!r && props->share.With) {
909 r = jmap_set_sharewith(mbox,
910 props->share.With, props->share.overwrite_acl);
911 if (!r) {
912 char *userid = mboxname_to_userid(mbox->name);
913 r = caldav_update_shareacls(userid);
914 free(userid);
915 }
916 }
917
918 /* supported components */
919 if (!r && props->comp_types >= 0) {
920 const char *comp_annot =
921 DAV_ANNOT_NS "<" XML_NS_CALDAV ">supported-calendar-component-set";
922 buf_printf(&val, "%lu", (unsigned long) props->comp_types);
923 r = annotate_state_writemask(astate, comp_annot, req->userid, &val);
924 if (r) {
925 syslog(LOG_ERR, "failed to write annotation %s: %s",
926 comp_annot, error_message(r));
927 }
928 buf_reset(&val);
929 }
930
931 buf_free(&val);
932 if (r) {
933 mailbox_abort(mbox);
934 }
935 jmap_closembox(req, &mbox);
936 return r;
937 }
938
939 /* Delete the calendar mailbox named mboxname for the userid in req. */
setcalendars_destroy(jmap_req_t * req,const char * mboxname)940 static int setcalendars_destroy(jmap_req_t *req, const char *mboxname)
941 {
942 if (!jmap_hasrights(req, mboxname, JACL_READITEMS))
943 return IMAP_NOTFOUND;
944 if (!jmap_hasrights(req, mboxname, JACL_DELETE))
945 return IMAP_PERMISSION_DENIED;
946
947 char *userid = mboxname_to_userid(mboxname);
948 struct caldav_db *db = caldav_open_userid(userid);
949 if (!db) {
950 syslog(LOG_ERR, "caldav_open_mailbox failed for user %s", userid);
951 free(userid);
952 return IMAP_INTERNAL;
953 }
954 /* XXX
955 * JMAP spec says that: "A calendar MAY be deleted that is currently
956 * associated with one or more events. In this case, the events belonging
957 * to this calendar MUST also be deleted. Conceptually, this MUST happen
958 * prior to the calendar itself being deleted, and MUST generate a push
959 * event that modifies the calendarState for the account, and has a
960 * clientId of null, to indicate that a change has been made to the
961 * calendar data not explicitly requested by the client."
962 *
963 * Need the Events API for this requirement.
964 */
965 int r = caldav_delmbox(db, mboxname);
966 if (r) {
967 syslog(LOG_ERR, "failed to delete mailbox from caldav_db: %s",
968 error_message(r));
969 free(userid);
970 return r;
971 }
972 jmap_myrights_delete(req, mboxname);
973
974 /* Remove from subscriptions db */
975 mboxlist_changesub(mboxname, req->userid, req->authstate, 0, 1, 0);
976
977 struct mboxevent *mboxevent = mboxevent_new(EVENT_MAILBOX_DELETE);
978 if (mboxlist_delayed_delete_isenabled()) {
979 r = mboxlist_delayed_deletemailbox(mboxname,
980 httpd_userisadmin || httpd_userisproxyadmin,
981 httpd_userid, req->authstate, mboxevent,
982 MBOXLIST_DELETE_CHECKACL);
983 } else {
984 r = mboxlist_deletemailbox(mboxname,
985 httpd_userisadmin || httpd_userisproxyadmin,
986 httpd_userid, req->authstate, mboxevent,
987 MBOXLIST_DELETE_CHECKACL);
988 }
989 mboxevent_free(&mboxevent);
990
991 if (!r) r = caldav_update_shareacls(userid);
992
993 int rr = caldav_close(db);
994 if (!r) r = rr;
995
996 free(userid);
997
998 return r;
999 }
1000
jmap_calendar_set(struct jmap_req * req)1001 static int jmap_calendar_set(struct jmap_req *req)
1002 {
1003 struct mboxlock *namespacelock = user_namespacelock(req->accountid);
1004 struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
1005 struct jmap_set set;
1006 json_t *err = NULL;
1007 int r = 0;
1008
1009 /* Parse arguments */
1010 jmap_set_parse(req, &parser, calendar_props, NULL, NULL, &set, &err);
1011 if (err) {
1012 jmap_error(req, err);
1013 goto done;
1014 }
1015
1016 if (set.if_in_state) {
1017 /* TODO rewrite state function to use char* not json_t* */
1018 json_t *jstate = json_string(set.if_in_state);
1019 if (jmap_cmpstate(req, jstate, MBTYPE_CALENDAR)) {
1020 jmap_error(req, json_pack("{s:s}", "type", "stateMismatch"));
1021 json_decref(jstate);
1022 goto done;
1023 }
1024 json_decref(jstate);
1025 set.old_state = xstrdup(set.if_in_state);
1026 }
1027 else {
1028 json_t *jstate = jmap_getstate(req, MBTYPE_CALENDAR, /*refresh*/0);
1029 set.old_state = xstrdup(json_string_value(jstate));
1030 json_decref(jstate);
1031 }
1032
1033 r = caldav_create_defaultcalendars(req->accountid);
1034 if (r == IMAP_MAILBOX_NONEXISTENT) {
1035 /* The account exists but does not have a root mailbox. */
1036 json_t *err = json_pack("{s:s}", "type", "accountNoCalendars");
1037 json_array_append_new(req->response, json_pack("[s,o,s]",
1038 "error", err, req->tag));
1039 r = 0;
1040 goto done;
1041 }
1042 if (r) {
1043 goto done;
1044 }
1045
1046 /* create */
1047 const char *key;
1048 json_t *arg, *record;
1049 json_object_foreach(set.create, key, arg) {
1050 /* Validate calendar id. */
1051 if (!strlen(key)) {
1052 json_t *err= json_pack("{s:s}", "type", "invalidArguments");
1053 json_object_set_new(set.not_created, key, err);
1054 continue;
1055 }
1056
1057 /* Parse and validate properties. */
1058 json_t *invalid = json_pack("[]"), *shareWith = NULL;
1059 const char *name = NULL;
1060 const char *color = NULL;
1061 int32_t sortOrder = 0;
1062 int isVisible = 1;
1063 int isSubscribed = 1;
1064 int pe; /* parse error */
1065 short flag;
1066 json_t *scheduleAddressSet = NULL;
1067
1068 /* Mandatory properties. */
1069 pe = jmap_readprop(arg, "name", 1, invalid, "s", &name);
1070 if (pe > 0 && strnlen(name, 256) == 256) {
1071 json_array_append_new(invalid, json_string("name"));
1072 }
1073
1074 jmap_readprop(arg, "color", 0, invalid, "s", &color);
1075
1076 pe = jmap_readprop(arg, "sortOrder", 0, invalid, "i", &sortOrder);
1077 if (pe > 0 && sortOrder < 0) {
1078 json_array_append_new(invalid, json_string("sortOrder"));
1079 }
1080 jmap_readprop(arg, "isVisible", 0, invalid, "b", &isVisible);
1081 pe = jmap_readprop(arg, "isSubscribed", 0, invalid, "b", &isSubscribed);
1082 if (pe > 0 && !strcmp(req->accountid, req->userid)) {
1083 if (!isSubscribed) {
1084 /* XXX unsubscribing own calendars isn't supported */
1085 json_array_append_new(invalid, json_string("isSubscribed"));
1086 }
1087 else {
1088 isSubscribed = -1; // ignore
1089 }
1090 }
1091
1092 /* Optional properties. */
1093 jmap_readprop(arg, "shareWith", 0, invalid, "o", &shareWith);
1094
1095 jmap_readprop(arg, "scheduleAddressSet", 0, invalid, "o", &scheduleAddressSet);
1096
1097 /* Optional properties. If present, these MUST be set to true. */
1098 flag = 1; jmap_readprop(arg, "mayReadFreeBusy", 0, invalid, "b", &flag);
1099 if (!flag) {
1100 json_array_append_new(invalid, json_string("mayReadFreeBusy"));
1101 }
1102 flag = 1; jmap_readprop(arg, "mayReadItems", 0, invalid, "b", &flag);
1103 if (!flag) {
1104 json_array_append_new(invalid, json_string("mayReadItems"));
1105 }
1106 flag = 1; jmap_readprop(arg, "mayAddItems", 0, invalid, "b", &flag);
1107 if (!flag) {
1108 json_array_append_new(invalid, json_string("mayAddItems"));
1109 }
1110 flag = 1; jmap_readprop(arg, "mayModifyItems", 0, invalid, "b", &flag);
1111 if (!flag) {
1112 json_array_append_new(invalid, json_string("mayModifyItems"));
1113 }
1114 flag = 1; jmap_readprop(arg, "mayRemoveItems", 0, invalid, "b", &flag);
1115 if (!flag) {
1116 json_array_append_new(invalid, json_string("mayRemoveItems"));
1117 }
1118 flag = 1; jmap_readprop(arg, "mayRename", 0, invalid, "b", &flag);
1119 if (!flag) {
1120 json_array_append_new(invalid, json_string("mayRename"));
1121 }
1122 flag = 1; jmap_readprop(arg, "mayDelete", 0, invalid, "b", &flag);
1123 if (!flag) {
1124 json_array_append_new(invalid, json_string("mayDelete"));
1125 }
1126 flag = 1; jmap_readprop(arg, "mayAdmin", 0, invalid, "b", &flag);
1127 if (!flag) {
1128 json_array_append_new(invalid, json_string("mayAdmin"));
1129 }
1130
1131 /* Report any property errors and bail out. */
1132 if (json_array_size(invalid)) {
1133 json_t *err = json_pack("{s:s, s:o}",
1134 "type", "invalidProperties",
1135 "properties", invalid);
1136 json_object_set_new(set.not_created, key, err);
1137 continue;
1138 }
1139 json_decref(invalid);
1140
1141 /* Make sure we are allowed to create the calendar */
1142 char *parentname = caldav_mboxname(req->accountid, NULL);
1143 mbentry_t *mbparent = NULL;
1144 mboxlist_lookup(parentname, &mbparent, NULL);
1145 free(parentname);
1146 if (!jmap_hasrights_mbentry(req, mbparent, JACL_CREATECHILD)) {
1147 json_t *err = json_pack("{s:s}", "type", "accountReadOnly");
1148 json_object_set_new(set.not_created, key, err);
1149 mboxlist_entry_free(&mbparent);
1150 continue;
1151 }
1152 char *newacl = xstrdup("");
1153 char *acl = xstrdup(mbparent->acl);
1154 mboxlist_entry_free(&mbparent);
1155
1156 /* keep just the owner and admin parts of the new ACL! Everything
1157 * else will be added from share.With. All this crap should be
1158 * modularised some more rather than open-coded, but here we go */
1159 char *userid;
1160 char *nextid = NULL;
1161 for (userid = acl; userid; userid = nextid) {
1162 char *rightstr;
1163 int access;
1164
1165 rightstr = strchr(userid, '\t');
1166 if (!rightstr) break;
1167 *rightstr++ = '\0';
1168
1169 nextid = strchr(rightstr, '\t');
1170 if (!nextid) break;
1171 *nextid++ = '\0';
1172
1173 if (!strcmp(userid, req->accountid) || is_system_user(userid)) {
1174 /* owner or system */
1175 cyrus_acl_strtomask(rightstr, &access);
1176 r = cyrus_acl_set(&newacl, userid,
1177 ACL_MODE_SET, access, NULL, NULL);
1178 if (r) {
1179 free(acl);
1180 free(newacl);
1181 syslog(LOG_ERR, "IOERROR: failed to set_acl for calendar create (%s, %s) %s",
1182 userid, req->accountid, error_message(r));
1183 goto done;
1184 }
1185 }
1186 }
1187 free(acl);
1188
1189 /* Create the calendar */
1190 char *uid = xstrdup(makeuuid());
1191 char *mboxname = caldav_mboxname(req->accountid, uid);
1192 r = mboxlist_createsync(mboxname, MBTYPE_CALENDAR,
1193 NULL /* partition */,
1194 httpd_userid, httpd_authstate,
1195 /*options*/0, /*uidvalidity*/0,
1196 0, 0, 0, newacl, /*uniqueid*/NULL,
1197 /*localonly*/0, /*keep_intermediaries*/0,
1198 /*mailboxptr*/NULL);
1199 free(newacl);
1200 if (r) {
1201 syslog(LOG_ERR, "IOERROR: failed to create %s (%s)",
1202 mboxname, error_message(r));
1203 if (r == IMAP_PERMISSION_DENIED) {
1204 json_t *err = json_pack("{s:s}", "type", "accountReadOnly");
1205 json_object_set_new(set.not_created, key, err);
1206 }
1207 free(uid);
1208 free(mboxname);
1209 goto done;
1210 }
1211 struct setcalendar_props props = {
1212 name, color, sortOrder, isVisible, isSubscribed, scheduleAddressSet,
1213 { shareWith, /*overwrite_acl*/ 1}, config_types_to_caldav_types()
1214 };
1215 r = setcalendars_update(req, mboxname, &props, /*ignore_acl*/1);
1216 if (r) {
1217 free(uid);
1218 int rr = mboxlist_delete(mboxname);
1219 if (rr) {
1220 syslog(LOG_ERR, "could not delete mailbox %s: %s",
1221 mboxname, error_message(rr));
1222 }
1223 free(mboxname);
1224 goto done;
1225 }
1226
1227 /* Report calendar as created. */
1228 record = json_pack("{s:s}", "id", uid);
1229
1230 /* Add additional properties */
1231 if (jmap_is_using(req, JMAP_CALENDARS_EXTENSION)) {
1232 json_t *addrset = get_schedule_address_set(req->userid, mboxname);
1233 if (addrset) json_object_set_new(record, "scheduleAddressSet", addrset);
1234 }
1235
1236 json_object_set_new(set.created, key, record);
1237 jmap_add_id(req, key, uid);
1238 free(uid);
1239 free(mboxname);
1240 }
1241
1242 /* update */
1243 const char *uid;
1244 json_object_foreach(set.update, uid, arg) {
1245
1246 /* Validate uid */
1247 if (!uid) {
1248 continue;
1249 }
1250 if (uid && uid[0] == '#') {
1251 const char *newuid = jmap_lookup_id(req, uid + 1);
1252 if (!newuid) {
1253 json_t *err = json_pack("{s:s}", "type", "notFound");
1254 json_object_set_new(set.not_updated, uid, err);
1255 continue;
1256 }
1257 uid = newuid;
1258 }
1259
1260 /* Parse and validate properties. */
1261 json_t *invalid = json_pack("[]"), *shareWith = NULL;
1262
1263 char *mboxname = caldav_mboxname(req->accountid, uid);
1264 const char *name = NULL;
1265 const char *color = NULL;
1266 int32_t sortOrder = -1;
1267 int isVisible = -1;
1268 int isSubscribed = -1;
1269 int overwrite_acl = 1;
1270 int flag;
1271 json_t *scheduleAddressSet = NULL;
1272 int pe = 0; /* parse error */
1273 pe = jmap_readprop(arg, "name", 0, invalid, "s", &name);
1274 if (pe > 0 && strnlen(name, 256) == 256) {
1275 json_array_append_new(invalid, json_string("name"));
1276 }
1277 jmap_readprop(arg, "color", 0, invalid, "s", &color);
1278 pe = jmap_readprop(arg, "sortOrder", 0, invalid, "i", &sortOrder);
1279 if (pe > 0 && sortOrder < 0) {
1280 json_array_append_new(invalid, json_string("sortOrder"));
1281 }
1282 jmap_readprop(arg, "isVisible", 0, invalid, "b", &isVisible);
1283 pe = jmap_readprop(arg, "isSubscribed", 0, invalid, "b", &isSubscribed);
1284 if (pe > 0 && !strcmp(req->accountid, req->userid)) {
1285 if (!isSubscribed) {
1286 /* XXX unsubscribing own calendars isn't supported */
1287 json_array_append_new(invalid, json_string("isSubscribed"));
1288 }
1289 else {
1290 isSubscribed = -1; // ignore
1291 }
1292 }
1293
1294 /* Is shareWith overwritten or patched? */
1295 jmap_parse_sharewith_patch(arg, &shareWith);
1296 if (shareWith) {
1297 overwrite_acl = 0;
1298 json_object_set_new(arg, "shareWith", shareWith);
1299 }
1300 pe = jmap_readprop(arg, "shareWith", 0, invalid, "o", &shareWith);
1301 if (pe > 0 && !jmap_hasrights(req, mboxname, JACL_ADMIN)) {
1302 json_array_append_new(invalid, json_string("shareWith"));
1303 }
1304
1305 jmap_readprop(arg, "scheduleAddressSet", 0, invalid, "o", &scheduleAddressSet);
1306
1307 /* The mayFoo properties are immutable and MUST NOT set. */
1308 pe = jmap_readprop(arg, "mayReadFreeBusy", 0, invalid, "b", &flag);
1309 if (pe > 0) {
1310 json_array_append_new(invalid, json_string("mayReadFreeBusy"));
1311 }
1312 pe = jmap_readprop(arg, "mayReadItems", 0, invalid, "b", &flag);
1313 if (pe > 0) {
1314 json_array_append_new(invalid, json_string("mayReadItems"));
1315 }
1316 pe = jmap_readprop(arg, "mayAddItems", 0, invalid, "b", &flag);
1317 if (pe > 0) {
1318 json_array_append_new(invalid, json_string("mayAddItems"));
1319 }
1320 pe = jmap_readprop(arg, "mayModifyItems", 0, invalid, "b", &flag);
1321 if (pe > 0) {
1322 json_array_append_new(invalid, json_string("mayModifyItems"));
1323 }
1324 pe = jmap_readprop(arg, "mayRemoveItems", 0, invalid, "b", &flag);
1325 if (pe > 0) {
1326 json_array_append_new(invalid, json_string("mayRemoveItems"));
1327 }
1328 pe = jmap_readprop(arg, "mayRename", 0, invalid, "b", &flag);
1329 if (pe > 0) {
1330 json_array_append_new(invalid, json_string("mayRename"));
1331 }
1332 pe = jmap_readprop(arg, "mayDelete", 0, invalid, "b", &flag);
1333 if (pe > 0) {
1334 json_array_append_new(invalid, json_string("mayDelete"));
1335 }
1336
1337 /* Report any property errors and bail out. */
1338 if (json_array_size(invalid)) {
1339 json_t *err = json_pack("{s:s, s:o}",
1340 "type", "invalidProperties",
1341 "properties", invalid);
1342 json_object_set_new(set.not_updated, uid, err);
1343 free(mboxname);
1344 continue;
1345 }
1346 json_decref(invalid);
1347
1348 /* Make sure we don't mess up special calendars */
1349 mbname_t *mbname = mbname_from_intname(mboxname);
1350 if (!mbname || jmap_calendar_isspecial(mbname)) {
1351 json_t *err = json_pack("{s:s}", "type", "notFound");
1352 json_object_set_new(set.not_updated, uid, err);
1353 mbname_free(&mbname);
1354 free(mboxname);
1355 continue;
1356 }
1357 mbname_free(&mbname);
1358
1359 /* Update the calendar */
1360 struct setcalendar_props props = {
1361 name, color, sortOrder, isVisible, isSubscribed, scheduleAddressSet,
1362 { shareWith, overwrite_acl}, /*comp_types*/ -1
1363 };
1364 r = setcalendars_update(req, mboxname, &props, /*ignore_acl*/0);
1365 free(mboxname);
1366 if (r == IMAP_NOTFOUND || r == IMAP_MAILBOX_NONEXISTENT) {
1367 json_t *err = json_pack("{s:s}", "type", "notFound");
1368 json_object_set_new(set.not_updated, uid, err);
1369 r = 0;
1370 continue;
1371 }
1372 else if (r == IMAP_PERMISSION_DENIED) {
1373 json_t *err = json_pack("{s:s}", "type", "accountReadOnly");
1374 json_object_set_new(set.not_updated, uid, err);
1375 r = 0;
1376 continue;
1377 }
1378
1379 /* Report calendar as updated. */
1380 json_object_set_new(set.updated, uid, json_null());
1381 }
1382
1383
1384 /* destroy */
1385 size_t index;
1386 json_t *juid;
1387
1388 json_array_foreach(set.destroy, index, juid) {
1389
1390 /* Validate uid */
1391 const char *uid = json_string_value(juid);
1392 if (!uid) {
1393 continue;
1394 }
1395 if (uid && uid[0] == '#') {
1396 const char *newuid = jmap_lookup_id(req, uid + 1);
1397 if (!newuid) {
1398 json_t *err = json_pack("{s:s}", "type", "notFound");
1399 json_object_set_new(set.not_destroyed, uid, err);
1400 continue;
1401 }
1402 uid = newuid;
1403 }
1404
1405 /* Do not allow to remove the default calendar. */
1406 char *defaultname = caldav_scheddefault(req->accountid);
1407 if (!strcmp(uid, defaultname)) {
1408 /* XXX - The isDefault set error is not documented in the spec. */
1409 json_t *err = json_pack("{s:s}", "type", "isDefault");
1410 json_object_set_new(set.not_destroyed, uid, err);
1411 free(defaultname);
1412 continue;
1413 }
1414 free(defaultname);
1415
1416 /* Make sure we don't delete special calendars */
1417 char *mboxname = caldav_mboxname(req->accountid, uid);
1418 mbname_t *mbname = mbname_from_intname(mboxname);
1419 if (!mbname || jmap_calendar_isspecial(mbname)) {
1420 json_t *err = json_pack("{s:s}", "type", "notFound");
1421 json_object_set_new(set.not_destroyed, uid, err);
1422 mbname_free(&mbname);
1423 free(mboxname);
1424 continue;
1425 }
1426 mbname_free(&mbname);
1427
1428 /* Destroy calendar. */
1429 r = setcalendars_destroy(req, mboxname);
1430 free(mboxname);
1431 if (r == IMAP_NOTFOUND || r == IMAP_MAILBOX_NONEXISTENT) {
1432 json_t *err = json_pack("{s:s}", "type", "notFound");
1433 json_object_set_new(set.not_destroyed, uid, err);
1434 r = 0;
1435 continue;
1436 } else if (r == IMAP_PERMISSION_DENIED) {
1437 json_t *err = json_pack("{s:s}", "type", "accountReadOnly");
1438 json_object_set_new(set.not_destroyed, uid, err);
1439 r = 0;
1440 continue;
1441 } else if (r) {
1442 goto done;
1443 }
1444
1445 /* Report calendar as destroyed. */
1446 json_array_append_new(set.destroyed, json_string(uid));
1447 }
1448
1449
1450 // TODO refactor jmap_getstate to return a string, once
1451 // all code has been migrated to the new JMAP parser.
1452 json_t *jstate = jmap_getstate(req, MBTYPE_CALENDAR, /*refresh*/1);
1453 set.new_state = xstrdup(json_string_value(jstate));
1454 json_decref(jstate);
1455
1456 jmap_ok(req, jmap_set_reply(&set));
1457
1458 done:
1459 mboxname_release(&namespacelock);
1460 jmap_parser_fini(&parser);
1461 jmap_set_fini(&set);
1462 return r;
1463 }
1464
_encode_calendarevent_blobid(struct caldav_data * cdata,const char * userid,struct buf * dst)1465 static const char *_encode_calendarevent_blobid(struct caldav_data *cdata,
1466 const char *userid,
1467 struct buf *dst)
1468 {
1469 /* Set iCalendar smart blob prefix */
1470 buf_putc(dst, 'I');
1471
1472 /* Encode iCalendar UID */
1473 char *b64uid =
1474 jmap_encode_base64_nopad(cdata->ical_uid, strlen(cdata->ical_uid));
1475 if (!b64uid) {
1476 buf_reset(dst);
1477 return NULL;
1478 }
1479 buf_appendcstr(dst, b64uid);
1480 free(b64uid);
1481
1482 /* Encode modseq */
1483 buf_printf(dst, "-" MODSEQ_FMT, cdata->dav.modseq);
1484
1485 /* Encode user id */
1486 if (userid) {
1487 buf_putc(dst, '-');
1488 char *b64userid = jmap_encode_base64_nopad(userid, strlen(userid));
1489 if (!b64userid) {
1490 buf_reset(dst);
1491 return NULL;
1492 }
1493 buf_appendcstr(dst, b64userid);
1494 free(b64userid);
1495 }
1496
1497 return buf_cstring(dst);
1498 }
1499
_decode_calendarevent_blobid(const char * blobid,char ** uidptr,modseq_t * modseqptr,char ** useridptr)1500 static int _decode_calendarevent_blobid(const char *blobid,
1501 char **uidptr,
1502 modseq_t *modseqptr,
1503 char **useridptr)
1504 {
1505 char *uid = NULL;
1506 modseq_t modseq = 0;
1507 char *userid = NULL;
1508 int is_valid = 0;
1509
1510 /* Decode iCalendar UID */
1511 const char *base = blobid+1;
1512 const char *p = strchr(base, '-');
1513 if (!p) goto done;
1514 uid = jmap_decode_base64_nopad(base, p-base);
1515 if (!uid) goto done;
1516 base = p + 1;
1517
1518 /* Decode modseq */
1519 if (*base == '\0') goto done;
1520 char *endptr = NULL;
1521 errno = 0;
1522 modseq = strtoull(base, &endptr, 10);
1523 if (errno == ERANGE || (*endptr && *endptr != '-')) {
1524 goto done;
1525 }
1526 base = endptr;
1527
1528 /* Decode userid */
1529 if (*base == '-') {
1530 base += 1;
1531 size_t len = strlen(base);
1532 if (len) {
1533 userid = jmap_decode_base64_nopad(base, len);
1534 if (!userid) goto done;
1535 }
1536 base += len;
1537 }
1538
1539 /* All done */
1540 *uidptr = uid;
1541 *modseqptr = modseq;
1542 *useridptr = userid;
1543 is_valid = 1;
1544
1545 done:
1546 if (!is_valid) {
1547 free(uid);
1548 free(userid);
1549 }
1550 return is_valid;
1551 }
1552
1553
1554 struct calendarevent_getblob_rock {
1555 jmap_req_t *req;
1556 struct buf *buf;
1557 };
1558
_calendarevent_getblob_cb(const char * mailbox,uint32_t uid,const char * entry,const char * userid,const struct buf * value,const struct annotate_metadata * mdata,void * vrock)1559 static int _calendarevent_getblob_cb(const char *mailbox __attribute__((unused)),
1560 uint32_t uid __attribute__((unused)),
1561 const char *entry __attribute__((unused)),
1562 const char *userid,
1563 const struct buf *value,
1564 const struct annotate_metadata *mdata __attribute__((unused)),
1565 void *vrock)
1566 {
1567 if (!buf_len(value)) return 0;
1568
1569 struct calendarevent_getblob_rock *rock = vrock;
1570 jmap_req_t *req = rock->req;
1571 struct buf *buf = rock->buf;
1572
1573 /* Parse the value and fetch the patch */
1574 struct dlist *dl;
1575 const char *vpatchstr = NULL;
1576 dlist_parsemap(&dl, 1, 0, buf_base(value), buf_len(value));
1577 dlist_getatom(dl, "VPATCH", &vpatchstr);
1578 if (vpatchstr) buf_setcstr(buf, vpatchstr);
1579 dlist_free(&dl);
1580 if (!buf_len(buf)) return 0;
1581
1582 /* Write VPATCH blob */
1583 char *part_headers = NULL;
1584 if (userid) part_headers = strconcat("X-UserId: ", userid, "\r\n", NULL);
1585 req->txn->resp_body.type = "text/calendar; component=VPATCH";
1586 req->txn->resp_body.len = buf_len(buf);
1587 write_multipart_body(0, req->txn, buf_base(buf), buf_len(buf), part_headers);
1588 free(part_headers);
1589
1590 return 0;
1591 }
1592
jmap_calendarevent_getblob(jmap_req_t * req,const char * blobid,const char * accept_mime)1593 static int jmap_calendarevent_getblob(jmap_req_t *req,
1594 const char *blobid,
1595 const char *accept_mime)
1596 {
1597 struct caldav_db *db = NULL;
1598 struct caldav_data *cdata = NULL;
1599 struct mailbox *mailbox = NULL;
1600 icalcomponent *ical = NULL;
1601 char *uid = NULL;
1602 char *userid = NULL;
1603 modseq_t modseq;
1604 struct buf buf = BUF_INITIALIZER;
1605 int res = 0;
1606 int r;
1607
1608 if (*blobid != 'I') return 0;
1609
1610 if (!_decode_calendarevent_blobid(blobid, &uid, &modseq, &userid)) {
1611 res = HTTP_BAD_REQUEST;
1612 goto done;
1613 }
1614
1615 /* Validate user id */
1616 if ((userid && strcmp(userid, req->userid)) || (!userid && (!httpd_userisadmin))) {
1617 res = HTTP_NOT_FOUND;
1618 goto done;
1619 }
1620
1621 /* Lookup uid in CaldavDB */
1622 db = caldav_open_userid(req->accountid);
1623 if (!db) {
1624 req->txn->error.desc = "no calendar db";
1625 res = HTTP_SERVER_ERROR;
1626 goto done;
1627 }
1628 if (caldav_lookup_uid(db, uid, &cdata)) {
1629 res = HTTP_NOT_FOUND;
1630 goto done;
1631 }
1632 if (!jmap_hasrights(req, cdata->dav.mailbox, JACL_READITEMS)) {
1633 res = HTTP_NOT_FOUND;
1634 goto done;
1635 }
1636
1637 /* Validate modseq */
1638 if (modseq != cdata->dav.modseq) {
1639 res = HTTP_NOT_FOUND;
1640 goto done;
1641 }
1642
1643 /* Open mailbox, we need it now */
1644 if ((r = jmap_openmbox(req, cdata->dav.mailbox, &mailbox, 0))) {
1645 req->txn->error.desc = error_message(r);
1646 res = HTTP_SERVER_ERROR;
1647 goto done;
1648 }
1649
1650 /* Make sure client can handle blob type. */
1651 if (accept_mime) {
1652 if (userid) {
1653 if (strcmp(accept_mime, "application/octet-stream") &&
1654 strcmp(accept_mime, "text/calendar")) {
1655 res = HTTP_NOT_ACCEPTABLE;
1656 goto done;
1657 }
1658 }
1659 else if (strcmp(accept_mime, "multipart/mixed")) {
1660 res = HTTP_NOT_ACCEPTABLE;
1661 goto done;
1662 }
1663 }
1664
1665 /* Load iCalendar data */
1666 if (userid) {
1667 /* Fetch ical resource with personalized data */
1668 ical = caldav_record_to_ical(mailbox, cdata, req->userid, NULL);
1669 }
1670 else {
1671 /* Fetch ical resource without personalized data */
1672 struct index_record record;
1673 if (!mailbox_find_index_record(mailbox, cdata->dav.imap_uid, &record)) {
1674 ical = record_to_ical(mailbox, &record, NULL);
1675 }
1676 }
1677 if (!ical) {
1678 req->txn->error.desc = "failed to load record";
1679 res = HTTP_SERVER_ERROR;
1680 goto done;
1681 }
1682
1683 /* Write blob to socket */
1684 if (userid) {
1685 /* Set Content headers */
1686 if (accept_mime) {
1687 if (strcmp(accept_mime, "application/octet-stream") &&
1688 strcmp(accept_mime, "text/calendar")) {
1689 res = HTTP_NOT_ACCEPTABLE;
1690 goto done;
1691 }
1692 }
1693 char *content_type = NULL;
1694 if (!accept_mime || !strcmp(accept_mime, "text/calendar")) {
1695 const char *comp_type = caldav_comp_type_as_string(cdata->comp_type);
1696 if (comp_type) {
1697 content_type = strconcat("text/calendar; component=", comp_type, NULL);
1698 req->txn->resp_body.type = content_type;
1699 }
1700 }
1701 if (!req->txn->resp_body.type) {
1702 req->txn->resp_body.type = accept_mime;
1703 }
1704
1705 /* Write body */
1706 buf_setcstr(&buf, icalcomponent_as_ical_string(ical));
1707 req->txn->resp_body.len = buf_len(&buf);
1708 write_body(HTTP_OK, req->txn, buf_base(&buf), buf_len(&buf));
1709 free(content_type);
1710 res = HTTP_OK;
1711 }
1712 else {
1713 /* Iniitialize multipart body */
1714 if (accept_mime && strcmp(accept_mime, "multipart/mixed")) {
1715 res = HTTP_NOT_ACCEPTABLE;
1716 goto done;
1717 }
1718 req->txn->resp_body.type = "multipart/mixed";
1719 write_multipart_body(HTTP_OK, req->txn, NULL, 0, NULL);
1720
1721 /* Set main component Content headers */
1722 char *content_type = NULL;
1723 const char *comp_type = caldav_comp_type_as_string(cdata->comp_type);
1724 if (comp_type) {
1725 content_type = strconcat("text/calendar; component=", comp_type, NULL);
1726 req->txn->resp_body.type = content_type;
1727 }
1728 else req->txn->resp_body.type = "text/calendar";
1729
1730 /* Write main component body */
1731 buf_setcstr(&buf, icalcomponent_as_ical_string(ical));
1732 req->txn->resp_body.len = buf_len(&buf);
1733 write_multipart_body(0, req->txn, buf_base(&buf), buf_len(&buf), NULL);
1734
1735 /* Write userdata parts */
1736 struct calendarevent_getblob_rock rock = { req, &buf };
1737 annotatemore_findall(cdata->dav.mailbox, cdata->dav.imap_uid,
1738 PER_USER_CAL_DATA, 0, _calendarevent_getblob_cb,
1739 &rock, 0);
1740
1741 write_multipart_body(0, req->txn, NULL, 0, NULL);
1742
1743 free(content_type);
1744 res = HTTP_OK;
1745 }
1746
1747 done:
1748 if (res != HTTP_OK && !req->txn->error.desc) {
1749 const char *desc = NULL;
1750 switch (res) {
1751 case HTTP_BAD_REQUEST:
1752 desc = "invalid calendar event blobid";
1753 break;
1754 case HTTP_NOT_FOUND:
1755 desc = "failed to find blob by calendar blobid";
1756 break;
1757 default:
1758 desc = error_message(res);
1759 }
1760 req->txn->error.desc = desc;
1761 }
1762 if (ical) icalcomponent_free(ical);
1763 if (mailbox) jmap_closembox(req, &mailbox);
1764 if (db) caldav_close(db);
1765 buf_free(&buf);
1766 free(userid);
1767 free(uid);
1768 return res;
1769 }
1770
1771 struct event_id {
1772 const char *raw; /* as requested by client */
1773 char *uid;
1774 char *recurid;
1775 };
1776
1777 /* Return NULL if id is neither a simple UID or structured id */
parse_eventid(const char * id)1778 static struct event_id *parse_eventid(const char *id)
1779 {
1780 struct event_id *eid = xzmalloc(sizeof(struct event_id));
1781 const char *p;
1782
1783 if ((p = strchr(id, ';')) == NULL) {
1784 eid->raw = id;
1785 eid->uid = xstrdup(id);
1786 return eid;
1787 }
1788 if (*p + 1 == '\0') {
1789 free(eid);
1790 return NULL;
1791 }
1792 eid->raw = id;
1793 eid->uid = xstrndup(id, p - id);
1794 eid->recurid = xstrdup(p + 1);
1795
1796 return eid;
1797 }
1798
free_eventid(struct event_id ** eidptr)1799 static void free_eventid(struct event_id **eidptr)
1800 {
1801 if (eidptr == NULL || *eidptr == NULL) return;
1802
1803 struct event_id *eid = *eidptr;
1804 free(eid->uid);
1805 free(eid->recurid);
1806 free(eid);
1807 *eidptr = NULL;
1808 }
1809
1810 struct getcalendarevents_rock {
1811 struct caldav_db *db;
1812 struct jmap_req *req;
1813 struct jmap_get *get;
1814 struct mailbox *mailbox;
1815 hashu64_table jmapcache;
1816 ptrarray_t *want_eventids;
1817 int check_acl;
1818 };
1819
1820 struct recurid_instanceof_rock {
1821 icaltimetype recurid;
1822 int found;
1823 };
1824
_recurid_instanceof_cb(icalcomponent * comp,icaltimetype start,icaltimetype end,void * vrock)1825 static int _recurid_instanceof_cb(icalcomponent *comp __attribute__((unused)),
1826 icaltimetype start,
1827 icaltimetype end __attribute__((unused)),
1828 void *vrock)
1829 {
1830 struct recurid_instanceof_rock *rock = vrock;
1831 struct icaltimetype recurid = rock->recurid;
1832
1833 if (start.is_date && !recurid.is_date) {
1834 start.is_date = 0;
1835 start.hour = 0;
1836 start.minute = 0;
1837 start.second = 0;
1838 }
1839 else if (!start.is_date && recurid.is_date) {
1840 recurid.is_date = 0;
1841 recurid.hour = 0;
1842 recurid.minute = 0;
1843 recurid.second = 0;
1844 }
1845
1846 int cmp = icaltime_compare(start, recurid);
1847 if (cmp == 0) {
1848 rock->found = 1;
1849 }
1850 return cmp < 0;
1851 }
1852
_recurid_is_instanceof(icaltimetype recurid,icalcomponent * ical,int rrule_only)1853 static int _recurid_is_instanceof(icaltimetype recurid, icalcomponent *ical, int rrule_only)
1854 {
1855 icaltimetype tstart = recurid;
1856 icaltime_adjust(&tstart, -1, 0, 0, 0);
1857 icaltimetype tend = recurid;
1858 icaltime_adjust(&tend, 1, 0, 0, 0);
1859 struct icalperiodtype timerange = {
1860 tstart, tend, icaldurationtype_null_duration()
1861 };
1862 struct recurid_instanceof_rock rock = { recurid, 0 };
1863 icalcomponent *mycomp = NULL;
1864
1865 if (rrule_only) {
1866 if (icalcomponent_isa(ical) == ICAL_VCALENDAR_COMPONENT) {
1867 /* Find the master component */
1868 icalcomponent *comp = icalcomponent_get_first_real_component(ical);
1869 icalcomponent_kind kind = icalcomponent_isa(comp);
1870 icalcomponent *mastercomp = NULL;
1871
1872 for (; comp; comp = icalcomponent_get_next_component(ical, kind)) {
1873 if (!icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY)) {
1874 mastercomp = comp;
1875 break;
1876 }
1877 }
1878 if (!mastercomp) {
1879 return 0;
1880 }
1881 ical = mastercomp;
1882 }
1883 if (icalcomponent_get_first_property(ical, ICAL_RDATE_PROPERTY)) {
1884 /* Remove RDATEs */
1885 mycomp = icalcomponent_clone(ical);
1886 icalproperty *next;
1887 icalproperty *prop = icalcomponent_get_first_property(mycomp, ICAL_RDATE_PROPERTY);
1888 for ( ; prop; prop = next) {
1889 next = icalcomponent_get_next_property(mycomp, ICAL_RDATE_PROPERTY);
1890 icalcomponent_remove_property(mycomp, prop);
1891 icalproperty_free(prop);
1892 }
1893 ical = mycomp;
1894 }
1895 }
1896
1897 icalcomponent_myforeach(ical, timerange, NULL, _recurid_instanceof_cb, &rock);
1898 int found = rock.found;
1899 if (mycomp) icalcomponent_free(mycomp);
1900 return found;
1901 }
1902
getcalendarevents_filterinstance(json_t * myevent,hash_table * props,const char * id,const char * ical_uid)1903 static void getcalendarevents_filterinstance(json_t *myevent,
1904 hash_table *props,
1905 const char *id,
1906 const char *ical_uid)
1907 {
1908 json_object_del(myevent, "recurrenceOverrides");
1909 json_object_del(myevent, "recurrenceRule");
1910 jmap_filterprops(myevent, props);
1911 json_object_set_new(myevent, "id", json_string(id));
1912 json_object_set_new(myevent, "uid", json_string(ical_uid));
1913 json_object_set_new(myevent, "@type", json_string("jsevent"));
1914 }
1915
getcalendarevents_getinstances(json_t * jsevent,struct caldav_data * cdata,icalcomponent * ical,struct getcalendarevents_rock * rock)1916 static int getcalendarevents_getinstances(json_t *jsevent,
1917 struct caldav_data *cdata,
1918 icalcomponent *ical,
1919 struct getcalendarevents_rock *rock)
1920 {
1921 jmap_req_t *req = rock->req;
1922 icalcomponent *myical = NULL;
1923 int r = 0;
1924
1925 int i;
1926 for (i = 0; i < ptrarray_size(rock->want_eventids); i++) {
1927 struct event_id *eid = ptrarray_nth(rock->want_eventids, i);
1928
1929 if (eid->recurid == NULL) {
1930 /* Client requested main event */
1931 json_t *myevent = json_deep_copy(jsevent);
1932 jmap_filterprops(myevent, rock->get->props);
1933 json_object_set_new(myevent, "id", json_string(cdata->ical_uid));
1934 json_object_set_new(myevent, "uid", json_string(cdata->ical_uid));
1935 json_object_set_new(myevent, "@type", json_string("jsevent"));
1936 json_array_append_new(rock->get->list, myevent);
1937 continue;
1938 }
1939
1940 /* Client requested event recurrence instance */
1941 json_t *override = json_object_get(
1942 json_object_get(jsevent, "recurrenceOverrides"), eid->recurid);
1943 if (override) {
1944 if (json_object_get(override, "excluded") != json_true()) {
1945 /* Instance is a recurrence override */
1946 json_t *myevent = jmap_patchobject_apply(jsevent, override, NULL);
1947 getcalendarevents_filterinstance(myevent, rock->get->props, eid->raw, cdata->ical_uid);
1948 if (json_object_get(override, "start") == NULL) {
1949 json_object_set_new(myevent, "start", json_string(eid->recurid));
1950 }
1951 json_object_set_new(myevent, "recurrenceId", json_string(eid->recurid));
1952 json_array_append_new(rock->get->list, myevent);
1953 }
1954 else {
1955 /* Instance is excluded */
1956 json_array_append_new(rock->get->not_found, json_string(eid->raw));
1957 }
1958 }
1959 else {
1960 /* Check if RRULE generates an instance at this timestamp */
1961 if (!ical) {
1962 /* Open calendar mailbox. */
1963 if (!rock->mailbox || strcmp(rock->mailbox->name, cdata->dav.mailbox)) {
1964 jmap_closembox(req, &rock->mailbox);
1965 r = jmap_openmbox(req, cdata->dav.mailbox, &rock->mailbox, 0);
1966 if (r) goto done;
1967 }
1968 myical = caldav_record_to_ical(rock->mailbox, cdata, httpd_userid, NULL);
1969 if (!myical) {
1970 syslog(LOG_ERR, "caldav_record_to_ical failed for record %u:%s",
1971 cdata->dav.imap_uid, rock->mailbox->name);
1972 json_array_append_new(rock->get->not_found, json_string(eid->raw));
1973 continue;
1974 }
1975 else ical = myical;
1976 }
1977 struct jmapical_datetime timestamp = JMAPICAL_DATETIME_INITIALIZER;
1978 if (jmapical_localdatetime_from_string(eid->recurid, ×tamp) < 0) {
1979 json_array_append_new(rock->get->not_found, json_string(eid->raw));
1980 continue;
1981 }
1982 icaltimetype icalrecurid = jmapical_datetime_to_icaltime(×tamp, NULL);
1983 if (!_recurid_is_instanceof(icalrecurid, ical, 1/*rrule_only*/)) {
1984 json_array_append_new(rock->get->not_found, json_string(eid->raw));
1985 continue;
1986 }
1987
1988 /* Build instance */
1989 struct buf buf = BUF_INITIALIZER;
1990 jmapical_localdatetime_as_string(×tamp, &buf);
1991 json_t *jstart = json_string(buf_cstring(&buf));
1992 buf_free(&buf);
1993
1994 json_t *myevent = json_deep_copy(jsevent);
1995 getcalendarevents_filterinstance(myevent, rock->get->props, eid->raw, cdata->ical_uid);
1996 json_object_set_new(myevent, "start", jstart);
1997 json_object_set_new(myevent, "recurrenceId", json_string(eid->recurid));
1998 json_array_append_new(rock->get->list, myevent);
1999 }
2000 }
2001
2002 done:
2003 if (myical) icalcomponent_free(myical);
2004 return r;
2005 }
2006
find_participant_by_addr(json_t * participants,const char * addr)2007 static const char *find_participant_by_addr(json_t *participants, const char *addr)
2008 {
2009 if (!strncasecmp(addr, "mailto:", 7)) addr += 7;
2010
2011 const char *id;
2012 json_t *jpart;
2013 json_object_foreach(participants, id, jpart) {
2014 json_t *jsendTo = json_object_get(jpart, "sendTo");
2015 const char *val = json_string_value(json_object_get(jsendTo, "imip"));
2016 if (val && !strncasecmp(val, "mailto:", 7)) {
2017 val += 7;
2018 }
2019 else if (!val) {
2020 // email shouldn't be used for scheduling, but it allows
2021 // to identify the Participant by their email address
2022 val = json_string_value(json_object_get(jpart, "email"));
2023 }
2024 if (!strcasecmpsafe(val, addr)) {
2025 return id;
2026 }
2027 }
2028
2029 return NULL;
2030 }
2031
getcalendarevents_cb(void * vrock,struct caldav_data * cdata)2032 static int getcalendarevents_cb(void *vrock, struct caldav_data *cdata)
2033 {
2034 struct getcalendarevents_rock *rock = vrock;
2035 int r = 0;
2036 icalcomponent* ical = NULL;
2037 json_t *jsevent = NULL;
2038 jmap_req_t *req = rock->req;
2039 strarray_t schedule_addresses = STRARRAY_INITIALIZER;
2040
2041 if (!cdata->dav.alive)
2042 return 0;
2043
2044 /* Check component type and ACL */
2045 if (cdata->comp_type != CAL_COMP_VEVENT ||
2046 !jmap_hasrights(req, cdata->dav.mailbox, JACL_READITEMS)) {
2047 return 0;
2048 }
2049
2050 if (cdata->jmapversion == JMAPCACHE_CALVERSION) {
2051 json_error_t jerr;
2052 jsevent = json_loads(cdata->jmapdata, 0, &jerr);
2053 if (jsevent) goto gotevent;
2054 }
2055
2056 /* Open calendar mailbox. */
2057 if (!rock->mailbox || strcmp(rock->mailbox->name, cdata->dav.mailbox)) {
2058 jmap_closembox(req, &rock->mailbox);
2059 r = jmap_openmbox(req, cdata->dav.mailbox, &rock->mailbox, 0);
2060 if (r) goto done;
2061 }
2062
2063 /* Load message containing the resource and parse iCal data */
2064 ical = caldav_record_to_ical(rock->mailbox, cdata, httpd_userid, &schedule_addresses);
2065 if (!ical) {
2066 syslog(LOG_ERR, "caldav_record_to_ical failed for record %u:%s",
2067 cdata->dav.imap_uid, rock->mailbox->name);
2068 r = IMAP_INTERNAL;
2069 goto done;
2070 }
2071
2072 /* Convert to JMAP */
2073 jsevent = jmapical_tojmap(ical, NULL);
2074 if (!jsevent) {
2075 syslog(LOG_ERR, "jmapical_tojson: can't convert %u:%s",
2076 cdata->dav.imap_uid, rock->mailbox->name);
2077 r = IMAP_INTERNAL;
2078 goto done;
2079 }
2080 icalcomponent_free(ical);
2081 ical = NULL;
2082
2083 /* Add participant id */
2084 json_t *jparticipants = json_object_get(jsevent, "participants");
2085 const char *participant_id = NULL;
2086 int i;
2087 for (i = 0; i < strarray_size(&schedule_addresses); i++) {
2088 participant_id = find_participant_by_addr(jparticipants,
2089 strarray_nth(&schedule_addresses, i));
2090 if (participant_id) break;
2091 }
2092 json_object_set_new(jsevent, "participantId", participant_id ?
2093 json_string(participant_id) : json_null());
2094
2095 /* Add to cache */
2096 hashu64_insert(cdata->dav.rowid, json_dumps(jsevent, 0), &rock->jmapcache);
2097
2098 gotevent:
2099
2100 /* Add JMAP-only fields. */
2101 if (jmap_wantprop(rock->get->props, "x-href")) {
2102 char *xhref = jmap_xhref(cdata->dav.mailbox, cdata->dav.resource);
2103 json_object_set_new(jsevent, "x-href", json_string(xhref));
2104 free(xhref);
2105 }
2106 if (jmap_wantprop(rock->get->props, "calendarId")) {
2107 json_object_set_new(jsevent, "calendarId",
2108 json_string(strrchr(cdata->dav.mailbox, '.')+1));
2109 }
2110 if (jmap_wantprop(rock->get->props, "blobId")) {
2111 json_t *jblobid = json_null();
2112 struct buf blobid = BUF_INITIALIZER;
2113 if (_encode_calendarevent_blobid(cdata, req->userid, &blobid)) {
2114 jblobid = json_string(buf_cstring(&blobid));
2115 }
2116 buf_free(&blobid);
2117 json_object_set_new(jsevent, "blobId", jblobid);
2118 }
2119 if (jmap_wantprop(rock->get->props, "debugBlobId")) {
2120 json_t *jblobid = json_null();
2121 if (httpd_userisadmin) {
2122 struct buf blobid = BUF_INITIALIZER;
2123 if (_encode_calendarevent_blobid(cdata, NULL, &blobid)) {
2124 jblobid = json_string(buf_cstring(&blobid));
2125 }
2126 buf_free(&blobid);
2127 }
2128 json_object_set_new(jsevent, "debugBlobId", jblobid);
2129 }
2130
2131 if (rock->want_eventids == NULL) {
2132 /* Client requested all events */
2133 jmap_filterprops(jsevent, rock->get->props);
2134 json_object_set_new(jsevent, "id", json_string(cdata->ical_uid));
2135 json_object_set_new(jsevent, "uid", json_string(cdata->ical_uid));
2136 json_object_set_new(jsevent, "@type", json_string("jsevent"));
2137 json_array_append_new(rock->get->list, jsevent);
2138 }
2139 else {
2140 /* Client requested specific event ids */
2141 r = getcalendarevents_getinstances(jsevent, cdata, ical, rock);
2142 json_decref(jsevent);
2143 if (r) goto done;
2144 }
2145
2146 done:
2147 strarray_fini(&schedule_addresses);
2148 if (ical) icalcomponent_free(ical);
2149 return r;
2150 }
2151
2152 static const jmap_property_t event_props[] = {
2153 {
2154 "id",
2155 NULL,
2156 JMAP_PROP_IMMUTABLE | JMAP_PROP_ALWAYS_GET
2157 },
2158 {
2159 "calendarId",
2160 NULL,
2161 0
2162 },
2163 {
2164 "participantId",
2165 NULL,
2166 0
2167 },
2168
2169 /* JSCalendar common properties */
2170 {
2171 "@type",
2172 NULL,
2173 0
2174 },
2175 {
2176 "uid",
2177 NULL,
2178 0
2179 },
2180 {
2181 "relatedTo",
2182 NULL,
2183 0
2184 },
2185 {
2186 "prodId",
2187 NULL,
2188 0
2189 },
2190 {
2191 "created",
2192 NULL,
2193 0
2194 },
2195 {
2196 "updated",
2197 NULL,
2198 0
2199 },
2200 {
2201 "sequence",
2202 NULL,
2203 0
2204 },
2205 {
2206 "method",
2207 NULL,
2208 0
2209 },
2210 {
2211 "title",
2212 NULL,
2213 0
2214 },
2215 {
2216 "description",
2217 NULL,
2218 0
2219 },
2220 {
2221 "descriptionContentType",
2222 NULL,
2223 0
2224 },
2225 {
2226 "locations",
2227 NULL,
2228 0
2229 },
2230 {
2231 "virtualLocations",
2232 NULL,
2233 0
2234 },
2235 {
2236 "links",
2237 NULL,
2238 0
2239 },
2240 {
2241 "locale",
2242 NULL,
2243 0
2244 },
2245 {
2246 "keywords",
2247 NULL,
2248 0
2249 },
2250 {
2251 "categories",
2252 NULL,
2253 0
2254 },
2255 {
2256 "color",
2257 NULL,
2258 0
2259 },
2260 {
2261 "recurrenceId",
2262 NULL,
2263 0
2264 },
2265 {
2266 "recurrenceRule",
2267 NULL,
2268 0
2269 },
2270 {
2271 "recurrenceOverrides",
2272 NULL,
2273 0
2274 },
2275 {
2276 "excluded",
2277 NULL,
2278 0
2279 },
2280 {
2281 "priority",
2282 NULL,
2283 0
2284 },
2285 {
2286 "freeBusyStatus",
2287 NULL,
2288 0
2289 },
2290 {
2291 "privacy",
2292 NULL,
2293 0
2294 },
2295 {
2296 "replyTo",
2297 NULL,
2298 0
2299 },
2300 {
2301 "participants",
2302 NULL,
2303 0
2304 },
2305 {
2306 "useDefaultAlerts",
2307 NULL,
2308 0
2309 },
2310 {
2311 "alerts",
2312 NULL,
2313 0
2314 },
2315 {
2316 "localizations",
2317 NULL,
2318 0
2319 },
2320
2321 /* JSEvent properties */
2322 {
2323 "start",
2324 NULL,
2325 0
2326 },
2327 {
2328 "timeZone",
2329 NULL,
2330 0
2331 },
2332 {
2333 "duration",
2334 NULL,
2335 0
2336 },
2337 {
2338 "showWithoutTime",
2339 NULL,
2340 0
2341 },
2342 {
2343 "status",
2344 NULL,
2345 0
2346 },
2347
2348 /* FM specific */
2349 {
2350 "x-href",
2351 JMAP_CALENDARS_EXTENSION,
2352 0
2353 },
2354 {
2355 "blobId",
2356 JMAP_CALENDARS_EXTENSION,
2357 JMAP_PROP_SERVER_SET | JMAP_PROP_SKIP_GET
2358 },
2359 {
2360 "debugBlobId",
2361 JMAP_DEBUG_EXTENSION,
2362 JMAP_PROP_SERVER_SET | JMAP_PROP_SKIP_GET
2363 },
2364 { NULL, NULL, 0 }
2365 };
2366
cachecalendarevents_cb(uint64_t rowid,void * payload,void * vrock)2367 static void cachecalendarevents_cb(uint64_t rowid, void *payload, void *vrock)
2368 {
2369 const char *eventrep = payload;
2370 struct getcalendarevents_rock *rock = vrock;
2371
2372 // there's no way to return errors, but luckily it doesn't matter if we
2373 // fail to cache
2374 caldav_write_jmapcache(rock->db, rowid, httpd_userid,
2375 JMAPCACHE_CALVERSION, eventrep);
2376 }
2377
jmap_calendarevent_get(struct jmap_req * req)2378 static int jmap_calendarevent_get(struct jmap_req *req)
2379 {
2380 struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
2381 struct jmap_get get;
2382 struct caldav_db *db = NULL;
2383 json_t *err = NULL;
2384 int r = 0;
2385
2386 if (!has_calendars(req)) {
2387 jmap_error(req, json_pack("{s:s}", "type", "accountNoCalendars"));
2388 return 0;
2389 }
2390
2391 /* Build callback data */
2392 int checkacl = strcmp(req->accountid, req->userid);
2393 struct getcalendarevents_rock rock = { NULL /* db */,
2394 req, &get,
2395 NULL /* mbox */,
2396 HASHU64_TABLE_INITIALIZER, /* cache */
2397 NULL, /* want_eventids */
2398 checkacl };
2399
2400 construct_hashu64_table(&rock.jmapcache, 512, 0);
2401
2402 /* Parse request */
2403 jmap_get_parse(req, &parser, event_props, /*allow_null_ids*/1,
2404 NULL, NULL, &get, &err);
2405 if (err) {
2406 jmap_error(req, err);
2407 goto done;
2408 }
2409
2410 rock.db = db = caldav_open_userid(req->accountid);
2411 if (!db) {
2412 syslog(LOG_ERR,
2413 "caldav_open_mailbox failed for user %s", req->accountid);
2414 r = IMAP_INTERNAL;
2415 goto done;
2416 }
2417
2418 /* Does the client request specific events? */
2419 if (json_array_size(get.ids)) {
2420 size_t i;
2421 json_t *jval;
2422 hash_table eventids_by_uid = HASH_TABLE_INITIALIZER;
2423 construct_hash_table(&eventids_by_uid, json_array_size(get.ids), 0);
2424
2425 /* Split into single-valued uids and event recurrence instance ids */
2426 json_array_foreach(get.ids, i, jval) {
2427 const char *id = json_string_value(jval);
2428 struct event_id *eid = parse_eventid(id);
2429 if (eid) {
2430 ptrarray_t *eventids = hash_lookup(eid->uid, &eventids_by_uid);
2431 if (!eventids) {
2432 eventids = ptrarray_new();
2433 hash_insert(eid->uid, eventids, &eventids_by_uid);
2434 }
2435 ptrarray_append(eventids, eid);
2436 }
2437 else json_array_append(get.not_found, jval);
2438 }
2439
2440 /* Lookup events by UID */
2441 hash_iter *iter = hash_table_iter(&eventids_by_uid);
2442 while (hash_iter_next(iter)) {
2443 const char *uid = hash_iter_key(iter);
2444 size_t nseen = json_array_size(get.list) + json_array_size(get.not_found);
2445 rock.want_eventids = hash_iter_val(iter);
2446 r = caldav_get_events(db, httpd_userid, NULL, uid, &getcalendarevents_cb, &rock);
2447 if (r) break;
2448 if (nseen == json_array_size(get.list) + json_array_size(get.not_found)) {
2449 /* caldavdb silently ignores non-existent uids */
2450 int j;
2451 for (j = 0; j < ptrarray_size(rock.want_eventids); j++) {
2452 struct event_id *eid = ptrarray_nth(rock.want_eventids, j);
2453 json_array_append_new(rock.get->not_found, json_string(eid->raw));
2454 }
2455 }
2456 }
2457 hash_iter_free(&iter);
2458
2459 /* Clean up memory */
2460 iter = hash_table_iter(&eventids_by_uid);
2461 while (hash_iter_next(iter)) {
2462 ptrarray_t *eventids = hash_iter_val(iter);
2463 struct event_id *eid;
2464 while ((eid = ptrarray_pop(eventids))) {
2465 free_eventid(&eid);
2466 }
2467 ptrarray_free(eventids);
2468 }
2469 hash_iter_free(&iter);
2470 free_hash_table(&eventids_by_uid, NULL);
2471 } else if (json_is_null(get.ids) || get.ids == NULL) {
2472 /* Return all visible events */
2473 r = caldav_get_events(db, httpd_userid, NULL, NULL, &getcalendarevents_cb, &rock);
2474 }
2475 if (r) goto done;
2476
2477 if (hashu64_count(&rock.jmapcache)) {
2478 r = caldav_begin(db);
2479 if (!r) hashu64_enumerate(&rock.jmapcache, cachecalendarevents_cb, &rock);
2480 if (r) caldav_abort(db);
2481 else r = caldav_commit(db);
2482 if (r) goto done;
2483 }
2484
2485 /* Build response */
2486 json_t *jstate = jmap_getstate(req, MBTYPE_CALENDAR, /*refresh*/0);
2487 get.state = xstrdup(json_string_value(jstate));
2488 json_decref(jstate);
2489 jmap_ok(req, jmap_get_reply(&get));
2490
2491 done:
2492 jmap_parser_fini(&parser);
2493 jmap_get_fini(&get);
2494 if (db) caldav_close(db);
2495 if (rock.mailbox) jmap_closembox(req, &rock.mailbox);
2496 free_hashu64_table(&rock.jmapcache, free);
2497 return r;
2498 }
2499
setcalendarevents_schedule(jmap_req_t * req,const char * mboxname,strarray_t * schedule_addresses,icalcomponent * oldical,icalcomponent * ical,int mode)2500 static int setcalendarevents_schedule(jmap_req_t *req,
2501 const char *mboxname,
2502 strarray_t *schedule_addresses,
2503 icalcomponent *oldical,
2504 icalcomponent *ical,
2505 int mode)
2506 {
2507 /* Determine if any scheduling is required. */
2508 icalcomponent *src = mode & JMAP_DESTROY ? oldical : ical;
2509 icalcomponent *comp =
2510 icalcomponent_get_first_component(src, ICAL_VEVENT_COMPONENT);
2511 icalproperty *prop =
2512 icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
2513 if (!prop) return 0;
2514 const char *organizer = icalproperty_get_organizer(prop);
2515 if (!organizer) return 0;
2516 if (!strncasecmp(organizer, "mailto:", 7)) organizer += 7;
2517
2518 get_schedule_addresses(req->txn->req_hdrs, mboxname,
2519 req->userid, schedule_addresses);
2520
2521 /* Validate create/update. */
2522 if (oldical && (mode & (JMAP_CREATE|JMAP_UPDATE))) {
2523 /* Don't allow ORGANIZER to be updated */
2524 const char *oldorganizer = NULL;
2525
2526 icalcomponent *oldcomp = NULL;
2527 icalproperty *prop = NULL;
2528 oldcomp =
2529 icalcomponent_get_first_component(oldical, ICAL_VEVENT_COMPONENT);
2530 if (oldcomp) {
2531 prop = icalcomponent_get_first_property(oldcomp,
2532 ICAL_ORGANIZER_PROPERTY);
2533 }
2534 if (prop) oldorganizer = icalproperty_get_organizer(prop);
2535 if (oldorganizer) {
2536 if (!strncasecmp(oldorganizer, "mailto:", 7)) oldorganizer += 7;
2537 if (strcasecmp(oldorganizer, organizer)) {
2538 /* XXX This should become a set error. */
2539 return 0;
2540 }
2541 }
2542 }
2543
2544 if (organizer &&
2545 /* XXX Hack for Outlook */ icalcomponent_get_first_invitee(comp)) {
2546 /* Send scheduling message. */
2547 if (strarray_find_case(schedule_addresses, organizer, 0) >= 0) {
2548 /* Organizer scheduling object resource */
2549 sched_request(req->userid, schedule_addresses, organizer, oldical, ical);
2550 } else {
2551 /* Attendee scheduling object resource */
2552 int omit_reply = 0;
2553 if (oldical && (mode & JMAP_DESTROY)) {
2554 for (prop = icalcomponent_get_first_property(comp, ICAL_ATTENDEE_PROPERTY);
2555 prop;
2556 prop = icalcomponent_get_next_property(comp, ICAL_ATTENDEE_PROPERTY)) {
2557 const char *addr = icalproperty_get_attendee(prop);
2558 if (!addr || strncasecmp(addr, "mailto:", 7) || strcasecmp(strarray_nth(schedule_addresses, 0), addr+7))
2559 continue;
2560 icalparameter *param = icalproperty_get_first_parameter(prop, ICAL_PARTSTAT_PARAMETER);
2561 omit_reply = !param || icalparameter_get_partstat(param) == ICAL_PARTSTAT_NEEDSACTION;
2562 break;
2563 }
2564 }
2565 if (!omit_reply && strarray_size(schedule_addresses))
2566 sched_reply(req->userid, schedule_addresses, oldical, ical);
2567 }
2568 }
2569
2570 return 0;
2571 }
2572
remove_itip_properties(icalcomponent * ical)2573 static void remove_itip_properties(icalcomponent *ical)
2574 {
2575 icalproperty *prop, *next;
2576 icalproperty_kind kind = ICAL_METHOD_PROPERTY;
2577
2578 for (prop = icalcomponent_get_first_property(ical, kind);
2579 prop;
2580 prop = next) {
2581
2582 next = icalcomponent_get_next_property(ical, kind);
2583 icalcomponent_remove_property(ical, prop);
2584 icalproperty_free(prop);
2585 }
2586
2587 }
2588
validate_participant_id(json_t * event,strarray_t * schedule_addresses,json_t * invalid)2589 static const char *validate_participant_id(json_t *event,
2590 strarray_t *schedule_addresses,
2591 json_t *invalid)
2592 {
2593 json_t *jparticipantId = json_object_get(event, "participantId");
2594 if (json_is_string(jparticipantId)) {
2595 const char *participant_id = json_string_value(jparticipantId);
2596 json_t *jparticipants = json_object_get(event, "participants");
2597 json_t *jpart = json_object_get(jparticipants, participant_id);
2598
2599 json_t *jsendTo = json_object_get(jpart, "sendTo");
2600 const char *val = json_string_value(json_object_get(jsendTo, "imip"));
2601 if (val && !strncasecmp(val, "mailto:", 7)) {
2602 val += 7;
2603 }
2604 else if (!val) {
2605 // email shouldn't be used for scheduling, but it allows
2606 // to identify the Participant by their email address
2607 val = json_string_value(json_object_get(jpart, "email"));
2608 }
2609 if (val && *val) {
2610 /* Found the participant. Reset schedule address. */
2611 strarray_addfirst_case(schedule_addresses, val);
2612 return participant_id;
2613 }
2614 else {
2615 json_array_append_new(invalid, json_string("participantId"));
2616 }
2617 }
2618 else if (JNOTNULL(jparticipantId)) {
2619 json_array_append_new(invalid, json_string("participantId"));
2620 }
2621 return NULL;
2622 }
2623
2624
setcalendarevents_create(jmap_req_t * req,const char * account_id,json_t * event,struct caldav_db * db,json_t * invalid,json_t * create)2625 static int setcalendarevents_create(jmap_req_t *req,
2626 const char *account_id,
2627 json_t *event,
2628 struct caldav_db *db,
2629 json_t *invalid,
2630 json_t *create)
2631 {
2632 int r, pe;
2633 int needrights = JACL_ADDITEMS|JACL_SETMETADATA;
2634 char *uid = NULL;
2635
2636 struct mailbox *mbox = NULL;
2637 char *mboxname = NULL;
2638 char *resource = NULL;
2639
2640 icalcomponent *ical = NULL;
2641 const char *calendarId = NULL;
2642 strarray_t schedule_addresses = STRARRAY_INITIALIZER;
2643
2644 if ((uid = (char *) json_string_value(json_object_get(event, "uid")))) {
2645 /* Use custom iCalendar UID from request object */
2646 uid = xstrdup(uid);
2647 } else {
2648 /* Create a iCalendar UID */
2649 uid = xstrdup(makeuuid());
2650 }
2651
2652 /* Validate calendarId */
2653 pe = jmap_readprop(event, "calendarId", 1, invalid, "s", &calendarId);
2654 if (pe > 0 && *calendarId &&*calendarId == '#') {
2655 calendarId = jmap_lookup_id(req, calendarId + 1);
2656 if (!calendarId) {
2657 json_array_append_new(invalid, json_string("calendarId"));
2658 }
2659 }
2660 if (json_array_size(invalid)) {
2661 free(uid);
2662 return 0;
2663 }
2664
2665 /* Determine mailbox and resource name of calendar event.
2666 * We attempt to reuse the UID as DAV resource name; but
2667 * only if it looks like a reasonable URL path segment. */
2668 struct buf buf = BUF_INITIALIZER;
2669 mboxname = caldav_mboxname(account_id, calendarId);
2670 const char *p;
2671 for (p = uid; *p; p++) {
2672 if ((*p >= '0' && *p <= '9') ||
2673 (*p >= 'a' && *p <= 'z') ||
2674 (*p >= 'A' && *p <= 'Z') ||
2675 (*p == '@' || *p == '.') ||
2676 (*p == '_' || *p == '-')) {
2677 continue;
2678 }
2679 break;
2680 }
2681 if (*p == '\0' && p - uid >= 16 && p - uid <= 200) {
2682 buf_setcstr(&buf, uid);
2683 } else {
2684 buf_setcstr(&buf, makeuuid());
2685 }
2686 buf_appendcstr(&buf, ".ics");
2687 resource = buf_newcstring(&buf);
2688 buf_free(&buf);
2689
2690 /* Check permissions. */
2691 if (!jmap_hasrights(req, mboxname, needrights)) {
2692 json_array_append_new(invalid, json_string("calendarId"));
2693 r = 0; goto done;
2694 }
2695
2696 /* Open mailbox for writing */
2697 r = jmap_openmbox(req, mboxname, &mbox, 1);
2698 if (r) {
2699 syslog(LOG_ERR, "jmap_openmbox(req, %s) failed: %s", mboxname, error_message(r));
2700 if (r == IMAP_MAILBOX_NONEXISTENT) {
2701 json_array_append_new(invalid, json_string("calendarId"));
2702 r = 0;
2703 }
2704 goto done;
2705 }
2706
2707 /* Convert the JMAP calendar event to ical. */
2708 if (!json_object_get(event, "uid")) {
2709 json_object_set_new(event, "uid", json_string(uid));
2710 }
2711
2712 if (!json_object_get(event, "created") || !json_object_get(event, "updated")) {
2713 char datestr[RFC3339_DATETIME_MAX+1];
2714 time_to_rfc3339(time(NULL), datestr, RFC3339_DATETIME_MAX);
2715 datestr[RFC3339_DATETIME_MAX] = '\0';
2716 if (!json_object_get(event, "created")) {
2717 json_object_set_new(event, "created", json_string(datestr));
2718 }
2719 if (!json_object_get(event, "updated")) {
2720 json_object_set_new(event, "updated", json_string(datestr));
2721 }
2722 }
2723 ical = jmapical_toical(event, NULL, invalid);
2724
2725 // check that participantId is either not present or is a valid participant
2726 validate_participant_id(event, &schedule_addresses, invalid);
2727
2728 if (json_array_size(invalid)) {
2729 r = 0;
2730 goto done;
2731 } else if (!ical) {
2732 r = IMAP_INTERNAL;
2733 goto done;
2734 }
2735
2736 /* Handle scheduling. */
2737 r = setcalendarevents_schedule(req, mboxname, &schedule_addresses,
2738 NULL, ical, JMAP_CREATE);
2739 if (r) goto done;
2740
2741 /* Remove METHOD property */
2742 remove_itip_properties(ical);
2743
2744 /* Store the VEVENT. */
2745 struct transaction_t txn;
2746 memset(&txn, 0, sizeof(struct transaction_t));
2747 txn.req_hdrs = spool_new_hdrcache();
2748 /* XXX - fix userid */
2749
2750 /* Locate the mailbox */
2751 r = proxy_mlookup(mbox->name, &txn.req_tgt.mbentry, NULL, NULL);
2752 if (r) {
2753 syslog(LOG_ERR, "mlookup(%s) failed: %s", mbox->name, error_message(r));
2754 }
2755 else {
2756 r = caldav_store_resource(&txn, ical, mbox, resource, 0,
2757 db, 0, httpd_userid, &schedule_addresses);
2758 }
2759 mboxlist_entry_free(&txn.req_tgt.mbentry);
2760 spool_free_hdrcache(txn.req_hdrs);
2761 buf_free(&txn.buf);
2762 if (r && r != HTTP_CREATED && r != HTTP_NO_CONTENT) {
2763 syslog(LOG_ERR, "caldav_store_resource failed for user %s: %s",
2764 req->accountid, error_message(r));
2765 goto done;
2766 }
2767 r = 0;
2768 json_object_set_new(create, "uid", json_string(uid));
2769
2770 char *xhref = jmap_xhref(mbox->name, resource);
2771 json_object_set_new(create, "x-href", json_string(xhref));
2772 free(xhref);
2773
2774 if (jmap_is_using(req, JMAP_CALENDARS_EXTENSION)) {
2775 struct caldav_data *cdata = NULL;
2776 r = caldav_lookup_uid(db, uid, &cdata);
2777 if (!r) {
2778 struct buf blobid = BUF_INITIALIZER;
2779 if (_encode_calendarevent_blobid(cdata, req->userid, &blobid)) {
2780 json_object_set_new(create, "blobId",
2781 json_string(buf_cstring(&blobid)));
2782 }
2783 buf_reset(&blobid);
2784 if (_encode_calendarevent_blobid(cdata, NULL, &blobid)) {
2785 json_object_set_new(create, "debugBlobId",
2786 json_string(buf_cstring(&blobid)));
2787 }
2788 buf_free(&blobid);
2789 }
2790 }
2791
2792 done:
2793 if (mbox) jmap_closembox(req, &mbox);
2794 if (ical) icalcomponent_free(ical);
2795 strarray_fini(&schedule_addresses);
2796 free(resource);
2797 free(mboxname);
2798 free(uid);
2799 return r;
2800 }
2801
eventpatch_updates_peruserprops_only(json_t * jdiff,const char * participant_id)2802 static int eventpatch_updates_peruserprops_only(json_t *jdiff, const char *participant_id)
2803 {
2804 strarray_t userprops = STRARRAY_INITIALIZER;
2805 if (participant_id) {
2806 strarray_appendm(&userprops,
2807 strconcat("participants/", participant_id, "/participationStatus", NULL));
2808 strarray_appendm(&userprops,
2809 strconcat("participants/", participant_id, "/participationComment", NULL));
2810 strarray_appendm(&userprops,
2811 strconcat("participants/", participant_id, "/expectReply", NULL));
2812 }
2813
2814 const char *prop;
2815 json_t *jval;
2816 int ret = 1;
2817 void *tmp;
2818
2819 json_object_foreach_safe(jdiff, tmp, prop, jval) {
2820 if (!strcmp(prop, "recurrenceOverrides")) {
2821 const char *recurid;
2822 json_t *joverride;
2823 json_object_foreach(jval, recurid, joverride) {
2824 if (!eventpatch_updates_peruserprops_only(joverride, participant_id)) {
2825 ret = 0;
2826 goto done;
2827 }
2828 }
2829 continue;
2830 }
2831 else if (!strcmp(prop, "participants")) {
2832 /* Patches *all* participants */
2833 ret = 0;
2834 goto done;
2835 }
2836 else if (!strncmp(prop, "recurrenceOverrides/", 20)) {
2837 /* Does prop point *into* an override? */
2838 const char *p = strchr(prop + 21, '/');
2839 if (!p) {
2840 /* Override value is a JSON object */
2841 if (!eventpatch_updates_peruserprops_only(jval, participant_id)) {
2842 ret = 0;
2843 goto done;
2844 }
2845 continue;
2846 }
2847 /* fall through */
2848 prop = p + 1;
2849 }
2850
2851 if (strcmp(prop, "keywords") &&
2852 strcmp(prop, "color") &&
2853 strcmp(prop, "freeBusyStatus") &&
2854 strcmp(prop, "useDefaultAlerts") &&
2855 strcmp(prop, "alerts") &&
2856 strncmp(prop, "alerts/", 7) &&
2857 (!participant_id || strarray_find(&userprops, prop, 0) < 0)) {
2858 /* Patches some non-user property */
2859 ret = 0;
2860 goto done;
2861 }
2862 }
2863
2864 done:
2865 strarray_fini(&userprops);
2866 return ret;
2867 }
2868
eventpatch_updates_recurrenceoverrides(json_t * event_patch)2869 static int eventpatch_updates_recurrenceoverrides(json_t *event_patch)
2870 {
2871 const char *prop;
2872 json_t *jval;
2873 json_object_foreach(event_patch, prop, jval) {
2874 if (!strncmp(prop, "recurrenceOverrides/", 20) && strchr(prop + 21, '/')) {
2875 return 1;
2876 }
2877 }
2878 return 0;
2879 }
2880
setcalendarevents_apply_patch(json_t * event_patch,icalcomponent * oldical,const char * recurid,json_t * invalid,strarray_t * schedule_addresses,icalcomponent ** newical,json_t * update,json_t ** err)2881 static int setcalendarevents_apply_patch(json_t *event_patch,
2882 icalcomponent *oldical,
2883 const char *recurid,
2884 json_t *invalid,
2885 strarray_t *schedule_addresses,
2886 icalcomponent **newical,
2887 json_t *update,
2888 json_t **err)
2889 {
2890 json_t *old_event = NULL;
2891 json_t *new_event = NULL;
2892 int r = 0;
2893
2894 old_event = jmapical_tojmap(oldical, NULL);
2895 if (!old_event) {
2896 r = IMAP_INTERNAL;
2897 goto done;
2898 }
2899 json_object_del(old_event, "updated");
2900
2901 /* Add participant id to old_event */
2902 json_t *jparticipants = json_object_get(old_event, "participants");
2903 const char *participant_id = NULL;
2904 int i;
2905 for (i = 0; i < strarray_size(schedule_addresses); i++) {
2906 participant_id = find_participant_by_addr(jparticipants,
2907 strarray_nth(schedule_addresses, i));
2908 if (participant_id) break;
2909 }
2910 json_object_set_new(old_event, "participantId", participant_id ?
2911 json_string(participant_id) : json_null());
2912
2913 if (recurid) {
2914 /* Update or create an override */
2915 struct jmapical_datetime recuriddt = JMAPICAL_DATETIME_INITIALIZER;
2916 if (jmapical_localdatetime_from_string(recurid, &recuriddt) < 0) {
2917 r = IMAP_NOTFOUND;
2918 goto done;
2919 }
2920 icaltimetype icalrecurid = jmapical_datetime_to_icaltime(&recuriddt, NULL);
2921
2922 int is_rdate = !_recurid_is_instanceof(icalrecurid, oldical, 1/*rrule_only*/);
2923
2924 json_t *old_overrides = json_object_get(old_event, "recurrenceOverrides");
2925 json_t *old_override = json_object_get(old_overrides, recurid);
2926 json_t *new_instance = NULL;
2927 json_t *new_override = NULL;
2928 if (old_override) {
2929 /* Patch an existing override */
2930 json_t *old_instance = jmap_patchobject_apply(old_event, old_override, NULL);
2931 new_instance = jmap_patchobject_apply(old_instance, event_patch, invalid);
2932 json_decref(old_instance);
2933 }
2934 else {
2935 /* Create a new override */
2936 new_instance = jmap_patchobject_apply(old_event, event_patch, invalid);
2937 }
2938 if (!new_instance) {
2939 *err = json_pack("{s:s}", "type", "invalidPatch");
2940 goto done;
2941 }
2942 json_object_del(new_instance, "recurrenceRule");
2943 json_object_del(new_instance, "recurrenceOverrides");
2944 new_override = jmap_patchobject_create(old_event, new_instance);
2945 json_object_del(new_override, "@type");
2946 json_object_del(new_override, "method");
2947 json_object_del(new_override, "prodId");
2948 json_object_del(new_override, "recurrenceId");
2949 json_object_del(new_override, "recurrenceRule");
2950 json_object_del(new_override, "recurrenceOverrides");
2951 json_object_del(new_override, "relatedTo");
2952 json_object_del(new_override, "replyTo");
2953 json_object_del(new_override, "uid");
2954 json_decref(new_instance);
2955
2956 if (json_boolean_value(json_object_get(new_override, "excluded"))) {
2957 if (is_rdate) {
2958 /* No need to set it in recurrenceOverrides */
2959 json_decref(new_override);
2960 new_override = NULL;
2961 }
2962 else if (json_object_size(new_override) > 1) {
2963 /* Normalize excluded override */
2964 json_decref(new_override);
2965 new_override = json_pack("{s:b}", "excluded", 1);
2966 }
2967 }
2968 else if (json_object_size(new_override) == 0) {
2969 if (!is_rdate) {
2970 /* No need to set it in recurrenceOverrides */
2971 json_decref(new_override);
2972 new_override = NULL;
2973 }
2974 }
2975
2976 /* Create the new JSEvent */
2977 new_event = json_deep_copy(old_event);
2978 json_t *new_overrides = json_object_get(new_event, "recurrenceOverrides");
2979 if (new_override) {
2980 /* Update or create override */
2981 if (new_overrides == NULL || json_is_null(new_overrides)) {
2982 new_overrides = json_object();
2983 json_object_set_new(new_event, "recurrenceOverrides", new_overrides);
2984 }
2985 json_object_set_new(new_overrides, recurid, new_override);
2986 } else {
2987 /* Remove existing override */
2988 json_object_del(new_overrides, recurid);
2989 }
2990 }
2991 else {
2992 /* Update a regular event */
2993 if (eventpatch_updates_recurrenceoverrides(event_patch)) {
2994 /* Split patch into main event and override patches */
2995 json_t *mainevent_patch = json_object();
2996 json_t *overrides_patch = json_object();
2997 const char *key;
2998 json_t *jval;
2999 json_object_foreach(event_patch, key, jval) {
3000 if (!strncmp(key, "recurrenceOverrides/", 20)) {
3001 json_object_set(overrides_patch, key, jval);
3002 }
3003 else {
3004 json_object_set(mainevent_patch, key, jval);
3005 }
3006 }
3007
3008 /* Apply patch to main event */
3009 json_t *old_mainevent = json_deep_copy(old_event);
3010 json_object_del(old_mainevent, "recurrenceOverrides");
3011 new_event = jmap_patchobject_apply(old_mainevent, mainevent_patch, invalid);
3012 if (!new_event) {
3013 *err = json_pack("{s:s}", "type", "invalidPatch");
3014 json_decref(old_mainevent);
3015 goto done;
3016 }
3017
3018 /* Expand current overrides from patched main event */
3019 json_t *old_overrides = json_object_get(old_event, "recurrenceOverrides");
3020 json_t *old_exp_overrides = json_object();
3021 json_t *old_override;
3022 const char *recurid;
3023 json_object_foreach(old_overrides, recurid, old_override) {
3024 if (json_boolean_value(json_object_get(old_override, "excluded"))) {
3025 json_object_set(old_exp_overrides, recurid, old_override);
3026 continue;
3027 }
3028 json_t *override = jmap_patchobject_apply(new_event, old_override, NULL);
3029 if (override) {
3030 json_object_set_new(old_exp_overrides, recurid, override);
3031 }
3032 }
3033 if (!json_object_size(old_exp_overrides)) {
3034 json_decref(old_exp_overrides);
3035 old_exp_overrides = json_null();
3036 }
3037
3038 /* Apply override patches to expanded overrides */
3039 json_t *new_exp_overrides = NULL;
3040 if (json_object_size(old_exp_overrides)) {
3041 json_t *old_wrapper = json_pack("{s:O}", "recurrenceOverrides", old_exp_overrides);
3042 json_t *new_wrapper = jmap_patchobject_apply(old_wrapper, overrides_patch, invalid);
3043 if (!new_wrapper) {
3044 *err = json_pack("{s:s}", "type", "invalidPatch");
3045 json_decref(old_wrapper);
3046 goto done;
3047 }
3048 new_exp_overrides = json_incref(json_object_get(new_wrapper, "recurrenceOverrides"));
3049 json_decref(old_wrapper);
3050 json_decref(new_wrapper);
3051 }
3052
3053 /* Diff patched overrides with patched main event */
3054 json_t *new_overrides = json_object();
3055 struct buf buf = BUF_INITIALIZER;
3056 json_object_foreach(new_exp_overrides, recurid, jval) {
3057 /* Don't diff excluded overrides */
3058 if (json_boolean_value(json_object_get(jval, "excluded"))) {
3059 json_object_set(new_overrides, recurid, jval);
3060 continue;
3061 }
3062 /* Don't diff replaced overrides */
3063 buf_setcstr(&buf, "recurrenceOverrides/");
3064 buf_appendcstr(&buf, recurid);
3065 if (json_object_get(overrides_patch, buf_cstring(&buf))) {
3066 json_object_set(new_overrides, recurid, jval);
3067 continue;
3068 }
3069 /* Diff updated override */
3070 json_t *new_override = jmap_patchobject_create(new_event, jval);
3071 if (!new_override) continue;
3072 json_object_set_new(new_overrides, recurid, new_override);
3073 }
3074 buf_free(&buf);
3075 if (!json_object_size(new_overrides)) {
3076 json_decref(new_overrides);
3077 new_overrides = json_null();
3078 }
3079
3080 /* Combine new main event with new overrides */
3081 json_object_set_new(new_event, "recurrenceOverrides", new_overrides);
3082
3083 json_decref(mainevent_patch);
3084 json_decref(overrides_patch);
3085 json_decref(old_exp_overrides);
3086 json_decref(new_exp_overrides);
3087 json_decref(old_mainevent);
3088 }
3089 else {
3090 /* The happy path - just apply the patch as provided */
3091 new_event = jmap_patchobject_apply(old_event, event_patch, invalid);
3092 if (!new_event) {
3093 *err = json_pack("{s:s}", "type", "invalidPatch");
3094 goto done;
3095 }
3096 }
3097 }
3098
3099 // check that participantId is either not present or is a valid participant
3100 participant_id = validate_participant_id(new_event, schedule_addresses, invalid);
3101
3102 /* Determine if to bump sequence */
3103 json_t *jdiff = jmap_patchobject_create(old_event, new_event);
3104 json_object_del(jdiff, "updated");
3105 if (!eventpatch_updates_peruserprops_only(jdiff, participant_id)) {
3106 json_int_t oldseq = json_integer_value(json_object_get(old_event, "sequence"));
3107 json_int_t newseq = json_integer_value(json_object_get(new_event, "sequence"));
3108 if (newseq <= oldseq) {
3109 json_int_t newseq = oldseq + 1;
3110 json_object_set_new(new_event, "sequence", json_integer(newseq));
3111 json_object_set_new(update, "sequence", json_integer(newseq));
3112 }
3113 }
3114 json_decref(jdiff);
3115
3116 /* Convert to iCalendar */
3117 *newical = jmapical_toical(new_event, oldical, invalid);
3118
3119 done:
3120 json_decref(new_event);
3121 json_decref(old_event);
3122 return r;
3123 }
3124
setcalendarevents_update(jmap_req_t * req,json_t * event_patch,struct event_id * eid,struct caldav_db * db,json_t * invalid,json_t * update,json_t ** err)3125 static int setcalendarevents_update(jmap_req_t *req,
3126 json_t *event_patch,
3127 struct event_id *eid,
3128 struct caldav_db *db,
3129 json_t *invalid,
3130 json_t *update,
3131 json_t **err)
3132 {
3133 int r, pe;
3134 int needrights = JACL_UPDATEITEMS|JACL_SETMETADATA;
3135
3136 struct caldav_data *cdata = NULL;
3137 struct mailbox *mbox = NULL;
3138 char *mboxname = NULL;
3139 struct mailbox *dstmbox = NULL;
3140 char *dstmboxname = NULL;
3141 struct mboxevent *mboxevent = NULL;
3142 char *resource = NULL;
3143
3144 icalcomponent *oldical = NULL;
3145 icalcomponent *ical = NULL;
3146 struct index_record record;
3147 const char *calendarId = NULL;
3148 strarray_t schedule_addresses = STRARRAY_INITIALIZER;
3149
3150 /* Validate calendarId */
3151 pe = jmap_readprop(event_patch, "calendarId", 0, invalid, "s", &calendarId);
3152 if (pe > 0 && *calendarId && *calendarId == '#') {
3153 calendarId = jmap_lookup_id(req, calendarId + 1);
3154 if (!calendarId) {
3155 json_array_append_new(invalid, json_string("calendarId"));
3156 }
3157 }
3158 if (json_array_size(invalid)) {
3159 r = 0;
3160 goto done;
3161 }
3162
3163 /* Determine mailbox and resource name of calendar event. */
3164 r = caldav_lookup_uid(db, eid->uid, &cdata);
3165 if (r && r != CYRUSDB_NOTFOUND) {
3166 syslog(LOG_ERR,
3167 "caldav_lookup_uid(%s) failed: %s", eid->uid, error_message(r));
3168 goto done;
3169 }
3170 if (r == CYRUSDB_NOTFOUND || !cdata->dav.alive ||
3171 !cdata->dav.rowid || !cdata->dav.imap_uid ||
3172 cdata->comp_type != CAL_COMP_VEVENT) {
3173 r = IMAP_NOTFOUND;
3174 goto done;
3175 }
3176 mboxname = xstrdup(cdata->dav.mailbox);
3177 resource = xstrdup(cdata->dav.resource);
3178
3179 /* Check permissions. */
3180 if (!jmap_hasrights(req, mboxname, needrights)) {
3181 json_array_append_new(invalid, json_string("calendarId"));
3182 r = 0;
3183 goto done;
3184 }
3185
3186 /* Open mailbox for writing */
3187 r = jmap_openmbox(req, mboxname, &mbox, 1);
3188 if (r == IMAP_MAILBOX_NONEXISTENT) {
3189 json_array_append_new(invalid, json_string("calendarId"));
3190 r = 0;
3191 goto done;
3192 }
3193 else if (r) {
3194 syslog(LOG_ERR, "jmap_openmbox(req, %s) failed: %s",
3195 mboxname, error_message(r));
3196 goto done;
3197 }
3198
3199 /* Fetch index record for the resource */
3200 memset(&record, 0, sizeof(struct index_record));
3201 r = mailbox_find_index_record(mbox, cdata->dav.imap_uid, &record);
3202 if (r == IMAP_NOTFOUND) {
3203 json_array_append_new(invalid, json_string("calendarId"));
3204 r = 0;
3205 goto done;
3206 } else if (r) {
3207 syslog(LOG_ERR, "mailbox_index_record(0x%x) failed: %s",
3208 cdata->dav.imap_uid, error_message(r));
3209 goto done;
3210 }
3211 /* Load VEVENT from record, personalizing as needed. */
3212 oldical = caldav_record_to_ical(mbox, cdata, httpd_userid, &schedule_addresses);
3213 if (!oldical) {
3214 syslog(LOG_ERR, "record_to_ical failed for record %u:%s",
3215 cdata->dav.imap_uid, mbox->name);
3216 r = IMAP_INTERNAL;
3217 goto done;
3218 }
3219 /* Apply patch */
3220 r = setcalendarevents_apply_patch(event_patch, oldical, eid->recurid,
3221 invalid, &schedule_addresses, &ical, update, err);
3222 if (json_array_size(invalid)) {
3223 r = 0;
3224 goto done;
3225 }
3226 else if (r) goto done;
3227
3228 if (calendarId) {
3229 /* Check, if we need to move the event. */
3230 dstmboxname = caldav_mboxname(req->accountid, calendarId);
3231 if (strcmp(mbox->name, dstmboxname)) {
3232 /* Check permissions */
3233 if (!jmap_hasrights(req, dstmboxname, needrights)) {
3234 json_array_append_new(invalid, json_string("calendarId"));
3235 r = 0;
3236 goto done;
3237 }
3238 /* Open destination mailbox for writing. */
3239 r = jmap_openmbox(req, dstmboxname, &dstmbox, 1);
3240 if (r == IMAP_MAILBOX_NONEXISTENT) {
3241 json_array_append_new(invalid, json_string("calendarId"));
3242 r = 0;
3243 goto done;
3244 } else if (r) {
3245 syslog(LOG_ERR, "jmap_openmbox(req, %s) failed: %s",
3246 dstmboxname, error_message(r));
3247 goto done;
3248 }
3249 }
3250 }
3251
3252 /* Handle scheduling. */
3253 r = setcalendarevents_schedule(req, mboxname, &schedule_addresses,
3254 oldical, ical, JMAP_UPDATE);
3255 if (r) goto done;
3256
3257
3258 if (dstmbox) {
3259 /* Expunge the resource from mailbox. */
3260 record.internal_flags |= FLAG_INTERNAL_EXPUNGED;
3261 mboxevent = mboxevent_new(EVENT_MESSAGE_EXPUNGE);
3262 r = mailbox_rewrite_index_record(mbox, &record);
3263 if (r) {
3264 syslog(LOG_ERR, "mailbox_rewrite_index_record (%s) failed: %s",
3265 cdata->dav.mailbox, error_message(r));
3266 jmap_closembox(req, &mbox);
3267 goto done;
3268 }
3269 mboxevent_extract_record(mboxevent, mbox, &record);
3270 mboxevent_extract_mailbox(mboxevent, mbox);
3271 mboxevent_set_numunseen(mboxevent, mbox, -1);
3272 mboxevent_set_access(mboxevent, NULL, NULL,
3273 req->userid, cdata->dav.mailbox, 0);
3274 jmap_closembox(req, &mbox);
3275 mboxevent_notify(&mboxevent);
3276 mboxevent_free(&mboxevent);
3277
3278 /* Close the mailbox we moved the event from. */
3279 jmap_closembox(req, &mbox);
3280 mbox = dstmbox;
3281 dstmbox = NULL;
3282 free(mboxname);
3283 mboxname = dstmboxname;
3284 dstmboxname = NULL;
3285 }
3286
3287 /* Remove METHOD property */
3288 remove_itip_properties(ical);
3289
3290 /* Store the updated VEVENT. */
3291 struct transaction_t txn;
3292 memset(&txn, 0, sizeof(struct transaction_t));
3293 txn.req_hdrs = spool_new_hdrcache();
3294 /* XXX - fix userid */
3295 r = proxy_mlookup(mbox->name, &txn.req_tgt.mbentry, NULL, NULL);
3296 if (r) {
3297 syslog(LOG_ERR, "mlookup(%s) failed: %s", mbox->name, error_message(r));
3298 }
3299 else {
3300 r = caldav_store_resource(&txn, ical, mbox, resource, record.createdmodseq,
3301 db, 0, httpd_userid, &schedule_addresses);
3302 }
3303 transaction_free(&txn);
3304 if (r && r != HTTP_CREATED && r != HTTP_NO_CONTENT) {
3305 syslog(LOG_ERR, "caldav_store_resource failed for user %s: %s",
3306 req->accountid, error_message(r));
3307 goto done;
3308 }
3309 r = 0;
3310
3311 done:
3312 if (mbox) jmap_closembox(req, &mbox);
3313 if (dstmbox) jmap_closembox(req, &dstmbox);
3314 if (ical) icalcomponent_free(ical);
3315 if (oldical) icalcomponent_free(oldical);
3316 strarray_fini(&schedule_addresses);
3317 free(dstmboxname);
3318 free(resource);
3319 free(mboxname);
3320 return r;
3321 }
3322
setcalendarevents_destroy(jmap_req_t * req,struct event_id * eid,struct caldav_db * db)3323 static int setcalendarevents_destroy(jmap_req_t *req,
3324 struct event_id *eid,
3325 struct caldav_db *db)
3326 {
3327 int r;
3328 int needrights = JACL_REMOVEITEMS;
3329
3330 struct caldav_data *cdata = NULL;
3331 struct mailbox *mbox = NULL;
3332 char *mboxname = NULL;
3333 struct mboxevent *mboxevent = NULL;
3334 char *resource = NULL;
3335
3336 icalcomponent *oldical = NULL;
3337 icalcomponent *ical = NULL;
3338 struct index_record record;
3339 strarray_t schedule_addresses = STRARRAY_INITIALIZER;
3340
3341 if (eid->recurid) {
3342 /* Destroying a recurrence instance is setting it excluded */
3343 json_t *event_patch = json_pack("{s:b}", "excluded", 1);
3344 json_t *invalid = json_array();
3345 json_t *update = NULL;
3346 json_t *err = NULL;
3347 r = setcalendarevents_update(req, event_patch, eid, db, invalid, update, &err);
3348 json_decref(event_patch);
3349 json_decref(update);
3350 if (err || (!r && json_array_size(invalid))) {
3351 r = IMAP_INTERNAL;
3352 json_decref(err);
3353 }
3354 json_decref(invalid);
3355 return r;
3356 }
3357
3358 /* Determine mailbox and resource name of calendar event. */
3359 r = caldav_lookup_uid(db, eid->uid, &cdata);
3360 if (r) {
3361 syslog(LOG_ERR,
3362 "caldav_lookup_uid(%s) failed: %s", eid->uid, cyrusdb_strerror(r));
3363 r = CYRUSDB_NOTFOUND ? IMAP_NOTFOUND : IMAP_INTERNAL;
3364 goto done;
3365 }
3366 mboxname = xstrdup(cdata->dav.mailbox);
3367 resource = xstrdup(cdata->dav.resource);
3368
3369 /* Check permissions. */
3370 if (!jmap_hasrights(req, mboxname, JACL_READITEMS)) {
3371 r = IMAP_NOTFOUND;
3372 goto done;
3373 }
3374 if (!jmap_hasrights(req, mboxname, needrights)) {
3375 r = IMAP_PERMISSION_DENIED;
3376 goto done;
3377 }
3378
3379 /* Open mailbox for writing */
3380 r = jmap_openmbox(req, mboxname, &mbox, 1);
3381 if (r) {
3382 syslog(LOG_ERR, "jmap_openmbox(req, %s) failed: %s",
3383 mboxname, error_message(r));
3384 goto done;
3385 }
3386
3387 /* Fetch index record for the resource. Need this for scheduling. */
3388 memset(&record, 0, sizeof(struct index_record));
3389 r = mailbox_find_index_record(mbox, cdata->dav.imap_uid, &record);
3390 if (r) {
3391 syslog(LOG_ERR, "mailbox_index_record(0x%x) failed: %s",
3392 cdata->dav.imap_uid, error_message(r));
3393 goto done;
3394 }
3395 /* Load VEVENT from record. */
3396 oldical = record_to_ical(mbox, &record, &schedule_addresses);
3397 if (!oldical) {
3398 syslog(LOG_ERR, "record_to_ical failed for record %u:%s",
3399 cdata->dav.imap_uid, mbox->name);
3400 r = IMAP_INTERNAL;
3401 goto done;
3402 }
3403
3404 /* Handle scheduling. */
3405 r = setcalendarevents_schedule(req, mboxname, &schedule_addresses,
3406 oldical, ical, JMAP_DESTROY);
3407 if (r) goto done;
3408
3409
3410 /* Expunge the resource from mailbox. */
3411 record.internal_flags |= FLAG_INTERNAL_EXPUNGED;
3412 mboxevent = mboxevent_new(EVENT_MESSAGE_EXPUNGE);
3413 r = mailbox_rewrite_index_record(mbox, &record);
3414 if (r) {
3415 syslog(LOG_ERR, "mailbox_rewrite_index_record (%s) failed: %s",
3416 cdata->dav.mailbox, error_message(r));
3417 jmap_closembox(req, &mbox);
3418 goto done;
3419 }
3420 mboxevent_extract_record(mboxevent, mbox, &record);
3421 mboxevent_extract_mailbox(mboxevent, mbox);
3422 mboxevent_set_numunseen(mboxevent, mbox, -1);
3423 mboxevent_set_access(mboxevent, NULL, NULL,
3424 req->userid, cdata->dav.mailbox, 0);
3425 jmap_closembox(req, &mbox);
3426 mboxevent_notify(&mboxevent);
3427 mboxevent_free(&mboxevent);
3428
3429 done:
3430 if (mbox) jmap_closembox(req, &mbox);
3431 if (oldical) icalcomponent_free(oldical);
3432 strarray_fini(&schedule_addresses);
3433 free(resource);
3434 free(mboxname);
3435 return r;
3436 }
3437
setcalendarevents_parse_id(jmap_req_t * req,const char * id)3438 static struct event_id *setcalendarevents_parse_id(jmap_req_t *req, const char *id)
3439 {
3440 if (id && id[0] == '#') {
3441 const char *newid = jmap_lookup_id(req, id + 1);
3442 if (!newid) return NULL;
3443 id = newid;
3444 }
3445 return parse_eventid(id);
3446 }
3447
3448
jmap_calendarevent_set(struct jmap_req * req)3449 static int jmap_calendarevent_set(struct jmap_req *req)
3450 {
3451 struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
3452 struct jmap_set set;
3453 json_t *err = NULL;
3454 struct caldav_db *db = NULL;
3455 struct event_id *eid = NULL;
3456 const char *id;
3457 int r = 0;
3458
3459 /* Parse arguments */
3460 jmap_set_parse(req, &parser, event_props, NULL, NULL, &set, &err);
3461 if (err) {
3462 jmap_error(req, err);
3463 goto done;
3464 }
3465
3466 if (set.if_in_state) {
3467 /* TODO rewrite state function to use char* not json_t* */
3468 json_t *jstate = json_string(set.if_in_state);
3469 if (jmap_cmpstate(req, jstate, MBTYPE_CALENDAR)) {
3470 jmap_error(req, json_pack("{s:s}", "type", "stateMismatch"));
3471 goto done;
3472 }
3473 json_decref(jstate);
3474 set.old_state = xstrdup(set.if_in_state);
3475 }
3476 else {
3477 json_t *jstate = jmap_getstate(req, MBTYPE_CALENDAR, /*refresh*/0);
3478 set.old_state = xstrdup(json_string_value(jstate));
3479 json_decref(jstate);
3480 }
3481
3482 r = caldav_create_defaultcalendars(req->accountid);
3483 if (r == IMAP_MAILBOX_NONEXISTENT) {
3484 /* The account exists but does not have a root mailbox. */
3485 json_t *err = json_pack("{s:s}", "type", "accountNoCalendars");
3486 json_array_append_new(req->response, json_pack("[s,o,s]",
3487 "error", err, req->tag));
3488 return 0;
3489 } else if (r) return r;
3490
3491 db = caldav_open_userid(req->accountid);
3492 if (!db) {
3493 syslog(LOG_ERR, "caldav_open_mailbox failed for user %s", req->userid);
3494 r = IMAP_INTERNAL;
3495 goto done;
3496 }
3497
3498
3499 /* create */
3500 const char *key;
3501 json_t *arg;
3502 json_object_foreach(set.create, key, arg) {
3503 /* Validate calendar event id. */
3504 if (!strlen(key)) {
3505 json_t *err= json_pack("{s:s}", "type", "invalidArguments");
3506 json_object_set_new(set.not_created, key, err);
3507 continue;
3508 }
3509
3510 /* Create the calendar event. */
3511 json_t *invalid = json_pack("[]");
3512 json_t *create = json_pack("{}");
3513 r = setcalendarevents_create(req, req->accountid, arg, db, invalid, create);
3514 if (r) {
3515 json_t *err;
3516 switch (r) {
3517 case HTTP_FORBIDDEN:
3518 case IMAP_PERMISSION_DENIED:
3519 err = json_pack("{s:s}", "type", "forbidden");
3520 break;
3521 case IMAP_QUOTA_EXCEEDED:
3522 err = json_pack("{s:s}", "type", "overQuota");
3523 break;
3524 default:
3525 err = jmap_server_error(r);
3526 }
3527 json_object_set_new(set.not_created, key, err);
3528 json_decref(create);
3529 r = 0;
3530 continue;
3531 }
3532 if (json_array_size(invalid)) {
3533 json_t *err = json_pack("{s:s s:o}",
3534 "type", "invalidProperties",
3535 "properties", invalid);
3536 json_object_set_new(set.not_created, key, err);
3537 json_decref(create);
3538 continue;
3539 }
3540 json_decref(invalid);
3541
3542 /* Report calendar event as created. */
3543 const char *uid = json_string_value(json_object_get(create, "uid"));
3544 json_object_set_new(create, "id", json_string(uid));
3545 json_object_set_new(set.created, key, create);
3546 jmap_add_id(req, key, uid);
3547 }
3548
3549
3550 /* update */
3551 json_object_foreach(set.update, id, arg) {
3552 free_eventid(&eid);
3553
3554 eid = setcalendarevents_parse_id(req, id);
3555 if (!eid) {
3556 json_object_set_new(set.not_updated, id,
3557 json_pack("{s:s}", "type", "notFound"));
3558 continue;
3559 }
3560
3561 const char *uidval = NULL;
3562 if ((uidval = json_string_value(json_object_get(arg, "uid")))) {
3563 /* The uid property must match the current iCalendar UID */
3564 if (strcmp(uidval, eid->uid)) {
3565 json_t *err = json_pack(
3566 "{s:s, s:o}",
3567 "type", "invalidProperties",
3568 "properties", json_pack("[s]"));
3569 json_object_set_new(set.not_updated, eid->raw, err);
3570 continue;
3571 }
3572 }
3573
3574 /* Update the calendar event. */
3575 json_t *invalid = json_pack("[]");
3576 json_t *update = json_pack("{}");
3577 json_t *err = NULL;
3578 r = setcalendarevents_update(req, arg, eid, db, invalid, update, &err);
3579 if (r || err) {
3580 if (!err) {
3581 switch (r) {
3582 case IMAP_NOTFOUND:
3583 err = json_pack("{s:s}", "type", "notFound");
3584 break;
3585 case HTTP_FORBIDDEN:
3586 case IMAP_PERMISSION_DENIED:
3587 err = json_pack("{s:s}", "type", "forbidden");
3588 break;
3589 case HTTP_NO_STORAGE:
3590 case IMAP_QUOTA_EXCEEDED:
3591 err = json_pack("{s:s}", "type", "overQuota");
3592 break;
3593 default:
3594 err = jmap_server_error(r);
3595 }
3596 }
3597 json_object_set_new(set.not_updated, eid->raw, err);
3598 json_decref(invalid);
3599 json_decref(update);
3600 r = 0;
3601 continue;
3602 }
3603
3604 if (json_array_size(invalid)) {
3605 json_t *err = json_pack(
3606 "{s:s, s:o}", "type", "invalidProperties",
3607 "properties", invalid);
3608 json_object_set_new(set.not_updated, eid->raw, err);
3609 json_decref(update);
3610 continue;
3611 }
3612 json_decref(invalid);
3613
3614 if(!json_object_size(update)) {
3615 json_decref(update);
3616 update = json_null();
3617 }
3618
3619 /* Report calendar event as updated. */
3620 json_object_set_new(set.updated, eid->raw, update);
3621 }
3622 free_eventid(&eid);
3623
3624
3625 /* destroy */
3626 size_t index;
3627 json_t *juid;
3628
3629 json_array_foreach(set.destroy, index, juid) {
3630 free_eventid(&eid);
3631
3632 const char *id = json_string_value(juid);
3633 if (!id) continue;
3634
3635 eid = setcalendarevents_parse_id(req, id);
3636 if (!eid) {
3637 json_object_set_new(set.not_destroyed, id,
3638 json_pack("{s:s}", "type", "notFound"));
3639 continue;
3640 }
3641
3642 /* Destroy the calendar event. */
3643 r = setcalendarevents_destroy(req, eid, db);
3644 if (r == IMAP_NOTFOUND) {
3645 json_t *err = json_pack("{s:s}", "type", "notFound");
3646 json_object_set_new(set.not_destroyed, eid->raw, err);
3647 r = 0;
3648 continue;
3649 } else if (r == IMAP_PERMISSION_DENIED) {
3650 json_t *err = json_pack("{s:s}", "type", "forbidden");
3651 json_object_set_new(set.not_destroyed, eid->raw, err);
3652 r = 0;
3653 continue;
3654 } else if (r) {
3655 goto done;
3656 }
3657
3658 /* Report calendar event as destroyed. */
3659 json_array_append_new(set.destroyed, json_string(eid->raw));
3660 }
3661 free_eventid(&eid);
3662
3663
3664 // TODO refactor jmap_getstate to return a string, once
3665 // all code has been migrated to the new JMAP parser.
3666 json_t *jstate = jmap_getstate(req, MBTYPE_CALENDAR, /*refresh*/1);
3667 set.new_state = xstrdup(json_string_value(jstate));
3668 json_decref(jstate);
3669
3670 jmap_ok(req, jmap_set_reply(&set));
3671
3672 done:
3673 jmap_parser_fini(&parser);
3674 jmap_set_fini(&set);
3675 if (db) caldav_close(db);
3676 free_eventid(&eid);
3677 return r;
3678 }
3679
3680 struct geteventchanges_rock {
3681 jmap_req_t *req;
3682 struct jmap_changes *changes;
3683 size_t seen_records;
3684 modseq_t highestmodseq;
3685 int check_acl;
3686 hash_table *mboxrights;
3687 };
3688
strip_spurious_deletes(struct geteventchanges_rock * urock)3689 static void strip_spurious_deletes(struct geteventchanges_rock *urock)
3690 {
3691 /* if something is mentioned in both DELETEs and UPDATEs, it's probably
3692 * a move. O(N*M) algorithm, but there are rarely many, and the alternative
3693 * of a hash will cost more */
3694 unsigned i, j;
3695
3696 for (i = 0; i < json_array_size(urock->changes->destroyed); i++) {
3697 const char *del = json_string_value(json_array_get(urock->changes->destroyed, i));
3698
3699 for (j = 0; j < json_array_size(urock->changes->updated); j++) {
3700 const char *up =
3701 json_string_value(json_array_get(urock->changes->updated, j));
3702 if (!strcmpsafe(del, up)) {
3703 json_array_remove(urock->changes->destroyed, i--);
3704 break;
3705 }
3706 }
3707 }
3708 }
3709
geteventchanges_cb(void * vrock,struct caldav_data * cdata)3710 static int geteventchanges_cb(void *vrock, struct caldav_data *cdata)
3711 {
3712 struct geteventchanges_rock *rock = vrock;
3713 jmap_req_t *req = rock->req;
3714 struct jmap_changes *changes = rock->changes;
3715
3716 /* Check permissions */
3717 if (!jmap_hasrights(req, cdata->dav.mailbox, JACL_READITEMS))
3718 return 0;
3719
3720 if (cdata->comp_type != CAL_COMP_VEVENT)
3721 return 0;
3722
3723 /* Count, but don't process items that exceed the maximum record count. */
3724 if (changes->max_changes && ++(rock->seen_records) > changes->max_changes) {
3725 changes->has_more_changes = 1;
3726 return 0;
3727 }
3728
3729 /* Report item as updated or destroyed. */
3730 if (cdata->dav.alive) {
3731 if (cdata->dav.createdmodseq <= changes->since_modseq)
3732 json_array_append_new(changes->updated, json_string(cdata->ical_uid));
3733 else
3734 json_array_append_new(changes->created, json_string(cdata->ical_uid));
3735 } else {
3736 if (cdata->dav.createdmodseq <= changes->since_modseq)
3737 json_array_append_new(changes->destroyed, json_string(cdata->ical_uid));
3738 }
3739
3740 if (cdata->dav.modseq > rock->highestmodseq) {
3741 rock->highestmodseq = cdata->dav.modseq;
3742 }
3743
3744 return 0;
3745 }
3746
jmap_calendarevent_changes(struct jmap_req * req)3747 static int jmap_calendarevent_changes(struct jmap_req *req)
3748 {
3749 struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
3750 struct jmap_changes changes;
3751 json_t *err = NULL;
3752 struct caldav_db *db;
3753 struct geteventchanges_rock rock = {
3754 req,
3755 &changes,
3756 0 /*seen_records*/,
3757 0 /*highestmodseq*/,
3758 strcmp(req->accountid, req->userid) /* check_acl */,
3759 NULL /*mboxrights*/
3760 };
3761 int r = 0;
3762
3763 db = caldav_open_userid(req->accountid);
3764 if (!db) {
3765 syslog(LOG_ERR, "caldav_open_mailbox failed for user %s", req->accountid);
3766 r = IMAP_INTERNAL;
3767 goto done;
3768 }
3769
3770 /* Parse request */
3771 jmap_changes_parse(req, &parser, req->counters.caldavdeletedmodseq,
3772 NULL, NULL, &changes, &err);
3773 if (err) {
3774 jmap_error(req, err);
3775 goto done;
3776 }
3777
3778 /* Lookup changes. */
3779 r = caldav_get_updates(db, changes.since_modseq, NULL /*mboxname*/,
3780 CAL_COMP_VEVENT,
3781 changes.max_changes ? (int) changes.max_changes + 1 : -1,
3782 &geteventchanges_cb, &rock);
3783 if (r) goto done;
3784 strip_spurious_deletes(&rock);
3785
3786 /* Determine new state. */
3787 changes.new_modseq = changes.has_more_changes ?
3788 rock.highestmodseq : jmap_highestmodseq(req, MBTYPE_CALENDAR);
3789
3790 /* Build response */
3791 jmap_ok(req, jmap_changes_reply(&changes));
3792
3793 done:
3794 jmap_changes_fini(&changes);
3795 jmap_parser_fini(&parser);
3796 if (rock.mboxrights) {
3797 free_hash_table(rock.mboxrights, free);
3798 free(rock.mboxrights);
3799 }
3800 if (db) caldav_close(db);
3801 if (r) {
3802 jmap_error(req, jmap_server_error(r));
3803 }
3804 return 0;
3805 }
3806
eventquery_read_timerange(json_t * filter,time_t * before,time_t * after)3807 static void eventquery_read_timerange(json_t *filter, time_t *before, time_t *after)
3808 {
3809 *before = caldav_eternity;
3810 *after = caldav_epoch;
3811
3812 if (!JNOTNULL(filter)) {
3813 return;
3814 }
3815
3816 if (json_object_get(filter, "conditions")) {
3817 json_t *val;
3818 size_t i;
3819 time_t bf, af;
3820
3821 json_array_foreach(json_object_get(filter, "conditions"), i, val) {
3822 const char *op =
3823 json_string_value(json_object_get(filter, "operator"));
3824 bf = caldav_eternity;
3825 af = caldav_epoch;
3826
3827 eventquery_read_timerange(val, &bf, &af);
3828
3829 if (bf != caldav_eternity) {
3830 if (!strcmp(op, "OR")) {
3831 if (*before == caldav_eternity || *before < bf) {
3832 *before = bf;
3833 }
3834 }
3835 else if (!strcmp(op, "AND")) {
3836 if (*before == caldav_eternity || *before > bf) {
3837 *before = bf;
3838 }
3839 }
3840 else if (!strcmp(op, "NOT")) {
3841 if (*after == caldav_epoch || *after < bf) {
3842 *after = bf;
3843 }
3844 }
3845 }
3846
3847 if (af != caldav_epoch) {
3848 if (!strcmp(op, "OR")) {
3849 if (*after == caldav_epoch || *after > af) {
3850 *after = af;
3851 }
3852 }
3853 else if (!strcmp(op, "AND")) {
3854 if (*after == caldav_epoch || *after < af) {
3855 *after = af;
3856 }
3857 }
3858 else if (!strcmp(op, "NOT")) {
3859 if (*before == caldav_eternity || *before < af) {
3860 *before = af;
3861 }
3862 }
3863 }
3864 }
3865 } else {
3866 const char *sb = json_string_value(json_object_get(filter, "before"));
3867 const char *sa = json_string_value(json_object_get(filter, "after"));
3868
3869 if (!sb || time_from_iso8601(sb, before) == -1) {
3870 *before = caldav_eternity;
3871 }
3872 if (!sa || time_from_iso8601(sa, after) == -1) {
3873 *after = caldav_epoch;
3874 }
3875 }
3876 }
3877
eventquery_have_textsearch(json_t * filter)3878 static int eventquery_have_textsearch(json_t *filter)
3879 {
3880 if (!JNOTNULL(filter))
3881 return 0;
3882
3883 json_t *jval;
3884 size_t i;
3885 json_array_foreach(json_object_get(filter, "conditions"), i, jval) {
3886 if (eventquery_have_textsearch(jval))
3887 return 1;
3888 }
3889
3890 if (json_object_get(filter, "inCalendars") ||
3891 json_object_get(filter, "text") ||
3892 json_object_get(filter, "title") ||
3893 json_object_get(filter, "description") ||
3894 json_object_get(filter, "location") ||
3895 json_object_get(filter, "owner") ||
3896 json_object_get(filter, "attendee")) {
3897 return 1;
3898 }
3899
3900 return 0;
3901 }
3902
3903 struct eventquery_match {
3904 char *ical_uid;
3905 char *utcstart;
3906 icalcomponent *ical;
3907 char *recurid;
3908 };
3909
eventquery_match_fini(struct eventquery_match * match)3910 static void eventquery_match_fini(struct eventquery_match *match)
3911 {
3912 if (!match) return;
3913 free(match->ical_uid);
3914 free(match->utcstart);
3915 free(match->recurid);
3916 if (match->ical) icalcomponent_free(match->ical);
3917 }
3918
eventquery_match_free(struct eventquery_match ** matchp)3919 static void eventquery_match_free(struct eventquery_match **matchp) {
3920 if (!matchp || !*matchp) return;
3921 eventquery_match_fini(*matchp);
3922 free(*matchp);
3923 *matchp = NULL;
3924 }
3925
3926 struct eventquery_cmp_rock {
3927 enum caldav_sort *sort;
3928 size_t nsort;
3929 };
3930
QSORT_R_COMPAR_ARGS(const void * va,const void * vb,void * vrock)3931 static int eventquery_cmp QSORT_R_COMPAR_ARGS(const void *va,
3932 const void *vb,
3933 void *vrock)
3934 {
3935 enum caldav_sort *sort = ((struct eventquery_cmp_rock*)vrock)->sort;
3936 size_t nsort = ((struct eventquery_cmp_rock*)vrock)->nsort;
3937 struct eventquery_match *ma = (struct eventquery_match*) *(void**)va;
3938 struct eventquery_match *mb = (struct eventquery_match*) *(void**)vb;
3939 size_t i;
3940
3941 for (i = 0; i < nsort; i++) {
3942 int ret = 0;
3943 switch (sort[i] & ~CAL_SORT_DESC) {
3944 case CAL_SORT_UID:
3945 ret = strcmp(ma->ical_uid, mb->ical_uid);
3946 break;
3947 case CAL_SORT_START:
3948 ret = strcmp(ma->utcstart, mb->utcstart);
3949 break;
3950 default:
3951 ret = 0;
3952 }
3953 if (ret)
3954 return sort[i] & CAL_SORT_DESC ? -ret : ret;
3955 }
3956
3957 return 0;
3958 }
3959
3960 struct eventquery_rock {
3961 jmap_req_t *req;
3962 int expandrecur;
3963 struct mailbox *mailbox;
3964 ptrarray_t *matches;
3965 };
3966
eventquery_cb(void * vrock,struct caldav_data * cdata)3967 static int eventquery_cb(void *vrock, struct caldav_data *cdata)
3968 {
3969 struct eventquery_rock *rock = vrock;
3970 jmap_req_t *req = rock->req;
3971
3972 if (!cdata->dav.alive || cdata->comp_type != CAL_COMP_VEVENT) {
3973 return 0;
3974 }
3975 if (!jmap_hasrights(rock->req, cdata->dav.mailbox, JACL_READITEMS)) {
3976 return 0;
3977 }
3978
3979 struct eventquery_match *match = xzmalloc(sizeof(struct eventquery_match));
3980 match->ical_uid = xstrdup(cdata->ical_uid);
3981 match->utcstart = xstrdup(cdata->dtstart);
3982 if (rock->expandrecur) {
3983 /* Load iCalendar data */
3984 if (!rock->mailbox || strcmp(rock->mailbox->name, cdata->dav.mailbox)) {
3985 if (rock->mailbox) {
3986 jmap_closembox(req, &rock->mailbox);
3987 }
3988 int r = jmap_openmbox(req, cdata->dav.mailbox, &rock->mailbox, 0);
3989 if (r) return r;
3990 }
3991 match->ical = caldav_record_to_ical(rock->mailbox, cdata, req->userid, NULL);
3992 if (!match->ical) {
3993 syslog(LOG_ERR, "%s: can't load ical for ical uid %s",
3994 __func__, cdata->ical_uid);
3995 eventquery_match_free(&match);
3996 return IMAP_INTERNAL;
3997 }
3998 }
3999 ptrarray_append(rock->matches, match);
4000
4001 return 0;
4002 }
4003
eventquery_textsearch_match(search_expr_t * parent,const char * s,const char * name)4004 static void eventquery_textsearch_match(search_expr_t *parent, const char *s, const char *name)
4005 {
4006 search_expr_t *e;
4007 const search_attr_t *attr = search_attr_find(name);
4008
4009 e = search_expr_new(parent, SEOP_FUZZYMATCH);
4010 e->attr = attr;
4011 e->value.s = xstrdup(s);
4012 if (!e->value.s) {
4013 e->op = SEOP_FALSE;
4014 e->attr = NULL;
4015 }
4016 }
4017
eventquery_textsearch_build(jmap_req_t * req,json_t * filter,search_expr_t * parent)4018 static search_expr_t *eventquery_textsearch_build(jmap_req_t *req,
4019 json_t *filter,
4020 search_expr_t *parent)
4021 {
4022 search_expr_t *this, *e;
4023 const char *s;
4024 size_t i;
4025 json_t *val, *arg;
4026
4027 if (!JNOTNULL(filter)) {
4028 return search_expr_new(parent, SEOP_TRUE);
4029 }
4030
4031 if ((s = json_string_value(json_object_get(filter, "operator")))) {
4032 enum search_op op = SEOP_UNKNOWN;
4033
4034 if (!strcmp("AND", s)) {
4035 op = SEOP_AND;
4036 } else if (!strcmp("OR", s)) {
4037 op = SEOP_OR;
4038 } else if (!strcmp("NOT", s)) {
4039 op = SEOP_NOT;
4040 }
4041
4042 this = search_expr_new(parent, op);
4043 e = op == SEOP_NOT ? search_expr_new(this, SEOP_OR) : this;
4044
4045 json_array_foreach(json_object_get(filter, "conditions"), i, val) {
4046 eventquery_textsearch_build(req, val, e);
4047 }
4048 } else {
4049 this = search_expr_new(parent, SEOP_AND);
4050
4051 if ((arg = json_object_get(filter, "inCalendars"))) {
4052 e = search_expr_new(this, SEOP_OR);
4053 json_array_foreach(arg, i, val) {
4054 const char *id = json_string_value(val);
4055 search_expr_t *m = search_expr_new(e, SEOP_MATCH);
4056 m->attr = search_attr_find("folder");
4057 m->value.s = caldav_mboxname(req->accountid, id);
4058 }
4059 }
4060
4061 if ((s = json_string_value(json_object_get(filter, "text")))) {
4062 e = search_expr_new(this, SEOP_OR);
4063 eventquery_textsearch_match(e, s, "body");
4064 eventquery_textsearch_match(e, s, "subject");
4065 eventquery_textsearch_match(e, s, "from");
4066 eventquery_textsearch_match(e, s, "to");
4067 eventquery_textsearch_match(e, s, "location");
4068 }
4069 if ((s = json_string_value(json_object_get(filter, "title")))) {
4070 eventquery_textsearch_match(this, s, "subject");
4071 }
4072 if ((s = json_string_value(json_object_get(filter, "description")))) {
4073 eventquery_textsearch_match(this, s, "body");
4074 }
4075 if ((s = json_string_value(json_object_get(filter, "location")))) {
4076 eventquery_textsearch_match(this, s, "location");
4077 }
4078 if ((s = json_string_value(json_object_get(filter, "owner")))) {
4079 eventquery_textsearch_match(this, s, "from");
4080 }
4081 if ((s = json_string_value(json_object_get(filter, "attendee")))) {
4082 eventquery_textsearch_match(this, s, "to");
4083 }
4084 }
4085
4086 return this;
4087 }
4088
4089
eventquery_search_run(jmap_req_t * req,json_t * filter,struct caldav_db * db,time_t before,time_t after,enum caldav_sort * sort,size_t nsort,int expandrecur,ptrarray_t * matches)4090 static int eventquery_search_run(jmap_req_t *req,
4091 json_t *filter,
4092 struct caldav_db *db,
4093 time_t before, time_t after,
4094 enum caldav_sort *sort,
4095 size_t nsort,
4096 int expandrecur,
4097 ptrarray_t *matches)
4098 {
4099 int r, i;
4100 struct searchargs *searchargs = NULL;
4101 struct index_init init;
4102 struct index_state *state = NULL;
4103 search_query_t *query = NULL;
4104 struct sortcrit *sortcrit = NULL;
4105 struct buf buf = BUF_INITIALIZER;
4106 char *icalbefore = NULL;
4107 char *icalafter = NULL;
4108 icaltimezone *utc = icaltimezone_get_utc_timezone();
4109 struct mailbox *mailbox = NULL;
4110 const char *wantuid = json_string_value(json_object_get(filter, "uid"));
4111
4112 if (before != caldav_eternity) {
4113 icaltimetype t = icaltime_from_timet_with_zone(before, 0, utc);
4114 icalbefore = icaltime_as_ical_string_r(t);
4115 }
4116 if (after != caldav_epoch) {
4117 icaltimetype t = icaltime_from_timet_with_zone(after, 0, utc);
4118 icalafter = icaltime_as_ical_string_r(t);
4119 }
4120
4121 /* Build searchargs */
4122 searchargs = new_searchargs(NULL, GETSEARCH_CHARSET_FIRST,
4123 &jmap_namespace, req->accountid, req->authstate, 0);
4124 searchargs->root = eventquery_textsearch_build(req, filter, NULL);
4125
4126 sortcrit = xzmalloc(2 * sizeof(struct sortcrit));
4127 sortcrit[0].key = SORT_FOLDER;
4128 sortcrit[1].key = SORT_SEQUENCE;
4129
4130 /* Run the search query */
4131 memset(&init, 0, sizeof(init));
4132 init.userid = req->accountid;
4133 init.authstate = req->authstate;
4134 init.want_expunged = 0;
4135 init.want_mbtype = MBTYPE_CALENDAR;
4136 init.examine_mode = 1;
4137
4138 char *mboxname = mboxname_user_mbox(req->accountid, "#calendars");
4139 r = index_open(mboxname, &init, &state);
4140 free(mboxname);
4141 if (r) goto done;
4142
4143 query = search_query_new(state, searchargs);
4144 query->sortcrit = sortcrit;
4145 query->multiple = 1;
4146 query->need_ids = 1;
4147 query->want_expunged = 0;
4148 query->want_mbtype = MBTYPE_CALENDAR;
4149 r = search_query_run(query);
4150 if (r && r != IMAP_NOTFOUND) goto done;
4151
4152 /* Process result */
4153 for (i = 0 ; i < query->merged_msgdata.count; i++) {
4154 MsgData *md = ptrarray_nth(&query->merged_msgdata, i);
4155
4156 search_folder_t *folder = md->folder;
4157 if (!folder || !jmap_hasrights(req, folder->mboxname, JACL_READITEMS)) {
4158 continue;
4159 }
4160
4161 struct caldav_data *cdata;
4162 if (caldav_lookup_imapuid(db, folder->mboxname, md->uid, &cdata, 0) == 0) {
4163
4164 /* Check time-range */
4165 if (icalafter && strcmp(cdata->dtend, icalafter) <= 0)
4166 continue;
4167 if (icalbefore && strcmp(cdata->dtstart, icalbefore) >= 0)
4168 continue;
4169
4170 if (wantuid && strcmp(wantuid, cdata->ical_uid))
4171 continue;
4172
4173 struct eventquery_match *match = xzmalloc(sizeof(struct eventquery_match));
4174 match->ical_uid = xstrdup(cdata->ical_uid);
4175 match->utcstart = xstrdup(cdata->dtstart);
4176 if (expandrecur) {
4177 /* Load iCalendar data */
4178 if (!mailbox || strcmp(mailbox->name, cdata->dav.mailbox)) {
4179 if (mailbox) {
4180 jmap_closembox(req, &mailbox);
4181 }
4182 r = jmap_openmbox(req, cdata->dav.mailbox, &mailbox, 0);
4183 if (r) goto done;
4184 }
4185
4186 match->ical = caldav_record_to_ical(mailbox, cdata, req->userid, NULL);
4187 if (!match->ical) {
4188 syslog(LOG_ERR, "%s: can't load ical for ical uid %s",
4189 __func__, cdata->ical_uid);
4190 free(match->ical_uid);
4191 free(match->utcstart);
4192 free(match);
4193 r = IMAP_INTERNAL;
4194 goto done;
4195 }
4196 }
4197 ptrarray_append(matches, match);
4198 }
4199 }
4200
4201 if (!expandrecur) {
4202 struct eventquery_cmp_rock rock = { sort, nsort };
4203 cyr_qsort_r(matches->data, matches->count, sizeof(void*),
4204 (int(*)(const void*, const void*, void*))eventquery_cmp, &rock);
4205 }
4206
4207 r = 0;
4208
4209 done:
4210 index_close(&state);
4211 if (searchargs) freesearchargs(searchargs);
4212 if (sortcrit) freesortcrit(sortcrit);
4213 if (query) search_query_free(query);
4214 jmap_closembox(req, &mailbox);
4215 free(icalbefore);
4216 free(icalafter);
4217 buf_free(&buf);
4218 return r;
4219 }
4220
4221 struct eventquery_fastpath_rock {
4222 jmap_req_t *req;
4223 struct jmap_query *query;
4224 };
4225
eventquery_fastpath_cb(void * rock,struct caldav_data * cdata)4226 static int eventquery_fastpath_cb(void *rock, struct caldav_data *cdata)
4227 {
4228 jmap_req_t *req = ((struct eventquery_fastpath_rock*)rock)->req;
4229 struct jmap_query *query = ((struct eventquery_fastpath_rock*)rock)->query;
4230
4231 assert(query->position >= 0);
4232
4233 /* Check type and permissions */
4234 if (!cdata->dav.alive || cdata->comp_type != CAL_COMP_VEVENT) {
4235 return 0;
4236 }
4237 if (!jmap_hasrights(req, cdata->dav.mailbox, JACL_READITEMS)) {
4238 return 0;
4239 }
4240
4241 query->total++;
4242
4243 /* Check search window */
4244 if (query->have_limit && json_array_size(query->ids) >= query->limit) {
4245 return 0;
4246 }
4247 if (query->position && (size_t) query->position <= query->total - 1) {
4248 return 0;
4249 }
4250
4251 json_array_append_new(query->ids, json_string(cdata->ical_uid));
4252
4253 return 0;
4254 }
4255
4256 struct eventquery_recur_rock {
4257 ptrarray_t *matches;
4258 struct buf *buf;
4259 };
4260
eventquery_recur_make_recurid(icalcomponent * comp,icaltimetype start,struct buf * buf)4261 static const char *eventquery_recur_make_recurid(icalcomponent *comp,
4262 icaltimetype start,
4263 struct buf *buf)
4264 {
4265 struct jmapical_datetime recuriddt = JMAPICAL_DATETIME_INITIALIZER;
4266
4267 icalproperty *prop;
4268 if ((prop = icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY))) {
4269 /* Recurrence override. */
4270 jmapical_datetime_from_icalprop(prop, &recuriddt);
4271 }
4272 else {
4273 /* RDATE or regular reccurence instance */
4274 for (prop = icalcomponent_get_first_property(comp, ICAL_RDATE_PROPERTY);
4275 prop;
4276 prop = icalcomponent_get_next_property(comp, ICAL_RDATE_PROPERTY)) {
4277 /* Read subseconds from RDATE */
4278 struct icaldatetimeperiodtype tval = icalproperty_get_rdate(prop);
4279 if (icaltime_compare(tval.time, start)) {
4280 /* XXX - could handle PERIOD type here */
4281 struct jmapical_datetime tmpdt = JMAPICAL_DATETIME_INITIALIZER;
4282 jmapical_datetime_from_icalprop(prop, &tmpdt);
4283 recuriddt.nano = tmpdt.nano;
4284 break;
4285 }
4286 }
4287 if (!recuriddt.nano) {
4288 /* Read subseconds from DTSTART */
4289 jmapical_datetime_from_icaltime(start, &recuriddt);
4290 prop = icalcomponent_get_first_property(comp, ICAL_DTSTART_PROPERTY);
4291 struct jmapical_datetime tmpdt = JMAPICAL_DATETIME_INITIALIZER;
4292 if (prop) jmapical_datetime_from_icalprop(prop, &tmpdt);
4293 recuriddt.nano = tmpdt.nano;
4294 }
4295 }
4296
4297 buf_reset(buf);
4298 jmapical_localdatetime_as_string(&recuriddt, buf);
4299 return buf_cstring(buf);
4300 }
4301
eventquery_recur_cb(icalcomponent * comp,icaltimetype start,icaltimetype end,void * vrock)4302 static int eventquery_recur_cb(icalcomponent *comp,
4303 icaltimetype start,
4304 icaltimetype end __attribute__((unused)),
4305 void *vrock)
4306 {
4307 struct eventquery_recur_rock *rock = vrock;
4308 icaltimezone *utc = icaltimezone_get_utc_timezone();
4309 icaltimetype utcstart = icaltime_convert_to_zone(start, utc);
4310
4311 struct eventquery_match *match = xzmalloc(sizeof(struct eventquery_match));
4312 match->ical_uid = xstrdup(icalcomponent_get_uid(comp));
4313 match->utcstart = xstrdup(icaltime_as_ical_string(utcstart));
4314 match->recurid = xstrdup(eventquery_recur_make_recurid(comp, start, rock->buf));
4315 ptrarray_append(rock->matches, match);
4316
4317 return 1;
4318 }
4319
eventquery_run(jmap_req_t * req,struct jmap_query * query,int expandrecur,json_t ** err)4320 static int eventquery_run(jmap_req_t *req,
4321 struct jmap_query *query,
4322 int expandrecur,
4323 json_t **err)
4324 {
4325 time_t before = caldav_eternity;
4326 time_t after = caldav_epoch;
4327 int r = HTTP_NOT_IMPLEMENTED;
4328 enum caldav_sort *sort = NULL;
4329 size_t nsort = 0;
4330
4331 /* Sanity check arguments */
4332 eventquery_read_timerange(query->filter, &before, &after);
4333 if (expandrecur && before == caldav_eternity) {
4334 /* Reject unbounded time-ranges for recurrence expansion */
4335 *err = json_pack("{s:s s:[s] s:s}", "type", "invalidArguments",
4336 "arguments", "expandRecurrences",
4337 "description","upper time-range filter MUST be set");
4338 return 0;
4339 }
4340
4341 ptrarray_t matches = PTRARRAY_INITIALIZER;
4342
4343 /* Open Caldav DB */
4344 struct caldav_db *db = caldav_open_userid(req->accountid);
4345 if (!db) {
4346 syslog(LOG_ERR, "%s:%s: can't open caldav db for %s",
4347 __FILE__, __func__, req->accountid);
4348 r = IMAP_INTERNAL;
4349 goto done;
4350 }
4351
4352 /* Parse sort */
4353 if (json_array_size(query->sort)) {
4354 nsort = json_array_size(query->sort);
4355 sort = xzmalloc(nsort * sizeof(enum caldav_sort));
4356 json_t *jval;
4357 size_t i;
4358 json_array_foreach(query->sort, i, jval) {
4359 const char *prop = json_string_value(json_object_get(jval, "property"));
4360 if (!strcmp(prop, "start"))
4361 sort[i] = CAL_SORT_START;
4362 else if (!strcmp(prop, "uid"))
4363 sort[i] = CAL_SORT_UID;
4364 else
4365 sort[i] = CAL_SORT_NONE;
4366 if (json_object_get(jval, "isAscending") == json_false()) {
4367 sort[i] |= CAL_SORT_DESC;
4368 }
4369 }
4370 }
4371
4372 int have_textsearch = eventquery_have_textsearch(query->filter);
4373
4374 /* Attempt to fast-path trivial query */
4375
4376 if (!have_textsearch && !expandrecur && query->position >= 0 && !query->anchor) {
4377 struct eventquery_fastpath_rock rock = { req, query };
4378 const char *wantuid = json_string_value(json_object_get(query->filter, "uid"));
4379 if (wantuid) {
4380 /* Super fast path! We only want a single UID */
4381 struct caldav_data *cdata = NULL;
4382 r = caldav_lookup_uid(db, wantuid, &cdata);
4383 if (!r) eventquery_fastpath_cb(&rock, cdata);
4384 if (r == CYRUSDB_NOTFOUND) r = 0;
4385 }
4386 else {
4387 /* Fast-path: we can offload most processing to Caldav DB. */
4388 r = caldav_foreach_timerange(db, NULL, after, before, sort, nsort,
4389 eventquery_fastpath_cb, &rock);
4390 }
4391 goto done;
4392 }
4393
4394 /* Handle non-trivial query */
4395
4396 if (have_textsearch) {
4397 /* Query and sort matches in search backend. */
4398 r = eventquery_search_run(req, query->filter, db, before, after,
4399 sort, nsort, expandrecur, &matches);
4400 if (r) goto done;
4401 }
4402 else {
4403 /* Query and sort matches in Caldav DB. */
4404 struct eventquery_rock rock = { req, expandrecur, NULL, &matches };
4405 enum caldav_sort mboxsort = CAL_SORT_MAILBOX;
4406 r = caldav_foreach_timerange(db, NULL, after, before,
4407 expandrecur ? &mboxsort : sort,
4408 expandrecur ? 1 : nsort,
4409 eventquery_cb, &rock);
4410 jmap_closembox(req, &rock.mailbox);
4411 }
4412
4413 if (expandrecur) {
4414 /* Expand and sort recurrence instance matches */
4415 icaltimezone *utc = icaltimezone_get_utc_timezone();
4416 struct icalperiodtype timerange = {
4417 icaltime_from_timet_with_zone(after, 0, utc),
4418 icaltime_from_timet_with_zone(before, 0, utc),
4419 icaldurationtype_null_duration()
4420 };
4421 ptrarray_t mymatches = PTRARRAY_INITIALIZER;
4422 struct buf buf = BUF_INITIALIZER;
4423 struct eventquery_match *match;
4424 while ((match = ptrarray_pop(&matches))) {
4425 icalcomponent *comp = icalcomponent_get_first_real_component(match->ical);
4426 icalcomponent_kind kind = icalcomponent_isa(comp);
4427
4428 int is_recurring = 0;
4429 for (; comp; comp = icalcomponent_get_next_component(match->ical, kind)) {
4430 if (icalcomponent_get_first_property(comp, ICAL_RRULE_PROPERTY) ||
4431 icalcomponent_get_first_property(comp, ICAL_RDATE_PROPERTY)) {
4432 is_recurring = 1;
4433 break;
4434 }
4435 }
4436
4437 if (is_recurring) {
4438 /* Expand all instances, we need them for totals */
4439 /* XXX - need tooManyRecurrenceInstances error ? */
4440 struct eventquery_recur_rock rock = { &mymatches, &buf };
4441 icalcomponent_myforeach(match->ical, timerange, utc,
4442 eventquery_recur_cb, &rock);
4443 eventquery_match_free(&match);
4444 }
4445 else ptrarray_append(&mymatches, match);
4446 }
4447 buf_free(&buf);
4448
4449 ptrarray_fini(&matches);
4450 matches = mymatches;
4451
4452 struct eventquery_cmp_rock rock = { sort, nsort };
4453 cyr_qsort_r(matches.data, matches.count, sizeof(void*), eventquery_cmp, &rock);
4454 }
4455
4456 query->total = ptrarray_size(&matches);
4457
4458 /* Determine start position of result list */
4459 size_t startpos = 0;
4460 if (query->anchor) {
4461 size_t j;
4462 for (j = 0; j < (size_t) ptrarray_size(&matches); j++) {
4463 struct eventquery_match *m = ptrarray_nth(&matches, j);
4464 if (!strcmp(query->anchor, m->ical_uid)) {
4465 /* Found anchor */
4466 if (query->anchor_offset < 0) {
4467 startpos = (size_t) -query->anchor_offset > j ?
4468 0 : j + query->anchor_offset;
4469 }
4470 else {
4471 startpos = j + query->anchor_offset;
4472 }
4473 break;
4474 }
4475 }
4476 }
4477 else if (query->position < 0) {
4478 startpos = -query->position > ptrarray_size(&matches) ?
4479 0 : ptrarray_size(&matches) + query->position;
4480 }
4481 else startpos = query->position;
4482
4483 /* Build result list */
4484 size_t i;
4485 struct buf buf = BUF_INITIALIZER;
4486 for (i = startpos; i < (size_t) ptrarray_size(&matches); i++) {
4487 if (query->have_limit && json_array_size(query->ids) >= query->limit) {
4488 break;
4489 }
4490 struct eventquery_match *m = ptrarray_nth(&matches, i);
4491 const char *id;
4492 if (m->recurid) {
4493 buf_setcstr(&buf, m->ical_uid);
4494 buf_putc(&buf, ';');
4495 buf_appendcstr(&buf, m->recurid);
4496 id = buf_cstring(&buf);
4497 }
4498 else id = m->ical_uid;
4499 json_array_append_new(query->ids, json_string(id));
4500 }
4501 buf_free(&buf);
4502
4503 done:
4504 if (db) caldav_close(db);
4505 if (ptrarray_size(&matches)) {
4506 int j;
4507 for (j = 0; j < ptrarray_size(&matches); j++) {
4508 struct eventquery_match *match = ptrarray_nth(&matches, j);
4509 eventquery_match_free(&match);
4510 }
4511 }
4512 ptrarray_fini(&matches);
4513 free(sort);
4514 return r;
4515 }
4516
validatefilter(jmap_req_t * req,struct jmap_parser * parser,json_t * filter,json_t * unsupported,void * rock,json_t ** err)4517 static void validatefilter(jmap_req_t *req __attribute__((unused)),
4518 struct jmap_parser *parser,
4519 json_t *filter,
4520 json_t *unsupported __attribute__((unused)),
4521 void *rock __attribute__((unused)),
4522 json_t **err __attribute__((unused)))
4523 {
4524 const char *field;
4525 json_t *arg;
4526
4527 json_object_foreach(filter, field, arg) {
4528 if (!strcmp(field, "inCalendars")) {
4529 if (!(json_is_array(arg) && json_array_size(arg))) {
4530 jmap_parser_invalid(parser, field);
4531 }
4532 else {
4533 size_t i;
4534 json_t *uid;
4535 json_array_foreach(arg, i, uid) {
4536 const char *id = json_string_value(uid);
4537 if (!id || id[0] == '#') {
4538 jmap_parser_push_index(parser, field, i, id);
4539 jmap_parser_invalid(parser, NULL);
4540 jmap_parser_pop(parser);
4541 }
4542 }
4543 }
4544 }
4545 else if (!strcmp(field, "before") ||
4546 !strcmp(field, "after")) {
4547 const char *s;
4548 if ((s = json_string_value(arg))) {
4549 int is_valid = 0;
4550 size_t len = strlen(s);
4551 if (len && s[len-1] == 'Z') {
4552 /* Validate UTCDateTime */
4553 struct tm tm;
4554 memset(&tm, 0, sizeof(struct tm));
4555 tm.tm_isdst = -1;
4556 const char *p = strptime(s, "%Y-%m-%dT%H:%M:%S", &tm);
4557 is_valid = p && *p == 'Z';
4558 }
4559 if (!is_valid) jmap_parser_invalid(parser, field);
4560 }
4561 else jmap_parser_invalid(parser, field);
4562 }
4563 else if (!strcmp(field, "text") ||
4564 !strcmp(field, "title") ||
4565 !strcmp(field, "description") ||
4566 !strcmp(field, "location") ||
4567 !strcmp(field, "uid") ||
4568 !strcmp(field, "owner") ||
4569 !strcmp(field, "attendee")) {
4570 if (!json_is_string(arg)) {
4571 jmap_parser_invalid(parser, field);
4572 }
4573 }
4574 else {
4575 jmap_parser_invalid(parser, field);
4576 }
4577 }
4578 }
4579
validatecomparator(jmap_req_t * req,struct jmap_comparator * comp,void * rock,json_t ** err)4580 static int validatecomparator(jmap_req_t *req __attribute__((unused)),
4581 struct jmap_comparator *comp,
4582 void *rock __attribute__((unused)),
4583 json_t **err __attribute__((unused)))
4584 {
4585 /* Reject any collation */
4586 if (comp->collation) {
4587 return 0;
4588 }
4589 if (!strcmp(comp->property, "start") ||
4590 !strcmp(comp->property, "uid")) {
4591 return 1;
4592 }
4593 return 0;
4594 }
4595
_calendarevent_queryargs_parse(jmap_req_t * req,struct jmap_parser * parser,const char * argname,json_t * argval,void * rock)4596 static int _calendarevent_queryargs_parse(jmap_req_t *req __attribute__((unused)),
4597 struct jmap_parser *parser __attribute__((unused)),
4598 const char *argname,
4599 json_t *argval,
4600 void *rock)
4601 {
4602 if (strcmp(argname, "expandRecurrences")) return 0;
4603
4604 if (json_is_boolean(argval)) {
4605 int *expandrecur = rock;
4606 *expandrecur = json_boolean_value(argval);
4607 }
4608 else {
4609 jmap_parser_invalid(parser, argname);
4610 }
4611 return 1;
4612 }
4613
jmap_calendarevent_query(struct jmap_req * req)4614 static int jmap_calendarevent_query(struct jmap_req *req)
4615 {
4616 struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
4617 struct jmap_query query;
4618 int expandrecur = 0;
4619
4620 /* Parse request */
4621 json_t *err = NULL;
4622 jmap_query_parse(req, &parser,
4623 _calendarevent_queryargs_parse, &expandrecur,
4624 validatefilter, NULL,
4625 validatecomparator, NULL,
4626 &query, &err);
4627 if (err) {
4628 jmap_error(req, err);
4629 goto done;
4630 }
4631 if (json_array_size(parser.invalid)) {
4632 err = json_pack("{s:s}", "type", "invalidArguments");
4633 json_object_set(err, "arguments", parser.invalid);
4634 jmap_error(req, err);
4635 goto done;
4636 }
4637
4638 int r = eventquery_run(req, &query, expandrecur, &err);
4639 if (r || err) {
4640 if (!err) err = jmap_server_error(r);
4641 jmap_error(req, err);
4642 goto done;
4643 }
4644
4645 /* Build response */
4646 json_t *jstate = jmap_getstate(req, MBTYPE_CALENDAR, /*refresh*/0);
4647 query.query_state = xstrdup(json_string_value(jstate));
4648 json_decref(jstate);
4649
4650 json_t *res = jmap_query_reply(&query);
4651 jmap_ok(req, res);
4652
4653 done:
4654 jmap_query_fini(&query);
4655 jmap_parser_fini(&parser);
4656 return 0;
4657 }
4658
_calendarevent_copy(jmap_req_t * req,json_t * jevent,struct caldav_db * src_db,struct caldav_db * dst_db,json_t ** new_event,json_t ** set_err)4659 static void _calendarevent_copy(jmap_req_t *req,
4660 json_t *jevent,
4661 struct caldav_db *src_db,
4662 struct caldav_db *dst_db,
4663 json_t **new_event,
4664 json_t **set_err)
4665 {
4666 struct jmap_parser myparser = JMAP_PARSER_INITIALIZER;
4667 icalcomponent *src_ical = NULL;
4668 json_t *dst_event = NULL;
4669 struct mailbox *src_mbox = NULL;
4670 strarray_t schedule_addresses = STRARRAY_INITIALIZER;
4671 int r = 0;
4672
4673 /* Read mandatory properties */
4674 const char *src_id = json_string_value(json_object_get(jevent, "id"));
4675 const char *dst_calendar_id = json_string_value(json_object_get(jevent, "calendarId"));
4676 if (!src_id) {
4677 jmap_parser_invalid(&myparser, "id");
4678 }
4679 if (!dst_calendar_id) {
4680 jmap_parser_invalid(&myparser, "calendarId");
4681 }
4682 if (json_array_size(myparser.invalid)) {
4683 *set_err = json_pack("{s:s s:O}", "type", "invalidProperties",
4684 "properties", myparser.invalid);
4685 goto done;
4686 }
4687
4688 /* Lookup event */
4689 struct caldav_data *cdata = NULL;
4690 r = caldav_lookup_uid(src_db, src_id, &cdata);
4691 if (r && r != CYRUSDB_NOTFOUND) {
4692 syslog(LOG_ERR, "caldav_lookup_uid(%s) failed: %s", src_id, error_message(r));
4693 goto done;
4694 }
4695 if (r == CYRUSDB_NOTFOUND || !cdata->dav.alive || !cdata->dav.rowid ||
4696 !cdata->dav.imap_uid || cdata->comp_type != CAL_COMP_VEVENT) {
4697 *set_err = json_pack("{s:s}", "type", "notFound");
4698 goto done;
4699 }
4700 if (!jmap_hasrights(req, cdata->dav.mailbox, JACL_READITEMS)) {
4701 *set_err = json_pack("{s:s}", "type", "notFound");
4702 goto done;
4703 }
4704
4705 /* Read source event */
4706 r = jmap_openmbox(req, cdata->dav.mailbox, &src_mbox, /*rw*/0);
4707 if (r) goto done;
4708 src_ical = caldav_record_to_ical(src_mbox, cdata, httpd_userid, &schedule_addresses);
4709 if (!src_ical) {
4710 syslog(LOG_ERR, "calendarevent_copy: can't convert %s to JMAP", src_id);
4711 r = IMAP_INTERNAL;
4712 goto done;
4713 }
4714
4715 /* Patch JMAP event */
4716 json_t *src_event = jmapical_tojmap(src_ical, NULL);
4717 if (src_event) {
4718 dst_event = jmap_patchobject_apply(src_event, jevent, NULL);
4719 }
4720 json_decref(src_event);
4721 if (!dst_event) {
4722 syslog(LOG_ERR, "calendarevent_copy: can't convert to ical: %s", src_id);
4723 r = IMAP_INTERNAL;
4724 goto done;
4725 }
4726
4727 /* Create event */
4728 json_t *invalid = json_array();
4729 *new_event = json_pack("{}");
4730 r = setcalendarevents_create(req, req->accountid, dst_event,
4731 dst_db, invalid, *new_event);
4732 if (r || json_array_size(invalid)) {
4733 if (!r) {
4734 *set_err = json_pack("{s:s s:o}", "type", "invalidProperties",
4735 "properties", invalid);
4736 }
4737 goto done;
4738 }
4739 json_decref(invalid);
4740 json_object_set(*new_event, "id", json_object_get(*new_event, "uid"));
4741
4742 done:
4743 if (r && *set_err == NULL) {
4744 if (r == CYRUSDB_NOTFOUND)
4745 *set_err = json_pack("{s:s}", "type", "notFound");
4746 else
4747 *set_err = jmap_server_error(r);
4748 return;
4749 }
4750 jmap_closembox(req, &src_mbox);
4751 strarray_fini(&schedule_addresses);
4752 if (src_ical) icalcomponent_free(src_ical);
4753 json_decref(dst_event);
4754 jmap_parser_fini(&myparser);
4755 }
4756
jmap_calendarevent_copy(struct jmap_req * req)4757 static int jmap_calendarevent_copy(struct jmap_req *req)
4758 {
4759 struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
4760 struct jmap_copy copy;
4761 json_t *err = NULL;
4762 struct caldav_db *src_db = NULL;
4763 struct caldav_db *dst_db = NULL;
4764 json_t *destroy_events = json_array();
4765
4766 /* Parse request */
4767 jmap_copy_parse(req, &parser, NULL, NULL, ©, &err);
4768 if (err) {
4769 jmap_error(req, err);
4770 goto done;
4771 }
4772
4773 src_db = caldav_open_userid(copy.from_account_id);
4774 if (!src_db) {
4775 jmap_error(req, json_pack("{s:s}", "type", "fromAccountNotFound"));
4776 goto done;
4777 }
4778 dst_db = caldav_open_userid(req->accountid);
4779 if (!dst_db) {
4780 jmap_error(req, json_pack("{s:s}", "type", "toAccountNotFound"));
4781 goto done;
4782 }
4783
4784 /* Process request */
4785 const char *creation_id;
4786 json_t *jevent;
4787 json_object_foreach(copy.create, creation_id, jevent) {
4788 /* Copy event */
4789 json_t *set_err = NULL;
4790 json_t *new_event = NULL;
4791
4792 _calendarevent_copy(req, jevent, src_db, dst_db, &new_event, &set_err);
4793 if (set_err) {
4794 json_object_set_new(copy.not_created, creation_id, set_err);
4795 continue;
4796 }
4797
4798 // copy the ID for later deletion
4799 json_array_append(destroy_events, json_object_get(jevent, "id"));
4800
4801 /* Report event as created */
4802 json_object_set_new(copy.created, creation_id, new_event);
4803 const char *event_id = json_string_value(json_object_get(new_event, "id"));
4804 jmap_add_id(req, creation_id, event_id);
4805 }
4806
4807 /* Build response */
4808 jmap_ok(req, jmap_copy_reply(©));
4809
4810 /* Destroy originals, if requested */
4811 if (copy.on_success_destroy_original && json_array_size(destroy_events)) {
4812 json_t *subargs = json_object();
4813 json_object_set(subargs, "destroy", destroy_events);
4814 json_object_set_new(subargs, "accountId", json_string(copy.from_account_id));
4815 jmap_add_subreq(req, "CalendarEvent/set", subargs, NULL);
4816 }
4817
4818 done:
4819 json_decref(destroy_events);
4820 if (src_db) caldav_close(src_db);
4821 if (dst_db) caldav_close(dst_db);
4822 jmap_parser_fini(&parser);
4823 jmap_copy_fini(©);
4824 return 0;
4825 }
4826