Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
103 KB
Referenced Files
None
Subscribers
None
diff --git a/imap/jmap_calendar.c b/imap/jmap_calendar.c
index f415a77fd..7b9089455 100644
--- a/imap/jmap_calendar.c
+++ b/imap/jmap_calendar.c
@@ -1,2998 +1,2998 @@
/* jmap_calendar.c -- Routines for handling JMAP calendar messages
*
* Copyright (c) 1994-2014 Carnegie Mellon University. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The name "Carnegie Mellon University" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For permission or any legal
* details, please contact
* Carnegie Mellon University
* Center for Technology Transfer and Enterprise Creation
* 4615 Forbes Avenue
* Suite 302
* Pittsburgh, PA 15213
* (412) 268-7393, fax: (412) 268-7395
* innovation@andrew.cmu.edu
*
* 4. Redistributions of any form whatsoever must retain the following
* acknowledgment:
* "This product includes software developed by Computing Services
* at Carnegie Mellon University (http://www.cmu.edu/computing/)."
*
* CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
* THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
* FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#include <config.h>
#include <ctype.h>
#include <assert.h>
#include <string.h>
#include <syslog.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include "annotate.h"
#include "caldav_db.h"
#include "global.h"
#include "hash.h"
#include "httpd.h"
#include "http_caldav.h"
#include "http_caldav_sched.h"
#include "http_dav.h"
#include "http_jmap.h"
#include "http_proxy.h"
#include "ical_support.h"
#include "json_support.h"
#include "jmap_ical.h"
#include "search_query.h"
#include "stristr.h"
#include "times.h"
#include "util.h"
#include "xmalloc.h"
/* generated headers are not necessarily in current directory */
#include "imap/http_err.h"
#include "imap/imap_err.h"
static int getCalendars(struct jmap_req *req);
static int getCalendarsUpdates(struct jmap_req *req);
static int setCalendars(struct jmap_req *req);
static int getCalendarEvents(struct jmap_req *req);
static int getCalendarEventsUpdates(struct jmap_req *req);
static int getCalendarEventsList(struct jmap_req *req);
static int setCalendarEvents(struct jmap_req *req);
static int getCalendarPreferences(struct jmap_req *req);
jmap_method_t jmap_calendar_methods[] = {
{ "Calendar/get", &getCalendars },
{ "Calendar/changes", &getCalendarsUpdates },
{ "Calendar/set", &setCalendars },
{ "CalendarEvent/get", &getCalendarEvents },
{ "CalendarEvent/changes", &getCalendarEventsUpdates },
{ "CalendarEvent/query", &getCalendarEventsList },
{ "CalendarEvent/set", &setCalendarEvents },
{ "CalendarPreference/get", &getCalendarPreferences },
{ NULL, NULL}
};
int jmap_calendar_init(hash_table *methods, json_t *capabilities __attribute__((unused)))
{
jmap_method_t *mp;
for (mp = jmap_calendar_methods; mp->name; mp++) {
hash_insert(mp->name, mp, methods);
}
return 0;
}
static int _wantprop(hash_table *props, const char *name)
{
if (!props) return 1;
if (hash_lookup(name, props)) return 1;
return 0;
}
static int JNOTNULL(json_t *item)
{
if (!item) return 0;
if (json_is_null(item)) return 0;
return 1;
}
static int readprop_full(json_t *root,
const char *prefix,
const char *name,
int mandatory,
json_t *invalid,
const char *fmt,
void *dst)
{
int r = 0;
json_t *jval = json_object_get(root, name);
if (!jval && mandatory) {
r = -1;
} else if (jval) {
json_error_t err;
if (json_unpack_ex(jval, &err, 0, fmt, dst)) {
r = -2;
} else {
r = 1;
}
}
if (r < 0 && prefix) {
struct buf buf = BUF_INITIALIZER;
buf_printf(&buf, "%s.%s", prefix, name);
json_array_append_new(invalid, json_string(buf_cstring(&buf)));
buf_free(&buf);
} else if (r < 0) {
json_array_append_new(invalid, json_string(name));
}
return r;
}
#define readprop(root, name, mandatory, invalid, fmt, dst) \
readprop_full((root), NULL, (name), (mandatory), (invalid), (fmt), (dst))
/* Helper flags for setCalendarEvents */
#define JMAP_CREATE (1<<0) /* Current request is a create. */
#define JMAP_UPDATE (1<<1) /* Current request is an update. */
#define JMAP_DESTROY (1<<2) /* Current request is a destroy. */
/* Return a non-zero value if uid maps to a special-purpose calendar mailbox,
* that may not be read or modified by the user. */
static int jmap_calendar_isspecial(mbname_t *mbname) {
if (!mboxname_iscalendarmailbox(mbname_intname(mbname), 0)) return 1;
const strarray_t *boxes = mbname_boxes(mbname);
const char *lastname = strarray_nth(boxes, boxes->count - 1);
/* Don't return user.foo.#calendars */
if (!strcmp(lastname, config_getstring(IMAPOPT_CALENDARPREFIX))) {
return 1;
}
/* SCHED_INBOX and SCHED_OUTBOX end in "/", so trim them */
if (!strncmp(lastname, SCHED_INBOX, strlen(SCHED_INBOX)-1)) return 1;
if (!strncmp(lastname, SCHED_OUTBOX, strlen(SCHED_OUTBOX)-1)) return 1;
if (!strncmp(lastname, MANAGED_ATTACH, strlen(MANAGED_ATTACH)-1)) return 1;
return 0;
}
struct getcalendars_rock {
struct jmap_req *req;
json_t *found;
struct hash_table *props;
struct mailbox *mailbox;
int skip_hidden;
};
static int getcalendars_cb(const mbentry_t *mbentry, void *vrock)
{
struct getcalendars_rock *rock = vrock;
mbname_t *mbname = NULL;
int r = 0;
/* Only calendars... */
if (!(mbentry->mbtype & MBTYPE_CALENDAR)) return 0;
/* ...which are at least readable or visible... */
int rights = jmap_myrights(rock->req, mbentry);
if ((rights & DACL_READ) != DACL_READ) {
return rock->skip_hidden ? 0 : IMAP_PERMISSION_DENIED;
}
/* ...and contain VEVENTs. */
struct buf attrib = BUF_INITIALIZER;
static const char *calcompset_annot =
DAV_ANNOT_NS "<" XML_NS_CALDAV ">supported-calendar-component-set";
unsigned long supported_components = -1; /* ALL component types by default. */
r = annotatemore_lookupmask(mbentry->name, calcompset_annot,
rock->req->accountid, &attrib);
if (attrib.len) {
supported_components = strtoul(buf_cstring(&attrib), NULL, 10);
buf_free(&attrib);
}
if (!(supported_components & CAL_COMP_VEVENT)) {
goto done;
}
/* OK, we want this one... */
mbname = mbname_from_intname(mbentry->name);
/* ...unless it's one of the special names. */
if (jmap_calendar_isspecial(mbname)) {
r = 0;
goto done;
}
json_t *obj = json_pack("{}");
const strarray_t *boxes = mbname_boxes(mbname);
const char *id = strarray_nth(boxes, boxes->count-1);
json_object_set_new(obj, "id", json_string(id));
if (_wantprop(rock->props, "x-href")) {
// FIXME - should the x-ref for a shared calendar point
// to the authenticated user's calendar home?
char *xhref = jmap_xhref(mbentry->name, NULL);
json_object_set_new(obj, "x-href", json_string(xhref));
free(xhref);
}
if (_wantprop(rock->props, "name")) {
struct buf attrib = BUF_INITIALIZER;
static const char *displayname_annot =
DAV_ANNOT_NS "<" XML_NS_DAV ">displayname";
r = annotatemore_lookupmask(mbentry->name, displayname_annot,
httpd_userid, &attrib);
/* fall back to last part of mailbox name */
if (r || !attrib.len) buf_setcstr(&attrib, id);
json_object_set_new(obj, "name", json_string(buf_cstring(&attrib)));
buf_free(&attrib);
}
if (_wantprop(rock->props, "color")) {
struct buf attrib = BUF_INITIALIZER;
static const char *color_annot =
DAV_ANNOT_NS "<" XML_NS_APPLE ">calendar-color";
r = annotatemore_lookupmask(mbentry->name, color_annot,
httpd_userid, &attrib);
if (!r && attrib.len)
json_object_set_new(obj, "color", json_string(buf_cstring(&attrib)));
buf_free(&attrib);
}
if (_wantprop(rock->props, "sortOrder")) {
struct buf attrib = BUF_INITIALIZER;
static const char *order_annot =
DAV_ANNOT_NS "<" XML_NS_APPLE ">calendar-order";
r = annotatemore_lookupmask(mbentry->name, order_annot,
httpd_userid, &attrib);
if (!r && attrib.len) {
char *ptr;
long val = strtol(buf_cstring(&attrib), &ptr, 10);
if (ptr && *ptr == '\0') {
json_object_set_new(obj, "sortOrder", json_integer(val));
}
else {
/* Ignore, but report non-numeric calendar-order values */
syslog(LOG_WARNING, "sortOrder: strtol(%s) failed",
buf_cstring(&attrib));
}
}
buf_free(&attrib);
}
if (_wantprop(rock->props, "isVisible")) {
struct buf attrib = BUF_INITIALIZER;
static const char *color_annot =
DAV_ANNOT_NS "<" XML_NS_CALDAV ">X-FM-isVisible";
r = annotatemore_lookupmask(mbentry->name, color_annot,
httpd_userid, &attrib);
if (!r && attrib.len) {
const char *val = buf_cstring(&attrib);
if (!strncmp(val, "true", 4) || !strncmp(val, "1", 1)) {
json_object_set_new(obj, "isVisible", json_true());
} else if (!strncmp(val, "false", 5) || !strncmp(val, "0", 1)) {
json_object_set_new(obj, "isVisible", json_false());
} else {
/* Report invalid value and fall back to default. */
syslog(LOG_WARNING,
"isVisible: invalid annotation value: %s", val);
json_object_set_new(obj, "isVisible", json_string("true"));
}
}
buf_free(&attrib);
}
if (_wantprop(rock->props, "mayReadFreeBusy")) {
json_object_set_new(obj, "mayReadFreeBusy",
rights & DACL_READFB ? json_true() : json_false());
}
if (_wantprop(rock->props, "mayReadItems")) {
json_object_set_new(obj, "mayReadItems",
rights & DACL_READ ? json_true() : json_false());
}
if (_wantprop(rock->props, "mayAddItems")) {
json_object_set_new(obj, "mayAddItems",
rights & DACL_WRITECONT ? json_true() : json_false());
}
if (_wantprop(rock->props, "mayModifyItems")) {
json_object_set_new(obj, "mayModifyItems",
rights & DACL_WRITECONT ? json_true() : json_false());
}
if (_wantprop(rock->props, "mayRemoveItems")) {
json_object_set_new(obj, "mayRemoveItems",
rights & DACL_RMRSRC ? json_true() : json_false());
}
if (_wantprop(rock->props, "mayRename")) {
json_object_set_new(obj, "mayRename",
rights & DACL_RMCOL ? json_true() : json_false());
}
if (_wantprop(rock->props, "mayDelete")) {
json_object_set_new(obj, "mayDelete",
rights & DACL_RMCOL ? json_true() : json_false());
}
json_array_append_new(rock->found, obj);
done:
mbname_free(&mbname);
return r;
}
static int getCalendars(struct jmap_req *req)
{
struct getcalendars_rock rock = {
req,
json_pack("[]") /*found*/,
NULL /*props*/,
NULL /*mailbox*/,
1 /*skiphidden */
};
int r = 0;
r = caldav_create_defaultcalendars(req->accountid);
if (r == IMAP_MAILBOX_NONEXISTENT) {
/* The account exists but does not have a root mailbox. */
json_t *err = json_pack("{s:s}", "type", "accountNoCalendars");
json_array_append_new(req->response, json_pack("[s,o,s]",
"error", err, req->tag));
return 0;
} else if (r) return r;
json_t *properties = json_object_get(req->args, "properties");
if (properties && json_array_size(properties)) {
rock.props = xzmalloc(sizeof(struct hash_table));
construct_hash_table(rock.props, json_array_size(properties), 0);
int i;
int size = json_array_size(properties);
for (i = 0; i < size; i++) {
const char *id = json_string_value(json_array_get(properties, i));
if (id == NULL) continue;
/* 1 == properties */
hash_insert(id, (void *)1, rock.props);
}
}
json_t *want = json_object_get(req->args, "ids");
json_t *notfound = json_array();
if (want) {
size_t i;
json_t *jval;
rock.skip_hidden = 0; /* complain about missing ACL rights */
json_array_foreach(want, i, jval) {
const char *id = json_string_value(jval);
/* FIXME - this should probably done everywhere */
if (!id) {
json_t *err = json_pack("{s:s, s:[s]}",
"type", "invalidArguments", "arguments", "ids");
json_array_append_new(req->response, json_pack("[s,o,s]",
"error", err, req->tag));
r = 0;
goto done;
}
if (id && id[0] == '#') {
id = hash_lookup(id + 1, &req->idmap->calendars);
}
if (!id) {
json_array_append(notfound, jval);
continue;
}
char *mboxname = caldav_mboxname(req->accountid, id);
mbentry_t *mbentry = NULL;
r = mboxlist_lookup(mboxname, &mbentry, NULL);
if (r == IMAP_NOTFOUND || !mbentry) {
json_array_append(notfound, jval);
r = 0;
goto doneloop;
}
r = getcalendars_cb(mbentry, &rock);
if (r == IMAP_PERMISSION_DENIED) {
json_array_append(notfound, jval);
r = 0;
goto doneloop;
}
doneloop:
if (mbentry) mboxlist_entry_free(&mbentry);
free(mboxname);
if (r) goto done;
}
}
else {
r = jmap_mboxlist(req, &getcalendars_cb, &rock);
if (r) goto done;
}
json_t *calendars = json_pack("{}");
json_incref(rock.found);
json_object_set_new(calendars, "accountId", json_string(req->accountid));
json_object_set_new(calendars, "state", jmap_getstate(req, MBTYPE_CALENDAR));
json_object_set_new(calendars, "list", rock.found);
if (json_array_size(notfound)) {
json_object_set_new(calendars, "notFound", notfound);
}
else {
json_decref(notfound);
json_object_set_new(calendars, "notFound", json_null());
}
json_t *item = json_pack("[]");
json_array_append_new(item, json_string("Calendar/get"));
json_array_append_new(item, calendars);
json_array_append_new(item, json_string(req->tag));
json_array_append_new(req->response, item);
done:
if (rock.props) {
free_hash_table(rock.props, NULL);
free(rock.props);
}
json_decref(rock.found);
return r;
}
struct calendarupdates_rock {
jmap_req_t *req;
modseq_t oldmodseq;
json_t *changed;
json_t *removed;
};
static int getcalendarupdates_cb(const mbentry_t *mbentry, void *vrock)
{
struct calendarupdates_rock *rock = (struct calendarupdates_rock *) vrock;
mbname_t *mbname = NULL;
jmap_req_t *req = rock->req;
int r = 0;
/* Ignore old changes. */
if (mbentry->foldermodseq <= rock->oldmodseq) {
goto done;
}
/* Ignore mailboxes that are hidden from us */
int rights = jmap_myrights(req, mbentry);
if (!(rights & DACL_READ)) return 0;
/* Ignore any mailboxes that aren't (possibly deleted) calendars. */
if (!mboxname_iscalendarmailbox(mbentry->name, mbentry->mbtype))
return 0;
/* Ignore special-purpose calendar mailboxes. */
mbname = mbname_from_intname(mbentry->name);
if (jmap_calendar_isspecial(mbname)) {
goto done;
}
/* Ignore calendars that don't store VEVENTs */
struct buf attrib = BUF_INITIALIZER;
static const char *calcompset_annot =
DAV_ANNOT_NS "<" XML_NS_CALDAV ">supported-calendar-component-set";
unsigned long supported_components = -1; /* ALL component types by default. */
r = annotatemore_lookupmask(mbentry->name, calcompset_annot,
rock->req->accountid, &attrib);
if (attrib.len) {
supported_components = strtoul(buf_cstring(&attrib), NULL, 10);
buf_free(&attrib);
}
if (!(supported_components & CAL_COMP_VEVENT)) {
goto done;
}
const strarray_t *boxes = mbname_boxes(mbname);
const char *id = strarray_nth(boxes, boxes->count-1);
/* Report this calendar as changed or removed. */
if (mbentry->mbtype & MBTYPE_CALENDAR) {
json_array_append_new(rock->changed, json_string(id));
} else if (mbentry->mbtype & MBTYPE_DELETED) {
json_array_append_new(rock->removed, json_string(id));
}
done:
mbname_free(&mbname);
return r;
}
static int getCalendarsUpdates(struct jmap_req *req)
{
int r, pe;
json_t *invalid;
const char *since = NULL;
int dofetch = 0;
struct buf buf = BUF_INITIALIZER;
modseq_t oldmodseq = 0;
r = caldav_create_defaultcalendars(req->accountid);
if (r == IMAP_MAILBOX_NONEXISTENT) {
/* The account exists but does not have a root mailbox. */
json_t *err = json_pack("{s:s}", "type", "accountNoCalendars");
json_array_append_new(req->response, json_pack("[s,o,s]",
"error", err, req->tag));
return 0;
} else if (r) return r;
/* Parse and validate arguments. */
invalid = json_pack("[]");
pe = readprop(req->args, "sinceState", 1 /*mandatory*/, invalid, "s", &since);
if (pe > 0) {
oldmodseq = atomodseq_t(since);
if (!oldmodseq) {
json_array_append_new(invalid, json_string("sinceState"));
}
}
readprop(req->args, "fetchRecords", 0 /*mandatory*/, invalid, "b", &dofetch);
if (json_array_size(invalid)) {
json_t *err = json_pack("{s:s, s:o}",
"type", "invalidArguments", "arguments", invalid);
json_array_append_new(req->response,
json_pack("[s,o,s]", "error", err, req->tag));
r = 0;
goto done;
}
json_decref(invalid);
/* Lookup any updates. */
char *mboxname = caldav_mboxname(req->accountid, NULL);
struct calendarupdates_rock rock = {
req,
oldmodseq,
json_pack("[]"),
json_pack("[]")
};
r = mboxlist_mboxtree(mboxname, getcalendarupdates_cb, &rock,
MBOXTREE_TOMBSTONES|MBOXTREE_SKIP_ROOT);
free(mboxname);
if (r) {
json_t *err = json_pack("{s:s}", "type", "cannotCalculateChanges");
json_array_append_new(req->response,
json_pack("[s,o,s]", "error", err, req->tag));
json_decref(rock.changed);
json_decref(rock.removed);
goto done;
}
/* Create response. */
json_t *calendarUpdates = json_pack("{}");
json_object_set_new(calendarUpdates, "accountId",
json_string(req->accountid));
json_object_set_new(calendarUpdates, "oldState", json_string(since));
json_object_set_new(calendarUpdates, "newState",
jmap_getstate(req, MBTYPE_CALENDAR));
json_object_set_new(calendarUpdates, "changed", rock.changed);
json_object_set_new(calendarUpdates, "removed", rock.removed);
json_t *item = json_pack("[]");
json_array_append_new(item, json_string("Calendar/changes"));
json_array_append_new(item, calendarUpdates);
json_array_append_new(item, json_string(req->tag));
json_array_append_new(req->response, item);
if (dofetch) {
struct jmap_req subreq = *req; // struct copy, woot
subreq.args = json_pack("{}");
json_object_set(subreq.args, "ids", rock.changed);
r = getCalendars(&subreq);
json_decref(subreq.args);
}
done:
buf_free(&buf);
return r;
}
/* jmap calendar APIs */
/* Update the calendar properties in the calendar mailbox named mboxname.
* NULL values and negative integers are ignored. Return 0 on success. */
static int setcalendars_update(jmap_req_t *req,
const char *mboxname,
const char *name,
const char *color,
int sortOrder,
int isVisible)
{
struct mailbox *mbox = NULL;
annotate_state_t *astate = NULL;
struct buf val = BUF_INITIALIZER;
int r;
int rights = jmap_myrights_byname(req, mboxname);
if (!(rights & DACL_READ)) {
return IMAP_MAILBOX_NONEXISTENT;
} else if (!(rights & DACL_WRITE)) {
return IMAP_PERMISSION_DENIED;
}
r = mailbox_open_iwl(mboxname, &mbox);
if (r) {
syslog(LOG_ERR, "mailbox_open_iwl(%s) failed: %s",
mboxname, error_message(r));
return r;
}
r = mailbox_get_annotate_state(mbox, 0, &astate);
if (r) {
syslog(LOG_ERR, "IOERROR: failed to open annotations %s: %s",
mbox->name, error_message(r));
}
/* name */
if (!r && name) {
buf_setcstr(&val, name);
static const char *displayname_annot =
DAV_ANNOT_NS "<" XML_NS_DAV ">displayname";
r = annotate_state_writemask(astate, displayname_annot,
httpd_userid, &val);
if (r) {
syslog(LOG_ERR, "failed to write annotation %s: %s",
displayname_annot, error_message(r));
}
buf_reset(&val);
}
/* color */
if (!r && color) {
buf_setcstr(&val, color);
static const char *color_annot =
DAV_ANNOT_NS "<" XML_NS_APPLE ">calendar-color";
r = annotate_state_writemask(astate, color_annot, httpd_userid, &val);
if (r) {
syslog(LOG_ERR, "failed to write annotation %s: %s",
color_annot, error_message(r));
}
buf_reset(&val);
}
/* sortOrder */
if (!r && sortOrder >= 0) {
buf_printf(&val, "%d", sortOrder);
static const char *sortOrder_annot =
DAV_ANNOT_NS "<" XML_NS_APPLE ">calendar-order";
r = annotate_state_writemask(astate, sortOrder_annot,
httpd_userid, &val);
if (r) {
syslog(LOG_ERR, "failed to write annotation %s: %s",
sortOrder_annot, error_message(r));
}
buf_reset(&val);
}
/* isVisible */
if (!r && isVisible >= 0) {
buf_setcstr(&val, isVisible ? "true" : "false");
static const char *sortOrder_annot =
DAV_ANNOT_NS "<" XML_NS_CALDAV ">X-FM-isVisible";
r = annotate_state_writemask(astate, sortOrder_annot,
httpd_userid, &val);
if (r) {
syslog(LOG_ERR, "failed to write annotation %s: %s",
sortOrder_annot, error_message(r));
}
buf_reset(&val);
}
buf_free(&val);
if (r) {
mailbox_abort(mbox);
}
mailbox_close(&mbox);
return r;
}
/* Delete the calendar mailbox named mboxname for the userid in req. */
static int setcalendars_destroy(jmap_req_t *req, const char *mboxname)
{
int r, rights;
rights = jmap_myrights_byname(req, mboxname);
if (!(rights & DACL_READ)) {
return IMAP_NOTFOUND;
} else if (!(rights & DACL_RMCOL)) {
return IMAP_PERMISSION_DENIED;
}
struct caldav_db *db = caldav_open_userid(req->userid);
if (!db) {
syslog(LOG_ERR, "caldav_open_mailbox failed for user %s", req->userid);
return IMAP_INTERNAL;
}
/* XXX
* JMAP spec says that: "A calendar MAY be deleted that is currently
* associated with one or more events. In this case, the events belonging
* to this calendar MUST also be deleted. Conceptually, this MUST happen
* prior to the calendar itself being deleted, and MUST generate a push
* event that modifies the calendarState for the account, and has a
* clientId of null, to indicate that a change has been made to the
* calendar data not explicitly requested by the client."
*
* Need the Events API for this requirement.
*/
r = caldav_delmbox(db, mboxname);
if (r) {
syslog(LOG_ERR, "failed to delete mailbox from caldav_db: %s",
error_message(r));
return r;
}
jmap_myrights_delete(req, mboxname);
struct mboxevent *mboxevent = mboxevent_new(EVENT_MAILBOX_DELETE);
if (mboxlist_delayed_delete_isenabled()) {
r = mboxlist_delayed_deletemailbox(mboxname,
httpd_userisadmin || httpd_userisproxyadmin,
httpd_userid, req->authstate, mboxevent,
1 /* checkacl */, 0 /* local_only */, 0 /* force */);
} else {
r = mboxlist_deletemailbox(mboxname,
httpd_userisadmin || httpd_userisproxyadmin,
httpd_userid, req->authstate, mboxevent,
1 /* checkacl */, 0 /* local_only */, 0 /* force */);
}
mboxevent_free(&mboxevent);
int rr = caldav_close(db);
if (!r) r = rr;
return r;
}
static int setCalendars(struct jmap_req *req)
{
int r = 0;
json_t *set = NULL;
json_t *state = json_object_get(req->args, "ifInState");
if (state && jmap_cmpstate(req, state, MBTYPE_CALENDAR)) {
json_array_append_new(req->response, json_pack("[s, {s:s}, s]",
"error", "type", "stateMismatch", req->tag));
goto done;
}
set = json_pack("{s:s}", "accountId", req->accountid);
json_object_set_new(set, "oldState", jmap_getstate(req, MBTYPE_CALENDAR));
r = caldav_create_defaultcalendars(req->accountid);
if (r == IMAP_MAILBOX_NONEXISTENT) {
/* The account exists but does not have a root mailbox. */
json_t *err = json_pack("{s:s}", "type", "accountNoCalendars");
json_array_append_new(req->response, json_pack("[s,o,s]",
"error", err, req->tag));
return 0;
} else if (r) return r;
json_t *create = json_object_get(req->args, "create");
if (create) {
json_t *created = json_pack("{}");
json_t *notCreated = json_pack("{}");
json_t *record;
const char *key;
json_t *arg;
json_object_foreach(create, key, arg) {
/* Validate calendar id. */
if (!strlen(key)) {
json_t *err= json_pack("{s:s}", "type", "invalidArguments");
json_object_set_new(notCreated, key, err);
continue;
}
/* Parse and validate properties. */
json_t *invalid = json_pack("[]");
const char *name = NULL;
const char *color = NULL;
int32_t sortOrder = -1;
int isVisible = 0;
int pe; /* parse error */
short flag;
/* Mandatory properties. */
pe = readprop(arg, "name", 1, invalid, "s", &name);
if (pe > 0 && strnlen(name, 256) == 256) {
json_array_append_new(invalid, json_string("name"));
}
readprop(arg, "color", 1, invalid, "s", &color);
pe = readprop(arg, "sortOrder", 1, invalid, "i", &sortOrder);
if (pe > 0 && sortOrder < 0) {
json_array_append_new(invalid, json_string("sortOrder"));
}
pe = readprop(arg, "isVisible", 1, invalid, "b", &isVisible);
if (pe > 0 && !isVisible) {
json_array_append_new(invalid, json_string("isVisible"));
}
/* Optional properties. If present, these MUST be set to true. */
flag = 1; readprop(arg, "mayReadFreeBusy", 0, invalid, "b", &flag);
if (!flag) {
json_array_append_new(invalid, json_string("mayReadFreeBusy"));
}
flag = 1; readprop(arg, "mayReadItems", 0, invalid, "b", &flag);
if (!flag) {
json_array_append_new(invalid, json_string("mayReadItems"));
}
flag = 1; readprop(arg, "mayAddItems", 0, invalid, "b", &flag);
if (!flag) {
json_array_append_new(invalid, json_string("mayAddItems"));
}
flag = 1; readprop(arg, "mayModifyItems", 0, invalid, "b", &flag);
if (!flag) {
json_array_append_new(invalid, json_string("mayModifyItems"));
}
flag = 1; readprop(arg, "mayRemoveItems", 0, invalid, "b", &flag);
if (!flag) {
json_array_append_new(invalid, json_string("mayRemoveItems"));
}
flag = 1; readprop(arg, "mayRename", 0, invalid, "b", &flag);
if (!flag) {
json_array_append_new(invalid, json_string("mayRename"));
}
flag = 1; readprop(arg, "mayDelete", 0, invalid, "b", &flag);
if (!flag) {
json_array_append_new(invalid, json_string("mayDelete"));
}
/* Report any property errors and bail out. */
if (json_array_size(invalid)) {
json_t *err = json_pack("{s:s, s:o}",
"type", "invalidProperties", "properties", invalid);
json_object_set_new(notCreated, key, err);
continue;
}
json_decref(invalid);
/* Prepare the ACL for this calendar */
struct buf acl = BUF_INITIALIZER;
if (strcmp(req->accountid, req->userid)) {
/* Make sure we are allowed to create the calendar */
char *parentname = caldav_mboxname(req->accountid, NULL);
mbentry_t *mbparent = NULL;
mboxlist_lookup(parentname, &mbparent, NULL);
free(parentname);
int rights = jmap_myrights(req, mbparent);
if (!(rights & DACL_MKCOL)) {
json_t *err = json_pack("{s:s}", "type", "accountReadOnly");
json_object_set_new(notCreated, key, err);
mboxlist_entry_free(&mbparent);
continue;
}
/* Copy the calendar home ACL for this shared calendar */
buf_setcstr(&acl, mbparent->acl);
mboxlist_entry_free(&mbparent);
} else {
/* Users may always create their own calendars */
char rights[100];
cyrus_acl_masktostr(DACL_ALL | DACL_READFB, rights);
buf_printf(&acl, "%s\t%s\t", httpd_userid, rights);
cyrus_acl_masktostr(DACL_READFB, rights);
buf_printf(&acl, "%s\t%s\t", "anyone", rights);
}
/* Create the calendar */
char *uid = xstrdup(makeuuid());
char *mboxname = caldav_mboxname(req->accountid, uid);
r = mboxlist_createsync(mboxname, MBTYPE_CALENDAR,
NULL /* partition */,
req->userid, req->authstate,
0 /* options */, 0 /* uidvalidity */,
0 /* highestmodseq */, buf_cstring(&acl),
NULL /* uniqueid */, 0 /* local_only */,
NULL /* mboxptr */);
buf_free(&acl);
if (r) {
syslog(LOG_ERR, "IOERROR: failed to create %s (%s)",
mboxname, error_message(r));
if (r == IMAP_PERMISSION_DENIED) {
json_t *err = json_pack("{s:s}", "type", "accountReadOnly");
json_object_set_new(notCreated, key, err);
}
free(mboxname);
goto done;
}
r = setcalendars_update(req, mboxname,
name, color, sortOrder, isVisible);
if (r) {
free(uid);
int rr = mboxlist_delete(mboxname);
if (rr) {
syslog(LOG_ERR, "could not delete mailbox %s: %s",
mboxname, error_message(rr));
}
free(mboxname);
goto done;
}
free(mboxname);
/* Report calendar as created. */
record = json_pack("{s:s}", "id", uid);
json_object_set_new(created, key, record);
/* hash_insert takes ownership of uid. */
hash_insert(key, uid, &req->idmap->calendars);
}
if (json_object_size(created)) {
json_object_set(set, "created", created);
}
json_decref(created);
if (json_object_size(notCreated)) {
json_object_set(set, "notCreated", notCreated);
}
json_decref(notCreated);
}
json_t *update = json_object_get(req->args, "update");
if (update) {
json_t *updated = json_pack("{}");
json_t *notUpdated = json_pack("{}");
const char *uid;
json_t *arg;
json_object_foreach(update, uid, arg) {
/* Validate uid */
if (!uid) {
continue;
}
if (uid && uid[0] == '#') {
const char *newuid =
hash_lookup(uid + 1, &req->idmap->calendars);
if (!newuid) {
json_t *err = json_pack("{s:s}", "type", "notFound");
json_object_set_new(notUpdated, uid, err);
continue;
}
uid = newuid;
}
/* Parse and validate properties. */
json_t *invalid = json_pack("[]");
const char *name = NULL;
const char *color = NULL;
int32_t sortOrder = -1;
int isVisible = -1;
int flag;
int pe = 0; /* parse error */
pe = readprop(arg, "name", 0, invalid, "s", &name);
if (pe > 0 && strnlen(name, 256) == 256) {
json_array_append_new(invalid, json_string("name"));
}
readprop(arg, "color", 0, invalid, "s", &color);
pe = readprop(arg, "sortOrder", 0, invalid, "i", &sortOrder);
if (pe > 0 && sortOrder < 0) {
json_array_append_new(invalid, json_string("sortOrder"));
}
readprop(arg, "isVisible", 0, invalid, "b", &isVisible);
/* The mayFoo properties are immutable and MUST NOT set. */
pe = readprop(arg, "mayReadFreeBusy", 0, invalid, "b", &flag);
if (pe > 0) {
json_array_append_new(invalid, json_string("mayReadFreeBusy"));
}
pe = readprop(arg, "mayReadItems", 0, invalid, "b", &flag);
if (pe > 0) {
json_array_append_new(invalid, json_string("mayReadItems"));
}
pe = readprop(arg, "mayAddItems", 0, invalid, "b", &flag);
if (pe > 0) {
json_array_append_new(invalid, json_string("mayAddItems"));
}
pe = readprop(arg, "mayModifyItems", 0, invalid, "b", &flag);
if (pe > 0) {
json_array_append_new(invalid, json_string("mayModifyItems"));
}
pe = readprop(arg, "mayRemoveItems", 0, invalid, "b", &flag);
if (pe > 0) {
json_array_append_new(invalid, json_string("mayRemoveItems"));
}
pe = readprop(arg, "mayRename", 0, invalid, "b", &flag);
if (pe > 0) {
json_array_append_new(invalid, json_string("mayRename"));
}
pe = readprop(arg, "mayDelete", 0, invalid, "b", &flag);
if (pe > 0) {
json_array_append_new(invalid, json_string("mayDelete"));
}
/* Report any property errors and bail out. */
if (json_array_size(invalid)) {
json_t *err = json_pack("{s:s, s:o}",
"type", "invalidProperties", "properties", invalid);
json_object_set_new(notUpdated, uid, err);
continue;
}
json_decref(invalid);
/* Make sure we don't mess up special calendars */
char *mboxname = caldav_mboxname(req->accountid, uid);
mbname_t *mbname = mbname_from_intname(mboxname);
if (!mbname || jmap_calendar_isspecial(mbname)) {
json_t *err = json_pack("{s:s}", "type", "notFound");
json_object_set_new(notUpdated, uid, err);
mbname_free(&mbname);
free(mboxname);
continue;
}
mbname_free(&mbname);
/* Update the calendar */
r = setcalendars_update(req, mboxname,
name, color, sortOrder, isVisible);
free(mboxname);
if (r == IMAP_NOTFOUND || r == IMAP_MAILBOX_NONEXISTENT) {
json_t *err = json_pack("{s:s}", "type", "notFound");
json_object_set_new(notUpdated, uid, err);
r = 0;
continue;
}
else if (r == IMAP_PERMISSION_DENIED) {
json_t *err = json_pack("{s:s}", "type", "accountReadOnly");
json_object_set_new(notUpdated, uid, err);
r = 0;
continue;
}
/* Report calendar as updated. */
json_object_set_new(updated, uid, json_null());
}
if (json_object_size(updated)) {
json_object_set(set, "updated", updated);
}
json_decref(updated);
if (json_object_size(notUpdated)) {
json_object_set(set, "notUpdated", notUpdated);
}
json_decref(notUpdated);
}
json_t *destroy = json_object_get(req->args, "destroy");
if (destroy) {
json_t *destroyed = json_pack("[]");
json_t *notDestroyed = json_pack("{}");
size_t index;
json_t *juid;
json_array_foreach(destroy, index, juid) {
/* Validate uid */
const char *uid = json_string_value(juid);
if (!uid) {
continue;
}
if (uid && uid[0] == '#') {
const char *newuid =
hash_lookup(uid + 1, &req->idmap->calendars);
if (!newuid) {
json_t *err = json_pack("{s:s}", "type", "notFound");
json_object_set_new(notDestroyed, uid, err);
continue;
}
uid = newuid;
}
/* Do not allow to remove the default calendar. */
char *mboxname = caldav_mboxname(req->accountid, NULL);
static const char *defaultcal_annot =
DAV_ANNOT_NS "<" XML_NS_CALDAV ">schedule-default-calendar";
struct buf attrib = BUF_INITIALIZER;
r = annotatemore_lookupmask(mboxname, defaultcal_annot,
req->accountid, &attrib);
free(mboxname);
const char *defaultcal = "Default";
if (!r && attrib.len) {
defaultcal = buf_cstring(&attrib);
}
if (!strcmp(uid, defaultcal)) {
/* XXX - The isDefault set error is not documented in the spec. */
json_t *err = json_pack("{s:s}", "type", "isDefault");
json_object_set_new(notDestroyed, uid, err);
buf_free(&attrib);
continue;
}
buf_free(&attrib);
/* Make sure we don't delete special calendars */
mboxname = caldav_mboxname(req->accountid, uid);
mbname_t *mbname = mbname_from_intname(mboxname);
if (!mbname || jmap_calendar_isspecial(mbname)) {
json_t *err = json_pack("{s:s}", "type", "notFound");
json_object_set_new(notDestroyed, uid, err);
mbname_free(&mbname);
free(mboxname);
continue;
}
mbname_free(&mbname);
/* Destroy calendar. */
r = setcalendars_destroy(req, mboxname);
free(mboxname);
if (r == IMAP_NOTFOUND || r == IMAP_MAILBOX_NONEXISTENT) {
json_t *err = json_pack("{s:s}", "type", "notFound");
json_object_set_new(notDestroyed, uid, err);
r = 0;
continue;
} else if (r == IMAP_PERMISSION_DENIED) {
json_t *err = json_pack("{s:s}", "type", "accountReadOnly");
json_object_set_new(notDestroyed, uid, err);
r = 0;
continue;
} else if (r) {
goto done;
}
/* Report calendar as destroyed. */
json_array_append_new(destroyed, json_string(uid));
}
if (json_array_size(destroyed)) {
json_object_set(set, "destroyed", destroyed);
}
json_decref(destroyed);
if (json_object_size(notDestroyed)) {
json_object_set(set, "notDestroyed", notDestroyed);
}
json_decref(notDestroyed);
}
/* Set newState field in calendarsSet. */
if (json_object_get(set, "created") ||
json_object_get(set, "updated") ||
json_object_get(set, "destroyed")) {
r = jmap_bumpstate(req, MBTYPE_CALENDAR);
if (r) goto done;
}
json_object_set_new(set, "newState", jmap_getstate(req, MBTYPE_CALENDAR));
json_incref(set);
json_t *item = json_pack("[]");
json_array_append_new(item, json_string("Calendar/set"));
json_array_append_new(item, set);
json_array_append_new(item, json_string(req->tag));
json_array_append_new(req->response, item);
done:
if (set) json_decref(set);
return r;
}
/* FIXME dup from jmapical.c */
/* Convert the JMAP local datetime in buf to tm time. Return non-zero on success. */
static int localdate_to_tm(const char *buf, struct tm *tm) {
/* Initialize tm. We don't know about daylight savings time here. */
memset(tm, 0, sizeof(struct tm));
tm->tm_isdst = -1;
/* Parse LocalDate. */
const char *p = strptime(buf, "%Y-%m-%dT%H:%M:%S", tm);
if (!p || *p) {
return 0;
}
return 1;
}
/* FIXME dup from jmapical.c */
static int localdate_to_icaltime(const char *buf,
icaltimetype *dt,
icaltimezone *tz,
int is_allday) {
struct tm tm;
int r;
char *s = NULL;
icaltimetype tmp;
int is_utc;
size_t n;
r = localdate_to_tm(buf, &tm);
if (!r) return 0;
if (is_allday && (tm.tm_sec || tm.tm_min || tm.tm_hour)) {
return 0;
}
is_utc = tz == icaltimezone_get_utc_timezone();
/* Can't use icaltime_from_timet_with_zone since it tries to convert
* t from UTC into tz. Let's feed ical a DATETIME string, instead. */
s = xcalloc(19, sizeof(char));
n = strftime(s, 18, "%Y%m%dT%H%M%S", &tm);
if (is_utc) {
s[n]='Z';
}
tmp = icaltime_from_string(s);
free(s);
if (icaltime_is_null_time(tmp)) {
return 0;
}
tmp.zone = tz;
tmp.is_date = is_allday;
*dt = tmp;
return 1;
}
/* FIXME dup from jmapical.c */
static int utcdate_to_icaltime(const char *src,
icaltimetype *dt)
{
struct buf buf = BUF_INITIALIZER;
size_t len = strlen(src);
int r;
icaltimezone *utc = icaltimezone_get_utc_timezone();
if (!len || src[len-1] != 'Z') {
return 0;
}
buf_setmap(&buf, src, len-1);
r = localdate_to_icaltime(buf_cstring(&buf), dt, utc, 0);
buf_free(&buf);
return r;
}
struct getcalendarevents_rock {
struct jmap_req *req;
json_t *found;
struct hash_table *props;
struct mailbox *mailbox;
int check_acl;
};
static int getcalendarevents_cb(void *vrock, struct caldav_data *cdata)
{
struct getcalendarevents_rock *rock = vrock;
int r = 0;
icalcomponent* ical = NULL;
json_t *obj, *jprops = NULL;
jmapical_err_t err;
jmap_req_t *req = rock->req;
if (!cdata->dav.alive) {
return 0;
}
/* Check mailbox ACL rights */
int rights = jmap_myrights_byname(req, cdata->dav.mailbox);
if (!(rights & DACL_READ))
return 0;
/* Open calendar mailbox. */
if (!rock->mailbox || strcmp(rock->mailbox->name, cdata->dav.mailbox)) {
mailbox_close(&rock->mailbox);
r = mailbox_open_irl(cdata->dav.mailbox, &rock->mailbox);
if (r) goto done;
}
/* Load message containing the resource and parse iCal data */
ical = caldav_record_to_ical(rock->mailbox, cdata, httpd_userid, NULL);
if (!ical) {
syslog(LOG_ERR, "caldav_record_to_ical failed for record %u:%s",
cdata->dav.imap_uid, rock->mailbox->name);
r = IMAP_INTERNAL;
goto done;
}
/* Convert to JMAP */
memset(&err, 0, sizeof(jmapical_err_t));
if (rock->props) {
/* XXX That's clumsy: the JMAP properties have already been converted
* to a Cyrus hash, but the jmapical API requires a JSON object. */
strarray_t *keys = hash_keys(rock->props);
int i;
jprops = json_pack("{}");
for (i = 0; i < strarray_size(keys); i++) {
json_object_set(jprops, strarray_nth(keys, i), json_null());
}
strarray_free(keys);
}
obj = jmapical_tojmap(ical, jprops, &err);
if (!obj || err.code) {
syslog(LOG_ERR, "jmapical_tojson: %s\n", jmapical_strerror(err.code));
r = IMAP_INTERNAL;
goto done;
}
icalcomponent_free(ical);
ical = NULL;
/* Add participant id */
if (_wantprop(rock->props, "participantId") && rock->req->userid) {
const char *userid = rock->req->userid;
const char *id;
json_t *p;
struct buf buf = BUF_INITIALIZER;
json_object_foreach(json_object_get(obj, "participants"), id, p) {
struct caldav_sched_param sparam;
const char *addr;
addr = json_string_value(json_object_get(p, "email"));
if (!addr) continue;
buf_setcstr(&buf, "mailto:");
buf_appendcstr(&buf, addr);
bzero(&sparam, sizeof(struct caldav_sched_param));
if (caladdress_lookup(addr, &sparam, userid) || !sparam.isyou) {
sched_param_free(&sparam);
continue;
}
/* First participant that matches isyou wins */
json_object_set_new(obj, "participantId", json_string(id));
sched_param_free(&sparam);
break;
}
buf_free(&buf);
}
/* Add JMAP-only fields. */
if (_wantprop(rock->props, "x-href")) {
char *xhref = jmap_xhref(cdata->dav.mailbox, cdata->dav.resource);
json_object_set_new(obj, "x-href", json_string(xhref));
free(xhref);
}
if (_wantprop(rock->props, "calendarId")) {
json_object_set_new(obj, "calendarId",
json_string(strrchr(cdata->dav.mailbox, '.')+1));
}
json_object_set_new(obj, "id", json_string(cdata->ical_uid));
/* Add JMAP event to response */
json_array_append_new(rock->found, obj);
done:
if (ical) icalcomponent_free(ical);
if (jprops) json_decref(jprops);
return r;
}
static int getCalendarEvents(struct jmap_req *req)
{
struct getcalendarevents_rock rock = {
req,
json_pack("[]") /*found*/,
NULL /*props*/,
NULL /*mailbox*/,
strcmp(req->accountid, req->userid) /*checkacl*/,
};
int r = 0;
r = caldav_create_defaultcalendars(req->accountid);
if (r == IMAP_MAILBOX_NONEXISTENT) {
/* The account exists but does not have a root mailbox. */
json_t *err = json_pack("{s:s}", "type", "accountNoCalendars");
json_array_append_new(req->response, json_pack("[s,o,s]",
"error", err, req->tag));
return 0;
} else if (r) return r;
json_t *properties = json_object_get(req->args, "properties");
if (properties && json_array_size(properties)) {
rock.props = xzmalloc(sizeof(struct hash_table));
construct_hash_table(rock.props, json_array_size(properties), 0);
int i;
int size = json_array_size(properties);
for (i = 0; i < size; i++) {
const char *id = json_string_value(json_array_get(properties, i));
if (id == NULL) continue;
/* 1 == properties */
hash_insert(id, (void *)1, rock.props);
}
}
struct caldav_db *db = caldav_open_userid(req->accountid);
if (!db) {
syslog(LOG_ERR,
"caldav_open_mailbox failed for user %s", req->accountid);
r = IMAP_INTERNAL;
goto done;
}
json_t *want = json_object_get(req->args, "ids");
json_t *notfound = json_array();
if (want) {
size_t i;
json_t *jval;
json_array_foreach(want, i, jval) {
const char *id = json_string_value(jval);
if (id && id[0] == '#') {
id = hash_lookup(id + 1, &req->idmap->calendarevents);
}
if (!id) continue;
size_t nfound = json_array_size(rock.found);
r = caldav_get_events(db, NULL, id, &getcalendarevents_cb, &rock);
if (r || nfound == json_array_size(rock.found)) {
json_array_append_new(notfound, json_string(id));
}
}
} else {
r = caldav_get_events(db, NULL, NULL, &getcalendarevents_cb, &rock);
if (r) goto done;
}
json_t *events = json_pack("{}");
json_object_set_new(events, "state", jmap_getstate(req, MBTYPE_CALENDAR));
json_incref(rock.found);
json_object_set_new(events, "accountId", json_string(req->accountid));
json_object_set_new(events, "list", rock.found);
if (json_array_size(notfound)) {
json_object_set_new(events, "notFound", notfound);
} else {
json_decref(notfound);
json_object_set_new(events, "notFound", json_null());
}
json_t *item = json_pack("[]");
json_array_append_new(item, json_string("CalendarEvent/get"));
json_array_append_new(item, events);
json_array_append_new(item, json_string(req->tag));
json_array_append_new(req->response, item);
done:
if (rock.props) {
free_hash_table(rock.props, NULL);
free(rock.props);
}
json_decref(rock.found);
if (db) caldav_close(db);
if (rock.mailbox) mailbox_close(&rock.mailbox);
return r;
}
static int setcalendarevents_schedule(jmap_req_t *req,
char **schedaddrp,
icalcomponent *oldical,
icalcomponent *ical,
int mode)
{
/* Determine if any scheduling is required. */
icalcomponent *src = mode & JMAP_DESTROY ? oldical : ical;
icalcomponent *comp =
icalcomponent_get_first_component(src, ICAL_VEVENT_COMPONENT);
icalproperty *prop =
icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
if (!prop) return 0;
const char *organizer = icalproperty_get_organizer(prop);
if (!organizer) return 0;
if (!strncasecmp(organizer, "mailto:", 7)) organizer += 7;
if (!*schedaddrp) {
const char **hdr =
spool_getheader(req->txn->req_hdrs, "Schedule-Address");
if (hdr) *schedaddrp = xstrdup(hdr[0]);
}
/* XXX - after legacy records are gone, we can strip this and just not send a
* cancellation if deleting a record which was never replied to... */
if (!*schedaddrp) {
/* userid corresponding to target */
*schedaddrp = xstrdup(req->userid);
/* or overridden address-set for target user */
const char *annotname =
DAV_ANNOT_NS "<" XML_NS_CALDAV ">calendar-user-address-set";
char *mailboxname = caldav_mboxname(*schedaddrp, NULL);
struct buf buf = BUF_INITIALIZER;
int r = annotatemore_lookupmask(mailboxname, annotname,
*schedaddrp, &buf);
free(mailboxname);
if (!r && buf.len > 7 && !strncasecmp(buf_cstring(&buf), "mailto:", 7)) {
free(*schedaddrp);
*schedaddrp = xstrdup(buf_cstring(&buf) + 7);
}
buf_free(&buf);
}
/* Validate create/update. */
if (oldical && (mode & (JMAP_CREATE|JMAP_UPDATE))) {
/* Don't allow ORGANIZER to be changed */
const char *oldorganizer = NULL;
icalcomponent *oldcomp = NULL;
icalproperty *prop = NULL;
oldcomp =
icalcomponent_get_first_component(oldical, ICAL_VEVENT_COMPONENT);
if (oldcomp) {
prop = icalcomponent_get_first_property(oldcomp,
ICAL_ORGANIZER_PROPERTY);
}
if (prop) oldorganizer = icalproperty_get_organizer(prop);
if (oldorganizer) {
if (!strncasecmp(oldorganizer, "mailto:", 7)) oldorganizer += 7;
if (strcasecmp(oldorganizer, organizer)) {
/* XXX This should become a set error. */
return 0;
}
}
}
if (organizer &&
/* XXX Hack for Outlook */ icalcomponent_get_first_invitee(comp)) {
/* Send scheduling message. */
if (!strcmpsafe(organizer, *schedaddrp)) {
/* Organizer scheduling object resource */
sched_request(req->userid, *schedaddrp, oldical, ical);
} else {
/* Attendee scheduling object resource */
sched_reply(req->userid, *schedaddrp, oldical, ical);
}
}
return 0;
}
static int setcalendarevents_create(jmap_req_t *req,
json_t *event,
struct caldav_db *db,
char **uidptr,
json_t *invalid)
{
int r, pe;
int needrights = DACL_WRITE;
char *uid = NULL;
struct mailbox *mbox = NULL;
char *mboxname = NULL;
char *resource = NULL;
icalcomponent *oldical = NULL;
icalcomponent *ical = NULL;
const char *calendarId = NULL;
char *schedule_address = NULL;
if ((uid = (char *) json_string_value(json_object_get(event, "uid")))) {
/* Use custom iCalendar UID from request object */
uid = xstrdup(uid);
} else {
/* Create a iCalendar UID */
uid = xstrdup(makeuuid());
}
/* Validate calendarId */
pe = readprop(event, "calendarId", 1, invalid, "s", &calendarId);
if (pe > 0 && *calendarId &&*calendarId == '#') {
calendarId =
(const char *) hash_lookup(calendarId + 1, &req->idmap->calendars);
if (!calendarId) {
json_array_append_new(invalid, json_string("calendarId"));
}
}
if (json_array_size(invalid)) {
free(uid);
*uidptr = NULL;
return 0;
}
/* Determine mailbox and resource name of calendar event.
* We attempt to reuse the UID as DAV resource name; but
* only if it looks like a reasonable URL path segment. */
struct buf buf = BUF_INITIALIZER;
mboxname = caldav_mboxname(req->accountid, calendarId);
const char *p;
for (p = uid; *p; p++) {
if ((*p >= '0' && *p <= '9') ||
(*p >= 'a' && *p <= 'z') ||
(*p >= 'A' && *p <= 'Z') ||
(*p == '@' || *p == '.') ||
(*p == '_' || *p == '-')) {
continue;
}
break;
}
if (*p == '\0' && p - uid >= 16 && p - uid <= 200) {
buf_setcstr(&buf, uid);
} else {
buf_setcstr(&buf, makeuuid());
}
buf_appendcstr(&buf, ".ics");
resource = buf_newcstring(&buf);
buf_free(&buf);
/* Check permissions. */
int rights = jmap_myrights_byname(req, mboxname);
if (!(rights & needrights)) {
json_array_append_new(invalid, json_string("calendarId"));
free(uid);
r = 0; goto done;
}
/* Open mailbox for writing */
r = mailbox_open_iwl(mboxname, &mbox);
if (r) {
syslog(LOG_ERR, "mailbox_open_iwl(%s) failed: %s",
mboxname, error_message(r));
goto done;
}
/* Convert the JMAP calendar event to ical. */
jmapical_err_t err;
memset(&err, 0, sizeof(jmapical_err_t));
if (!json_object_get(event, "uid")) {
json_object_set_new(event, "uid", json_string(uid));
}
ical = jmapical_toical(event, oldical, &err);
if (err.code == JMAPICAL_ERROR_PROPS) {
json_array_extend(invalid, err.props);
json_decref(err.props);
free(uid);
r = 0; goto done;
} else if (err.code) {
syslog(LOG_ERR, "jmapical_toical: %s", jmapical_strerror(err.code));
r = IMAP_INTERNAL;
goto done;
}
/* Handle scheduling. */
r = setcalendarevents_schedule(req, &schedule_address,
oldical, ical, JMAP_CREATE);
if (r) goto done;
/* Store the VEVENT. */
struct transaction_t txn;
memset(&txn, 0, sizeof(struct transaction_t));
txn.req_hdrs = spool_new_hdrcache();
/* XXX - fix userid */
/* Locate the mailbox */
r = http_mlookup(mbox->name, &txn.req_tgt.mbentry, NULL);
if (r) {
syslog(LOG_ERR, "mlookup(%s) failed: %s", mbox->name, error_message(r));
}
else {
r = caldav_store_resource(&txn, ical, mbox, resource,
db, 0, httpd_userid, schedule_address);
}
mboxlist_entry_free(&txn.req_tgt.mbentry);
spool_free_hdrcache(txn.req_hdrs);
buf_free(&txn.buf);
if (r && r != HTTP_CREATED && r != HTTP_NO_CONTENT) {
syslog(LOG_ERR, "caldav_store_resource failed for user %s: %s",
req->accountid, error_message(r));
r = IMAP_INTERNAL;
goto done;
}
r = 0;
*uidptr = uid;
done:
if (r) {
*uidptr = NULL;
free(uid);
}
if (mbox) mailbox_close(&mbox);
if (ical) icalcomponent_free(ical);
free(schedule_address);
free(resource);
free(mboxname);
return r;
}
static int setcalendarevents_update(jmap_req_t *req,
json_t *event,
const char *id,
struct caldav_db *db,
json_t *invalid)
{
int r, pe;
int needrights = DACL_RMRSRC|DACL_WRITE;
struct caldav_data *cdata = NULL;
struct mailbox *mbox = NULL;
char *mboxname = NULL;
struct mailbox *dstmbox = NULL;
char *dstmboxname = NULL;
struct mboxevent *mboxevent = NULL;
char *resource = NULL;
icalcomponent *oldical = NULL;
icalcomponent *ical = NULL;
struct index_record record;
const char *calendarId = NULL;
char *schedule_address = NULL;
/* Validate calendarId */
pe = readprop(event, "calendarId", 0, invalid, "s", &calendarId);
if (pe > 0 && *calendarId && *calendarId == '#') {
calendarId =
(const char *) hash_lookup(calendarId + 1, &req->idmap->calendars);
if (!calendarId) {
json_array_append_new(invalid, json_string("calendarId"));
}
}
if (json_array_size(invalid)) {
return 0;
}
/* Determine mailbox and resource name of calendar event. */
r = caldav_lookup_uid(db, id, &cdata);
if (r && r != CYRUSDB_NOTFOUND) {
syslog(LOG_ERR,
"caldav_lookup_uid(%s) failed: %s", id, error_message(r));
goto done;
}
if (r == CYRUSDB_NOTFOUND || !cdata->dav.alive ||
!cdata->dav.rowid || !cdata->dav.imap_uid) {
r = IMAP_NOTFOUND;
goto done;
}
mboxname = xstrdup(cdata->dav.mailbox);
resource = xstrdup(cdata->dav.resource);
/* Check permissions. */
int rights = jmap_myrights_byname(req, mboxname);
if (!(rights & needrights)) {
json_array_append_new(invalid, json_string("calendarId"));
r = 0;
goto done;
}
/* Open mailbox for writing */
r = mailbox_open_iwl(mboxname, &mbox);
if (r == IMAP_MAILBOX_NONEXISTENT) {
json_array_append_new(invalid, json_string("calendarId"));
r = 0;
goto done;
}
else if (r) {
syslog(LOG_ERR, "mailbox_open_iwl(%s) failed: %s",
mboxname, error_message(r));
goto done;
}
/* Fetch index record for the resource */
memset(&record, 0, sizeof(struct index_record));
r = mailbox_find_index_record(mbox, cdata->dav.imap_uid, &record);
if (r == IMAP_NOTFOUND) {
json_array_append_new(invalid, json_string("calendarId"));
r = 0;
goto done;
} else if (r) {
syslog(LOG_ERR, "mailbox_index_record(0x%x) failed: %s",
cdata->dav.imap_uid, error_message(r));
goto done;
}
/* Load VEVENT from record. */
oldical = record_to_ical(mbox, &record, &schedule_address);
if (!oldical) {
syslog(LOG_ERR, "record_to_ical failed for record %u:%s",
cdata->dav.imap_uid, mbox->name);
r = IMAP_INTERNAL;
goto done;
}
/* Convert the JMAP calendar event to ical. */
jmapical_err_t err;
memset(&err, 0, sizeof(jmapical_err_t));
if (!json_object_get(event, "uid")) {
json_object_set_new(event, "uid", json_string(id));
}
ical = jmapical_toical(event, oldical, &err);
if (err.code == JMAPICAL_ERROR_PROPS) {
/* Handle any property errors and bail out. */
json_array_extend(invalid, err.props);
r = 0; goto done;
} else if (err.code) {
syslog(LOG_ERR, "jmapical_toical: %s", jmapical_strerror(err.code));
r = IMAP_INTERNAL;
goto done;
}
if (calendarId) {
/* Check, if we need to move the event. */
dstmboxname = caldav_mboxname(req->accountid, calendarId);
if (strcmp(mbox->name, dstmboxname)) {
/* Check permissions */
int dstrights = jmap_myrights_byname(req, dstmboxname);
if (!(dstrights & needrights)) {
json_array_append_new(invalid, json_string("calendarId"));
r = 0;
goto done;
}
/* Open destination mailbox for writing. */
r = mailbox_open_iwl(dstmboxname, &dstmbox);
if (r == IMAP_MAILBOX_NONEXISTENT) {
json_array_append_new(invalid, json_string("calendarId"));
r = 0;
goto done;
} else if (r) {
syslog(LOG_ERR, "mailbox_open_iwl(%s) failed: %s",
dstmboxname, error_message(r));
goto done;
}
}
}
/* Handle scheduling. */
r = setcalendarevents_schedule(req, &schedule_address,
oldical, ical, JMAP_UPDATE);
if (r) goto done;
if (dstmbox) {
/* Expunge the resource from mailbox. */
record.system_flags |= FLAG_EXPUNGED;
mboxevent = mboxevent_new(EVENT_MESSAGE_EXPUNGE);
r = mailbox_rewrite_index_record(mbox, &record);
if (r) {
syslog(LOG_ERR, "mailbox_rewrite_index_record (%s) failed: %s",
cdata->dav.mailbox, error_message(r));
mailbox_close(&mbox);
goto done;
}
mboxevent_extract_record(mboxevent, mbox, &record);
mboxevent_extract_mailbox(mboxevent, mbox);
mboxevent_set_numunseen(mboxevent, mbox, -1);
mboxevent_set_access(mboxevent, NULL, NULL,
req->userid, cdata->dav.mailbox, 0);
mailbox_close(&mbox);
mboxevent_notify(&mboxevent);
mboxevent_free(&mboxevent);
/* Close the mailbox we moved the event from. */
mailbox_close(&mbox);
mbox = dstmbox;
dstmbox = NULL;
free(mboxname);
mboxname = dstmboxname;
dstmboxname = NULL;
}
/* Store the updated VEVENT. */
struct transaction_t txn;
memset(&txn, 0, sizeof(struct transaction_t));
txn.req_hdrs = spool_new_hdrcache();
/* XXX - fix userid */
r = http_mlookup(mbox->name, &txn.req_tgt.mbentry, NULL);
if (r) {
syslog(LOG_ERR, "mlookup(%s) failed: %s", mbox->name, error_message(r));
}
else {
r = caldav_store_resource(&txn, ical, mbox, resource,
db, 0, httpd_userid, schedule_address);
}
transaction_free(&txn);
if (r && r != HTTP_CREATED && r != HTTP_NO_CONTENT) {
syslog(LOG_ERR, "caldav_store_resource failed for user %s: %s",
req->accountid, error_message(r));
if (r == HTTP_FORBIDDEN)
r = IMAP_PERMISSION_DENIED;
else
r = IMAP_INTERNAL;
goto done;
}
r = 0;
done:
if (mbox) mailbox_close(&mbox);
if (dstmbox) mailbox_close(&dstmbox);
if (ical) icalcomponent_free(ical);
if (oldical) icalcomponent_free(oldical);
free(schedule_address);
free(dstmboxname);
free(resource);
free(mboxname);
return r;
}
static int setcalendarevents_destroy(jmap_req_t *req,
const char *id,
struct caldav_db *db)
{
int r, rights;
int needrights = DACL_RMRSRC;
struct caldav_data *cdata = NULL;
struct mailbox *mbox = NULL;
char *mboxname = NULL;
struct mboxevent *mboxevent = NULL;
char *resource = NULL;
icalcomponent *oldical = NULL;
icalcomponent *ical = NULL;
struct index_record record;
char *schedule_address = NULL;
/* Determine mailbox and resource name of calendar event. */
r = caldav_lookup_uid(db, id, &cdata);
if (r) {
syslog(LOG_ERR,
"caldav_lookup_uid(%s) failed: %s", id, cyrusdb_strerror(r));
r = CYRUSDB_NOTFOUND ? IMAP_NOTFOUND : IMAP_INTERNAL;
goto done;
}
mboxname = xstrdup(cdata->dav.mailbox);
resource = xstrdup(cdata->dav.resource);
/* Check permissions. */
rights = jmap_myrights_byname(req, mboxname);
if (!(rights & needrights)) {
r = rights & DACL_READ ? IMAP_PERMISSION_DENIED : IMAP_NOTFOUND;
goto done;
}
/* Open mailbox for writing */
r = mailbox_open_iwl(mboxname, &mbox);
if (r) {
syslog(LOG_ERR, "mailbox_open_iwl(%s) failed: %s",
mboxname, error_message(r));
goto done;
}
/* Fetch index record for the resource. Need this for scheduling. */
memset(&record, 0, sizeof(struct index_record));
r = mailbox_find_index_record(mbox, cdata->dav.imap_uid, &record);
if (r) {
syslog(LOG_ERR, "mailbox_index_record(0x%x) failed: %s",
cdata->dav.imap_uid, error_message(r));
goto done;
}
/* Load VEVENT from record. */
oldical = record_to_ical(mbox, &record, &schedule_address);
if (!oldical) {
syslog(LOG_ERR, "record_to_ical failed for record %u:%s",
cdata->dav.imap_uid, mbox->name);
r = IMAP_INTERNAL;
goto done;
}
/* Handle scheduling. */
r = setcalendarevents_schedule(req, &schedule_address,
oldical, ical, JMAP_DESTROY);
if (r) goto done;
/* Expunge the resource from mailbox. */
record.system_flags |= FLAG_EXPUNGED;
mboxevent = mboxevent_new(EVENT_MESSAGE_EXPUNGE);
r = mailbox_rewrite_index_record(mbox, &record);
if (r) {
syslog(LOG_ERR, "mailbox_rewrite_index_record (%s) failed: %s",
cdata->dav.mailbox, error_message(r));
mailbox_close(&mbox);
goto done;
}
mboxevent_extract_record(mboxevent, mbox, &record);
mboxevent_extract_mailbox(mboxevent, mbox);
mboxevent_set_numunseen(mboxevent, mbox, -1);
mboxevent_set_access(mboxevent, NULL, NULL,
req->userid, cdata->dav.mailbox, 0);
mailbox_close(&mbox);
mboxevent_notify(&mboxevent);
mboxevent_free(&mboxevent);
/* Keep the VEVENT in the database but set alive to 0, to report
* with getCalendarEventsUpdates. */
cdata->dav.alive = 0;
cdata->dav.modseq = record.modseq;
cdata->dav.imap_uid = record.uid;
r = caldav_write(db, cdata);
done:
if (mbox) mailbox_close(&mbox);
if (oldical) icalcomponent_free(oldical);
free(schedule_address);
free(resource);
free(mboxname);
return r;
}
static int setCalendarEvents(struct jmap_req *req)
{
struct caldav_db *db = NULL;
json_t *set = NULL;
int r = 0;
json_t *state = json_object_get(req->args, "ifInState");
if (state && jmap_cmpstate(req, state, MBTYPE_CALENDAR)) {
json_array_append_new(req->response, json_pack("[s, {s:s}, s]",
"error", "type", "stateMismatch", req->tag));
goto done;
}
set = json_pack("{s:s}", "accountId", req->accountid);
json_object_set_new(set, "oldState", jmap_getstate(req, MBTYPE_CALENDAR));
r = caldav_create_defaultcalendars(req->accountid);
if (r == IMAP_MAILBOX_NONEXISTENT) {
/* The account exists but does not have a root mailbox. */
json_t *err = json_pack("{s:s}", "type", "accountNoCalendars");
json_array_append_new(req->response, json_pack("[s,o,s]",
"error", err, req->tag));
return 0;
} else if (r) return r;
db = caldav_open_userid(req->accountid);
if (!db) {
syslog(LOG_ERR, "caldav_open_mailbox failed for user %s", req->userid);
r = IMAP_INTERNAL;
goto done;
}
json_t *create = json_object_get(req->args, "create");
if (create) {
json_t *created = json_pack("{}");
json_t *notCreated = json_pack("{}");
const char *key;
json_t *arg;
json_object_foreach(create, key, arg) {
char *uid = NULL;
/* Validate calendar event id. */
if (!strlen(key)) {
json_t *err= json_pack("{s:s}", "type", "invalidArguments");
json_object_set_new(notCreated, key, err);
continue;
}
/* Create the calendar event. */
json_t *invalid = json_pack("[]");
r = setcalendarevents_create(req, arg, db, &uid, invalid);
if (r) {
json_decref(invalid);
free(uid);
goto done;
}
if (json_array_size(invalid)) {
json_t *err = json_pack("{s:s, s:o}",
"type", "invalidProperties", "properties", invalid);
json_object_set_new(notCreated, key, err);
free(uid);
continue;
}
json_decref(invalid);
/* Report calendar event as created. */
json_object_set_new(created, key, json_pack("{s:s}", "id", uid));
hash_insert(key, uid, &req->idmap->calendarevents);
}
if (json_object_size(created)) {
json_object_set(set, "created", created);
}
json_decref(created);
if (json_object_size(notCreated)) {
json_object_set(set, "notCreated", notCreated);
}
json_decref(notCreated);
}
json_t *update = json_object_get(req->args, "update");
if (update) {
json_t *updated = json_pack("{}");
json_t *notUpdated = json_pack("{}");
const char *uid;
json_t *arg;
json_object_foreach(update, uid, arg) {
const char *val = NULL;
/* Validate uid. */
if (!uid) {
continue;
}
if (uid && uid[0] == '#') {
const char *newuid =
hash_lookup(uid + 1, &req->idmap->calendarevents);
if (!newuid) {
json_t *err = json_pack("{s:s}", "type", "notFound");
json_object_set_new(notUpdated, uid, err);
continue;
}
uid = newuid;
}
if ((val = (char *)json_string_value(json_object_get(arg, "uid")))) {
/* The uid property must match the current iCalendar UID */
if (strcmp(val, uid)) {
json_t *err = json_pack(
"{s:s, s:o}",
"type", "invalidProperties",
"properties", json_pack("[s]"));
json_object_set_new(notUpdated, uid, err);
continue;
}
}
/* Update the calendar event. */
json_t *invalid = json_pack("[]");
r = setcalendarevents_update(req, arg, uid, db, invalid);
if (r == IMAP_NOTFOUND) {
json_t *err = json_pack("{s:s}", "type", "notFound");
json_object_set_new(notUpdated, uid, err);
json_decref(invalid);
r = 0;
continue;
} else if (r) {
json_decref(invalid);
goto done;
}
if (json_array_size(invalid)) {
json_t *err = json_pack(
"{s:s, s:o}", "type", "invalidProperties",
"properties", invalid);
json_object_set_new(notUpdated, uid, err);
continue;
}
json_decref(invalid);
/* Report calendar event as updated. */
json_object_set_new(updated, uid, json_null());
}
if (json_object_size(updated)) {
json_object_set(set, "updated", updated);
}
json_decref(updated);
if (json_object_size(notUpdated)) {
json_object_set(set, "notUpdated", notUpdated);
}
json_decref(notUpdated);
}
json_t *destroy = json_object_get(req->args, "destroy");
if (destroy) {
json_t *destroyed = json_pack("[]");
json_t *notDestroyed = json_pack("{}");
size_t index;
json_t *juid;
json_array_foreach(destroy, index, juid) {
/* Validate uid. */
const char *uid = json_string_value(juid);
if (!uid) {
continue;
}
if (uid && uid[0] == '#') {
const char *newuid =
hash_lookup(uid + 1, &req->idmap->calendarevents);
if (!newuid) {
json_t *err = json_pack("{s:s}", "type", "notFound");
json_object_set_new(notDestroyed, uid, err);
continue;
}
uid = newuid;
}
/* Destroy the calendar event. */
r = setcalendarevents_destroy(req, uid, db);
if (r == IMAP_NOTFOUND) {
json_t *err = json_pack("{s:s}", "type", "notFound");
json_object_set_new(notDestroyed, uid, err);
r = 0;
continue;
} else if (r == IMAP_PERMISSION_DENIED) {
json_t *err = json_pack("{s:s}", "type", "forbidden");
json_object_set_new(notDestroyed, uid, err);
r = 0;
continue;
} else if (r) {
goto done;
}
/* Report calendar event as destroyed. */
json_array_append_new(destroyed, json_string(uid));
}
if (json_array_size(destroyed)) {
json_object_set(set, "destroyed", destroyed);
}
json_decref(destroyed);
if (json_object_size(notDestroyed)) {
json_object_set(set, "notDestroyed", notDestroyed);
}
json_decref(notDestroyed);
}
/* Set newState field in calendarsSet. */
if (json_object_get(set, "created") ||
json_object_get(set, "updated") ||
json_object_get(set, "destroyed")) {
r = jmap_bumpstate(req, MBTYPE_CALENDAR);
if (r) goto done;
}
json_object_set_new(set, "newState", jmap_getstate(req, MBTYPE_CALENDAR));
json_incref(set);
json_t *item = json_pack("[]");
json_array_append_new(item, json_string("CalendarEvent/set"));
json_array_append_new(item, set);
json_array_append_new(item, json_string(req->tag));
json_array_append_new(req->response, item);
done:
if (db) caldav_close(db);
if (set) json_decref(set);
return r;
}
struct geteventupdates_rock {
jmap_req_t *req;
json_t *changed;
json_t *removed;
size_t seen_records;
size_t max_records;
modseq_t highestmodseq;
int check_acl;
hash_table *mboxrights;
};
static void strip_spurious_deletes(struct geteventupdates_rock *urock)
{
/* if something is mentioned in both DELETEs and UPDATEs, it's probably
* a move. O(N*M) algorithm, but there are rarely many, and the alternative
* of a hash will cost more */
unsigned i, j;
for (i = 0; i < json_array_size(urock->removed); i++) {
const char *del = json_string_value(json_array_get(urock->removed, i));
for (j = 0; j < json_array_size(urock->changed); j++) {
const char *up =
json_string_value(json_array_get(urock->changed, j));
if (!strcmpsafe(del, up)) {
json_array_remove(urock->removed, i--);
break;
}
}
}
}
static int geteventupdates_cb(void *vrock, struct caldav_data *cdata)
{
struct geteventupdates_rock *rock = vrock;
jmap_req_t *req = rock->req;
/* Count, but don't process items that exceed the maximum record count. */
if (rock->max_records && ++(rock->seen_records) > rock->max_records) {
return 0;
}
/* Check permissions */
int rights = jmap_myrights_byname(req, cdata->dav.mailbox);
if (!(rights & DACL_READ))
return 0;
/* Report item as updated or removed. */
if (cdata->dav.alive) {
json_array_append_new(rock->changed, json_string(cdata->ical_uid));
} else {
json_array_append_new(rock->removed, json_string(cdata->ical_uid));
}
if (cdata->dav.modseq > rock->highestmodseq) {
rock->highestmodseq = cdata->dav.modseq;
}
return 0;
}
static int getCalendarEventsUpdates(struct jmap_req *req)
{
int r, pe;
json_t *invalid;
struct caldav_db *db;
const char *since;
modseq_t oldmodseq = 0;
json_int_t maxChanges = 0;
int dofetch = 0;
struct buf buf = BUF_INITIALIZER;
db = caldav_open_userid(req->accountid);
if (!db) {
syslog(LOG_ERR, "caldav_open_mailbox failed for user %s", req->accountid);
return IMAP_INTERNAL;
}
/* Parse and validate arguments. */
invalid = json_pack("[]");
pe = readprop(req->args, "sinceState", 1 /*mandatory*/, invalid, "s", &since);
if (pe > 0) {
oldmodseq = atomodseq_t(since);
if (!oldmodseq) {
json_array_append_new(invalid, json_string("sinceState"));
}
}
pe = readprop(req->args,
"maxChanges", 0 /*mandatory*/, invalid, "I", &maxChanges);
if (pe > 0) {
if (maxChanges <= 0) {
json_array_append_new(invalid, json_string("maxChanges"));
}
}
readprop(req->args, "fetchRecords", 0 /*mandatory*/, invalid, "b", &dofetch);
if (json_array_size(invalid)) {
json_t *err = json_pack("{s:s, s:o}",
"type", "invalidArguments", "arguments", invalid);
json_array_append_new(req->response,
json_pack("[s,o,s]", "error", err, req->tag));
return 0;
}
json_decref(invalid);
/* Lookup updates. */
struct geteventupdates_rock rock = {
req,
json_array() /*changed*/,
json_array() /*removed*/,
0 /*seen_records*/,
maxChanges /*max_records*/,
0 /*highestmodseq*/,
strcmp(req->accountid, req->userid) /* check_acl */,
NULL /*mboxrights*/
};
r = caldav_get_updates(db, oldmodseq, NULL /*mboxname*/, CAL_COMP_VEVENT,
maxChanges ? maxChanges + 1 : -1,
&geteventupdates_cb, &rock);
if (r) goto done;
strip_spurious_deletes(&rock);
/* Determine new state. */
modseq_t newstate;
int more = rock.max_records ? rock.seen_records > rock.max_records : 0;
if (more) {
newstate = rock.highestmodseq;
} else {
newstate = jmap_highestmodseq(req, MBTYPE_CALENDAR);
}
/* Create response. */
json_t *eventUpdates = json_pack("{}");
json_object_set_new(eventUpdates, "accountId", json_string(req->accountid));
json_object_set_new(eventUpdates, "oldState", json_string(since));
buf_printf(&buf, MODSEQ_FMT, newstate);
json_object_set_new(eventUpdates, "newState", json_string(buf_cstring(&buf)));
buf_reset(&buf);
json_object_set_new(eventUpdates, "hasMoreUpdates", json_boolean(more));
json_object_set(eventUpdates, "changed", rock.changed);
json_object_set(eventUpdates, "removed", rock.removed);
json_t *item = json_pack("[]");
- json_array_append_new(item, json_string("CalendarEvent/Changes"));
+ json_array_append_new(item, json_string("CalendarEvent/changes"));
json_array_append_new(item, eventUpdates);
json_array_append_new(item, json_string(req->tag));
json_array_append_new(req->response, item);
/* Fetch updated records, if requested. */
if (dofetch) {
json_t *props = json_object_get(req->args, "fetchRecordProperties");
struct jmap_req subreq = *req;
subreq.args = json_pack("{}");
json_object_set(subreq.args, "ids", rock.changed);
if (props) json_object_set(subreq.args, "properties", props);
r = getCalendarEvents(&subreq);
json_decref(subreq.args);
}
done:
buf_free(&buf);
if (rock.changed) json_decref(rock.changed);
if (rock.removed) json_decref(rock.removed);
if (rock.mboxrights) {
free_hash_table(rock.mboxrights, free);
free(rock.mboxrights);
}
if (db) caldav_close(db);
return r;
}
static void match_fuzzy(search_expr_t *parent, const char *s, const char *name)
{
search_expr_t *e;
const search_attr_t *attr = search_attr_find(name);
e = search_expr_new(parent, SEOP_FUZZYMATCH);
e->attr = attr;
e->value.s = xstrdup(s);
if (!e->value.s) {
e->op = SEOP_FALSE;
e->attr = NULL;
}
}
static search_expr_t *buildsearch(jmap_req_t *req, json_t *filter,
search_expr_t *parent)
{
search_expr_t *this, *e;
const char *s;
size_t i;
json_t *val, *arg;
if (!JNOTNULL(filter)) {
return search_expr_new(parent, SEOP_TRUE);
}
if ((s = json_string_value(json_object_get(filter, "operator")))) {
enum search_op op = SEOP_UNKNOWN;
if (!strcmp("AND", s)) {
op = SEOP_AND;
} else if (!strcmp("OR", s)) {
op = SEOP_OR;
} else if (!strcmp("NOT", s)) {
op = SEOP_NOT;
}
this = search_expr_new(parent, op);
e = op == SEOP_NOT ? search_expr_new(this, SEOP_OR) : this;
json_array_foreach(json_object_get(filter, "conditions"), i, val) {
buildsearch(req, val, e);
}
} else {
this = search_expr_new(parent, SEOP_AND);
if ((arg = json_object_get(filter, "inCalendars"))) {
e = search_expr_new(this, SEOP_OR);
json_array_foreach(arg, i, val) {
const char *id = json_string_value(val);
search_expr_t *m = search_expr_new(e, SEOP_MATCH);
m->attr = search_attr_find("folder");
m->value.s = caldav_mboxname(req->accountid, id);
}
}
if ((s = json_string_value(json_object_get(filter, "text")))) {
e = search_expr_new(this, SEOP_OR);
match_fuzzy(e, s, "body");
match_fuzzy(e, s, "subject");
match_fuzzy(e, s, "from");
match_fuzzy(e, s, "to");
match_fuzzy(e, s, "location");
}
if ((s = json_string_value(json_object_get(filter, "title")))) {
match_fuzzy(this, s, "subject");
}
if ((s = json_string_value(json_object_get(filter, "description")))) {
match_fuzzy(this, s, "body");
}
if ((s = json_string_value(json_object_get(filter, "location")))) {
match_fuzzy(this, s, "location");
}
if ((s = json_string_value(json_object_get(filter, "owner")))) {
match_fuzzy(this, s, "from");
}
if ((s = json_string_value(json_object_get(filter, "attendee")))) {
match_fuzzy(this, s, "to");
}
}
return this;
}
static void filter_timerange(json_t *filter, time_t *before, time_t *after,
int *skip_search)
{
*before = caldav_eternity;
*after = caldav_epoch;
if (!JNOTNULL(filter)) {
return;
}
if (json_object_get(filter, "conditions")) {
json_t *val;
size_t i;
time_t bf, af;
json_array_foreach(json_object_get(filter, "conditions"), i, val) {
const char *op =
json_string_value(json_object_get(filter, "operator"));
bf = caldav_eternity;
af = caldav_epoch;
filter_timerange(val, &bf, &af, skip_search);
if (bf != caldav_eternity) {
if (!strcmp(op, "OR")) {
if (*before == caldav_eternity || *before < bf) {
*before = bf;
}
}
else if (!strcmp(op, "AND")) {
if (*before == caldav_eternity || *before > bf) {
*before = bf;
}
}
else if (!strcmp(op, "NOT")) {
if (*after == caldav_epoch || *after < bf) {
*after = bf;
}
}
}
if (af != caldav_epoch) {
if (!strcmp(op, "OR")) {
if (*after == caldav_epoch || *after > af) {
*after = af;
}
}
else if (!strcmp(op, "AND")) {
if (*after == caldav_epoch || *after < af) {
*after = af;
}
}
else if (!strcmp(op, "NOT")) {
if (*before == caldav_eternity || *before < af) {
*before = af;
}
}
}
}
} else {
const char *sb = json_string_value(json_object_get(filter, "before"));
const char *sa = json_string_value(json_object_get(filter, "after"));
if (!sb || time_from_iso8601(sb, before) == -1) {
*before = caldav_eternity;
}
if (!sa || time_from_iso8601(sa, after) == -1) {
*after = caldav_epoch;
}
if (json_object_get(filter, "text") ||
json_object_get(filter, "title") ||
json_object_get(filter, "description") ||
json_object_get(filter, "location") ||
json_object_get(filter, "owner") ||
json_object_get(filter, "attendee")) {
*skip_search = 0;
}
}
}
struct search_timerange_rock {
jmap_req_t *req;
hash_table *search_timerange; /* hash of all UIDs within timerange */
size_t seen; /* seen uids inside and outside of window */
int check_acl; /* if true, check mailbox ACL for read access */
hash_table *mboxrights; /* cache of (int) ACLs, keyed by mailbox name */
int build_result; /* if true, filter search window and buidl result */
size_t limit; /* window limit */
size_t pos; /* window start position */
json_t *result; /* windowed search result */
};
static int search_timerange_cb(void *vrock, struct caldav_data *cdata)
{
struct search_timerange_rock *rock = vrock;
jmap_req_t *req = rock->req;
/* Ignore tombstones */
if (!cdata->dav.alive) {
return 0;
}
/* Check permissions */
int rights = jmap_myrights_byname(req, cdata->dav.mailbox);
if (!(rights & ACL_READ))
return 0;
/* Keep track of this event */
hash_insert(cdata->ical_uid, (void*)1, rock->search_timerange);
rock->seen++;
if (rock->build_result) {
/* Is it within the search window? */
if (rock->pos > rock->seen) {
return 0;
}
if (rock->limit && json_array_size(rock->result) >= rock->limit) {
return 0;
}
/* Add the event to the result list */
json_array_append_new(rock->result, json_string(cdata->ical_uid));
}
return 0;
}
static int jmapevent_search(jmap_req_t *req, json_t *filter,
size_t limit, size_t pos,
size_t *total, json_t **eventids)
{
int r, i;
struct searchargs *searchargs = NULL;
struct index_init init;
struct index_state *state = NULL;
search_query_t *query = NULL;
struct caldav_db *db = NULL;
time_t before, after;
char *icalbefore = NULL, *icalafter = NULL;
hash_table *search_timerange = NULL;
int skip_search = 1;
icaltimezone *utc = icaltimezone_get_utc_timezone();
struct sortcrit *sortcrit = NULL;
hash_table mboxrights = HASH_TABLE_INITIALIZER;
int check_acl = strcmp(req->accountid, req->userid);
if (check_acl) {
construct_hash_table(&mboxrights, 128, 0);
}
/* Initialize return values */
*eventids = json_pack("[]");
*total = 0;
/* Determine the filter timerange, if any */
filter_timerange(filter, &before, &after, &skip_search);
if (before != caldav_eternity) {
icaltimetype t = icaltime_from_timet_with_zone(before, 0, utc);
icalbefore = icaltime_as_ical_string_r(t);
}
if (after != caldav_epoch) {
icaltimetype t = icaltime_from_timet_with_zone(after, 0, utc);
icalafter = icaltime_as_ical_string_r(t);
}
if (!icalbefore && !icalafter) {
skip_search = 0;
}
/* Open the CalDAV database */
db = caldav_open_userid(req->accountid);
if (!db) {
syslog(LOG_ERR,
"caldav_open_mailbox failed for user %s", req->accountid);
r = IMAP_INTERNAL;
goto done;
}
/* Filter events by timerange */
if (before != caldav_eternity || after != caldav_epoch) {
search_timerange = xzmalloc(sizeof(hash_table));
construct_hash_table(search_timerange, 64, 0);
struct search_timerange_rock rock = {
req,
search_timerange,
0, /*seen*/
check_acl,
&mboxrights,
skip_search, /*build_result*/
limit,
pos,
*eventids /*result*/
};
r = caldav_foreach_timerange(db, NULL,
after, before, search_timerange_cb, &rock);
if (r) goto done;
*total = rock.seen;
}
/* Can we skip search? */
if (skip_search) goto done;
/* Reset search results */
*total = 0;
json_array_clear(*eventids);
/* Build searchargs */
searchargs = new_searchargs(NULL, GETSEARCH_CHARSET_FIRST,
&jmap_namespace, req->accountid, req->authstate, 0);
searchargs->root = buildsearch(req, filter, NULL);
/* Need some stable sort criteria for windowing */
sortcrit = xzmalloc(2 * sizeof(struct sortcrit));
sortcrit[0].flags |= SORT_REVERSE;
sortcrit[0].key = SORT_ARRIVAL;
sortcrit[1].key = SORT_SEQUENCE;
/* Run the search query */
memset(&init, 0, sizeof(init));
init.userid = req->accountid;
init.authstate = req->authstate;
init.want_expunged = 0;
init.want_mbtype = MBTYPE_CALENDAR;
r = index_open(req->inboxname, &init, &state);
if (r) goto done;
query = search_query_new(state, searchargs);
query->sortcrit = sortcrit;
query->multiple = 1;
query->need_ids = 1;
query->want_expunged = 0;
query->want_mbtype = MBTYPE_CALENDAR;
r = search_query_run(query);
if (r && r != IMAP_NOTFOUND) goto done;
r = 0;
/* Aggregate result */
for (i = 0 ; i < query->merged_msgdata.count; i++) {
MsgData *md = ptrarray_nth(&query->merged_msgdata, i);
search_folder_t *folder = md->folder;
struct caldav_data *cdata;
if (!folder) continue;
/* Check permissions */
int rights = jmap_myrights_byname(req, folder->mboxname);
if (!(rights & ACL_READ))
continue;
/* Fetch the CalDAV db record */
r = caldav_lookup_imapuid(db, folder->mboxname, md->uid, &cdata, 0);
if (r) continue;
/* Filter by timerange, if any */
if (search_timerange && !hash_lookup(cdata->ical_uid, search_timerange)) {
continue;
}
/* It's a legit search hit... */
*total = *total + 1;
/* ...probably outside the search window? */
if (limit && json_array_size(*eventids) + 1 > limit) {
continue;
}
if (pos >= *total) {
continue;
}
/* Add the search result */
json_array_append_new(*eventids, json_string(cdata->ical_uid));
}
done:
if (state) {
state->mailbox = NULL;
index_close(&state);
}
if (search_timerange) {
free_hash_table(search_timerange, NULL);
free(search_timerange);
}
free_hash_table(&mboxrights, free);
if (searchargs) freesearchargs(searchargs);
if (sortcrit) freesortcrit(sortcrit);
if (query) search_query_free(query);
if (db) caldav_close(db);
free(icalbefore);
free(icalafter);
return r;
}
static void validatefilter(json_t *filter, const char *prefix, json_t *invalid)
{
struct buf buf = BUF_INITIALIZER;
icaltimetype timeval;
const char *s;
json_t *arg, *val;
size_t i;
if (!JNOTNULL(filter) || json_typeof(filter) != JSON_OBJECT) {
json_array_append_new(invalid, json_string(prefix));
return;
}
if (readprop_full(filter, prefix, "operator", 0, invalid, "s", &s) > 0) {
if (strcmp("AND", s) && strcmp("OR", s) && strcmp("NOT", s)) {
buf_printf(&buf, "%s.%s", prefix, "operator");
json_array_append_new(invalid, json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
arg = json_object_get(filter, "conditions");
if (!json_array_size(arg)) {
buf_printf(&buf, "%s.conditions", prefix);
json_array_append_new(invalid, json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
json_array_foreach(arg, i, val) {
buf_printf(&buf, "%s.conditions[%zu]", prefix, i);
validatefilter(val, buf_cstring(&buf), invalid);
buf_reset(&buf);
}
}
else {
arg = json_object_get(filter, "inCalendars");
if (arg && json_array_size(arg)) {
size_t i;
json_t *uid;
json_array_foreach(arg, i, uid) {
const char *id = json_string_value(uid);
if (!id || id[0] == '#') {
buf_printf(&buf, "%s.calendars[%zu]", prefix, i);
json_array_append_new(invalid, json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
}
}
else if (JNOTNULL(arg) && !json_array_size(arg)) {
buf_printf(&buf, "%s.%s", prefix, "inCalendars");
json_array_append_new(invalid, json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
if (JNOTNULL(json_object_get(filter, "after"))) {
if (readprop_full(filter, prefix, "after", 1, invalid, "s", &s) > 0) {
if (!utcdate_to_icaltime(s, &timeval)) {
buf_printf(&buf, "%s.%s", prefix, "after");
json_array_append_new(invalid, json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
}
}
if (JNOTNULL(json_object_get(filter, "before"))) {
if (readprop_full(filter, prefix, "before", 1, invalid, "s", &s) > 0) {
if (!utcdate_to_icaltime(s, &timeval)) {
buf_printf(&buf, "%s.%s", prefix, "before");
json_array_append_new(invalid, json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
}
}
if (JNOTNULL(json_object_get(filter, "text"))) {
readprop_full(filter, prefix, "text", 1, invalid, "s", &s);
}
if (JNOTNULL(json_object_get(filter, "summary"))) {
readprop_full(filter, prefix, "summary", 1, invalid, "s", &s);
}
if (JNOTNULL(json_object_get(filter, "description"))) {
readprop_full(filter, prefix, "description", 1, invalid, "s", &s);
}
if (JNOTNULL(json_object_get(filter, "location"))) {
readprop_full(filter, prefix, "location", 1, invalid, "s", &s);
}
if (JNOTNULL(json_object_get(filter, "owner"))) {
readprop_full(filter, prefix, "owner", 1, invalid, "s", &s);
}
if (JNOTNULL(json_object_get(filter, "attendee"))) {
readprop_full(filter, prefix, "attendee", 1, invalid, "s", &s);
}
}
buf_free(&buf);
}
static int getCalendarEventsList(struct jmap_req *req)
{
int r = 0, pe;
json_t *invalid;
int dofetch = 0;
json_t *filter;
size_t total = 0;
json_t *events = NULL;
/* Parse and validate arguments. */
invalid = json_pack("[]");
/* filter */
filter = json_object_get(req->args, "filter");
if (JNOTNULL(filter)) {
validatefilter(filter, "filter", invalid);
}
/* position */
json_int_t pos = 0;
if (JNOTNULL(json_object_get(req->args, "position"))) {
pe = readprop(req->args, "position",
0 /*mandatory*/, invalid, "I", &pos);
if (pe > 0 && pos < 0) {
json_array_append_new(invalid, json_string("position"));
}
}
/* limit */
json_int_t limit = 0;
if (JNOTNULL(json_object_get(req->args, "limit"))) {
pe = readprop(req->args, "limit",
0 /*mandatory*/, invalid, "I", &limit);
if (pe > 0 && limit < 0) {
json_array_append_new(invalid, json_string("limit"));
}
}
/* fetchCalendarEvents */
if (JNOTNULL(json_object_get(req->args, "fetchCalendarEvents"))) {
readprop(req->args, "fetchCalendarEvents",
0 /*mandatory*/, invalid, "b", &dofetch);
}
if (json_array_size(invalid)) {
json_t *err = json_pack("{s:s, s:o}",
"type", "invalidArguments", "arguments", invalid);
json_array_append_new(req->response,
json_pack("[s,o,s]", "error", err, req->tag));
r = 0;
goto done;
}
json_decref(invalid);
/* Call search */
r = jmapevent_search(req, filter, limit, pos, &total, &events);
if (r) goto done;
/* Prepare response. */
json_t *eventList = json_pack("{}");
json_object_set_new(eventList, "accountId", json_string(req->accountid));
json_object_set_new(eventList, "state", jmap_getstate(req, MBTYPE_CALENDAR));
json_object_set_new(eventList, "position", json_integer(pos));
json_object_set_new(eventList, "total", json_integer(total));
json_object_set(eventList, "calendarEventIds", events);
if (filter) json_object_set(eventList, "filter", filter);
json_t *item = json_pack("[]");
json_array_append_new(item, json_string("CalendarEvent/query"));
json_array_append_new(item, eventList);
json_array_append_new(item, json_string(req->tag));
json_array_append_new(req->response, item);
/* Fetch updated records, if requested. */
if (dofetch) {
struct jmap_req subreq = *req;
subreq.args = json_pack("{}");
json_object_set_new(subreq.args, "accountId",
json_string(req->accountid));
json_object_set(subreq.args, "ids", events);
r = getCalendarEvents(&subreq);
json_decref(subreq.args);
}
done:
if (events) json_decref(events);
return r;
}
static int getCalendarPreferences(struct jmap_req *req)
{
/* Just a dummy implementation to make the JMAP web client happy. */
json_t *item = json_pack("[]");
json_t *res = json_pack("{}");
json_array_append_new(item, json_string("CalendarPreference/get"));
json_array_append_new(item, res);
json_object_set_new(res, "accountId", json_string(req->accountid));
json_array_append_new(item, json_string(req->tag));
json_array_append_new(req->response, item);
return 0;
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Apr 6, 1:13 AM (2 d, 12 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18831818
Default Alt Text
(103 KB)

Event Timeline