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, &timestamp) < 0) {
1979                 json_array_append_new(rock->get->not_found, json_string(eid->raw));
1980                 continue;
1981             }
1982             icaltimetype icalrecurid = jmapical_datetime_to_icaltime(&timestamp, 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(&timestamp, &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, &copy, &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(&copy));
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(&copy);
4824     return 0;
4825 }
4826