Page MenuHomePhorge

http_jmap.c
No OneTemporary

Authored By
Unknown
Size
250 KB
Referenced Files
None
Subscribers
None

http_jmap.c

/* http_jmap.c -- Routines for handling JMAP requests in httpd
*
* 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>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <ctype.h>
#include <string.h>
#include <syslog.h>
#include <assert.h>
#include <jansson.h>
#include "acl.h"
#include "annotate.h"
#include "append.h"
#include "caldav_db.h"
#include "carddav_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_proxy.h"
#include "ical_support.h"
#include "imap_err.h"
#include "mailbox.h"
#include "mboxlist.h"
#include "mboxname.h"
#include "statuscache.h"
#include "stristr.h"
#include "times.h"
#include "util.h"
#include "version.h"
#include "xmalloc.h"
#include "xstrlcat.h"
#include "xstrlcpy.h"
#include "zoneinfo_db.h"
/* generated headers are not necessarily in current directory */
#include "imap/http_err.h"
struct jmap_req {
const char *userid;
struct auth_state *authstate;
struct hash_table *idmap;
json_t *args;
json_t *response;
const char *state; // if changing things, this is pre-change state
struct mboxname_counters counters;
const char *tag;
struct transaction_t *txn;
};
struct namespace jmap_namespace;
static time_t compile_time;
static void jmap_init(struct buf *serverinfo);
static void jmap_auth(const char *userid);
static int jmap_get(struct transaction_t *txn, void *params);
static int jmap_post(struct transaction_t *txn, void *params);
static int getMailboxes(struct jmap_req *req);
static int getContactGroups(struct jmap_req *req);
static int getContactGroupUpdates(struct jmap_req *req);
static int setContactGroups(struct jmap_req *req);
static int getContacts(struct jmap_req *req);
static int getContactUpdates(struct jmap_req *req);
static int setContacts(struct jmap_req *req);
static int getCalendars(struct jmap_req *req);
static int getCalendarUpdates(struct jmap_req *req);
static int setCalendars(struct jmap_req *req);
static int getCalendarEvents(struct jmap_req *req);
static int getCalendarEventUpdates(struct jmap_req *req);
static int getCalendarEventList(struct jmap_req *req);
static int setCalendarEvents(struct jmap_req *req);
static int getCalendarPreferences(struct jmap_req *req);
static const struct message_t {
const char *name;
int (*proc)(struct jmap_req *req);
} messages[] = {
{ "getMailboxes", &getMailboxes },
{ "getContactGroups", &getContactGroups },
{ "getContactGroupUpdates", &getContactGroupUpdates },
{ "setContactGroups", &setContactGroups },
{ "getContacts", &getContacts },
{ "getContactUpdates", &getContactUpdates },
{ "setContacts", &setContacts },
{ "getCalendars", &getCalendars },
{ "getCalendarUpdates", &getCalendarUpdates },
{ "setCalendars", &setCalendars },
{ "getCalendarEvents", &getCalendarEvents },
{ "getCalendarEventUpdates",&getCalendarEventUpdates },
{ "getCalendarEventList", &getCalendarEventList },
{ "setCalendarEvents", &setCalendarEvents },
{ "getCalendarPreferences", &getCalendarPreferences },
{ NULL, NULL}
};
/* Namespace for JMAP */
struct namespace_t namespace_jmap = {
URL_NS_JMAP, 0, "/jmap", "/.well-known/jmap", 1 /* auth */,
/*mbtype*/0,
(ALLOW_READ | ALLOW_POST),
&jmap_init, &jmap_auth, NULL, NULL,
{
{ NULL, NULL }, /* ACL */
{ NULL, NULL }, /* COPY */
{ NULL, NULL }, /* DELETE */
{ &jmap_get, NULL }, /* GET */
{ &jmap_get, NULL }, /* HEAD */
{ NULL, NULL }, /* LOCK */
{ NULL, NULL }, /* MKCALENDAR */
{ NULL, NULL }, /* MKCOL */
{ NULL, NULL }, /* MOVE */
{ &meth_options, NULL }, /* OPTIONS */
{ &jmap_post, NULL }, /* POST */
{ NULL, NULL }, /* PROPFIND */
{ NULL, NULL }, /* PROPPATCH */
{ NULL, NULL }, /* PUT */
{ NULL, NULL }, /* REPORT */
{ &meth_trace, NULL }, /* TRACE */
{ NULL, NULL } /* UNLOCK */
}
};
static void jmap_init(struct buf *serverinfo __attribute__((unused)))
{
namespace_jmap.enabled =
config_httpmodules & IMAP_ENUM_HTTPMODULES_JMAP;
if (!namespace_jmap.enabled) return;
compile_time = calc_compile_time(__TIME__, __DATE__);
}
static void jmap_auth(const char *userid __attribute__((unused)))
{
/* Set namespace */
mboxname_init_namespace(&jmap_namespace,
httpd_userisadmin || httpd_userisproxyadmin);
}
/* Perform a GET/HEAD request */
static int jmap_get(struct transaction_t *txn __attribute__((unused)),
void *params __attribute__((unused)))
{
return HTTP_NO_CONTENT;
}
/* Perform a POST request */
static int jmap_post(struct transaction_t *txn,
void *params __attribute__((unused)))
{
const char **hdr;
json_t *req, *resp = NULL;
json_error_t jerr;
const struct message_t *mp = NULL;
struct mailbox *mailbox = NULL;
struct hash_table idmap;
size_t i, flags = JSON_PRESERVE_ORDER;
int ret;
char *buf, *inboxname = NULL;
/* Read body */
txn->req_body.flags |= BODY_DECODE;
ret = http_read_body(httpd_in, httpd_out,
txn->req_hdrs, &txn->req_body, &txn->error.desc);
if (ret) {
txn->flags.conn = CONN_CLOSE;
return ret;
}
if (!buf_len(&txn->req_body.payload)) return HTTP_BAD_REQUEST;
/* Check Content-Type */
if (!(hdr = spool_getheader(txn->req_hdrs, "Content-Type")) ||
!is_mediatype("application/json", hdr[0])) {
txn->error.desc = "This method requires a JSON request body\r\n";
return HTTP_BAD_MEDIATYPE;
}
/* Allocate map to store uids */
construct_hash_table(&idmap, 1024, 0);
/* Parse the JSON request */
req = json_loads(buf_cstring(&txn->req_body.payload), 0, &jerr);
if (!req || !json_is_array(req)) {
txn->error.desc = "Unable to parse JSON request body\r\n";
ret = HTTP_BAD_REQUEST;
goto done;
}
/* Start JSON response */
resp = json_array();
if (!resp) {
txn->error.desc = "Unable to create JSON response body\r\n";
ret = HTTP_SERVER_ERROR;
goto done;
}
inboxname = mboxname_user_mbox(httpd_userid, NULL);
/* we lock the user's INBOX before we start any operation, because that way we
* guarantee (via conversations magic) that nothing changes the modseqs except
* our operations */
int r = mailbox_open_iwl(inboxname, &mailbox);
if (r) {
txn->error.desc = error_message(r);
ret = HTTP_SERVER_ERROR;
goto done;
}
/* Process each message in the request */
for (i = 0; i < json_array_size(req); i++) {
json_t *msg = json_array_get(req, i);
const char *name = json_string_value(json_array_get(msg, 0));
json_t *args = json_array_get(msg, 1);
json_t *id = json_array_get(msg, 2);
/* XXX - better error reporting */
if (!id) continue;
const char *tag = json_string_value(id);
int r = 0;
/* Find the message processor */
for (mp = messages; mp->name && strcmp(name, mp->name); mp++);
if (!mp || !mp->name) {
json_array_append(resp, json_pack("[s {s:s} s]", "error", "type", "unknownMethod", tag));
continue;
}
struct jmap_req req;
req.userid = httpd_userid;
req.authstate = httpd_authstate;
req.args = args;
req.response = resp;
req.tag = tag;
req.idmap = &idmap;
req.txn = txn;
/* Read the modseq counters again, just in case something changed. */
r = mboxname_read_counters(inboxname, &req.counters);
if (r) goto done;
/* XXX - Make also contacts use counters. */
struct buf buf = BUF_INITIALIZER;
buf_printf(&buf, "%llu", req.counters.highestmodseq);
req.state = buf_cstring(&buf);
r = mp->proc(&req);
buf_free(&buf);
if (r) {
txn->error.desc = error_message(r);
ret = HTTP_SERVER_ERROR;
goto done;
}
}
/* unlock here so that we don't block on writing */
mailbox_unlock_index(mailbox, NULL);
/* Dump JSON object into a text buffer */
flags |= (config_httpprettytelemetry ? JSON_INDENT(2) : JSON_COMPACT);
buf = json_dumps(resp, flags);
if (!buf) {
txn->error.desc = "Error dumping JSON response object";
ret = HTTP_SERVER_ERROR;
goto done;
}
/* Output the JSON object */
txn->resp_body.type = "application/json; charset=utf-8";
write_body(HTTP_OK, txn, buf, strlen(buf));
free(buf);
done:
free_hash_table(&idmap, free);
mailbox_close(&mailbox);
free(inboxname);
if (req) json_decref(req);
if (resp) json_decref(resp);
return ret;
}
/* mboxlist_findall() callback to list mailboxes */
int getMailboxes_cb(const char *mboxname, int matchlen __attribute__((unused)),
int maycreate __attribute__((unused)),
void *rock)
{
json_t *list = (json_t *) rock, *mbox;
struct mboxlist_entry *mbentry = NULL;
struct mailbox *mailbox = NULL;
int r = 0, rights;
unsigned statusitems = STATUS_MESSAGES | STATUS_UNSEEN;
struct statusdata sdata;
/* Check ACL on mailbox for current user */
if ((r = mboxlist_lookup(mboxname, &mbentry, NULL))) {
syslog(LOG_INFO, "mboxlist_lookup(%s) failed: %s",
mboxname, error_message(r));
goto done;
}
rights = mbentry->acl ? cyrus_acl_myrights(httpd_authstate, mbentry->acl) : 0;
if ((rights & (ACL_LOOKUP | ACL_READ)) != (ACL_LOOKUP | ACL_READ)) {
goto done;
}
/* Open mailbox to get uniqueid */
if ((r = mailbox_open_irl(mboxname, &mailbox))) {
syslog(LOG_INFO, "mailbox_open_irl(%s) failed: %s",
mboxname, error_message(r));
goto done;
}
mailbox_unlock_index(mailbox, NULL);
r = status_lookup(mboxname, httpd_userid, statusitems, &sdata);
mbox = json_pack("{s:s s:s s:n s:n s:b s:b s:b s:b s:i s:i}",
"id", mailbox->uniqueid,
"name", mboxname,
"parentId",
"role",
"mayAddMessages", rights & ACL_INSERT,
"mayRemoveMessages", rights & ACL_DELETEMSG,
"mayCreateChild", rights & ACL_CREATE,
"mayDeleteMailbox", rights & ACL_DELETEMBOX,
"totalMessages", sdata.messages,
"unreadMessages", sdata.unseen);
json_array_append_new(list, mbox);
mailbox_close(&mailbox);
done:
return 0;
}
/* Execute a getMailboxes message */
static int getMailboxes(struct jmap_req *req)
{
json_t *item, *mailboxes, *list;
/* Start constructing our response */
item = json_pack("[s {s:s s:s} s]", "mailboxes",
"accountId", req->userid,
"state", req->state,
req->tag);
list = json_array();
/* Generate list of mailboxes */
int isadmin = httpd_userisadmin||httpd_userisproxyadmin;
mboxlist_findall(&jmap_namespace, "*", isadmin, httpd_userid,
httpd_authstate, &getMailboxes_cb, list);
mailboxes = json_array_get(item, 1);
json_object_set_new(mailboxes, "list", list);
/* xxx - args */
json_object_set_new(mailboxes, "notFound", json_null());
json_array_append_new(req->response, item);
return 0;
}
static void _add_xhref(json_t *obj, const char *mboxname, const char *resource)
{
/* XXX - look up root path from namespace? */
struct buf buf = BUF_INITIALIZER;
char *userid = mboxname_to_userid(mboxname);
const char *prefix = NULL;
if (mboxname_isaddressbookmailbox(mboxname, 0)) {
prefix = namespace_addressbook.prefix;
}
else if (mboxname_iscalendarmailbox(mboxname, 0)) {
prefix = namespace_calendar.prefix;
}
if (strchr(userid, '@') || !httpd_extradomain) {
buf_printf(&buf, "%s/user/%s/%s",
prefix, userid, strrchr(mboxname, '.')+1);
}
else {
buf_printf(&buf, "%s/user/%s@%s/%s",
prefix, userid, httpd_extradomain, strrchr(mboxname, '.')+1);
}
if (resource)
buf_printf(&buf, "/%s", resource);
json_object_set_new(obj, "x-href", json_string(buf_cstring(&buf)));
free(userid);
buf_free(&buf);
}
struct cards_rock {
struct jmap_req *req;
json_t *array;
struct hash_table *props;
struct mailbox *mailbox;
int rows;
};
static int getgroups_cb(void *rock, struct carddav_data *cdata)
{
struct cards_rock *crock = (struct cards_rock *) rock;
struct index_record record;
int r;
if (!crock->mailbox || strcmp(crock->mailbox->name, cdata->dav.mailbox)) {
mailbox_close(&crock->mailbox);
r = mailbox_open_irl(cdata->dav.mailbox, &crock->mailbox);
if (r) return r;
}
r = mailbox_find_index_record(crock->mailbox, cdata->dav.imap_uid, &record);
if (r) return r;
crock->rows++;
/* XXX - this could definitely be refactored from here and mailbox.c */
struct buf msg_buf = BUF_INITIALIZER;
struct vparse_state vparser;
struct vparse_entry *ventry = NULL;
/* Load message containing the resource and parse vcard data */
r = mailbox_map_record(crock->mailbox, &record, &msg_buf);
if (r) return r;
memset(&vparser, 0, sizeof(struct vparse_state));
vparser.base = buf_cstring(&msg_buf) + record.header_size;
r = vparse_parse(&vparser, 0);
buf_free(&msg_buf);
if (r) return r;
if (!vparser.card || !vparser.card->objects) {
vparse_free(&vparser);
return r;
}
struct vparse_card *vcard = vparser.card->objects;
json_t *obj = json_pack("{}");
json_object_set_new(obj, "id", json_string(cdata->vcard_uid));
json_object_set_new(obj, "addressbookId",
json_string(strrchr(cdata->dav.mailbox, '.')+1));
json_t *contactids = json_pack("[]");
json_t *otherids = json_pack("{}");
_add_xhref(obj, cdata->dav.mailbox, cdata->dav.resource);
for (ventry = vcard->properties; ventry; ventry = ventry->next) {
const char *name = ventry->name;
const char *propval = ventry->v.value;
if (!name) continue;
if (!propval) continue;
if (!strcmp(name, "fn")) {
json_object_set_new(obj, "name", json_string(propval));
}
else if (!strcmp(name, "x-addressbookserver-member")) {
if (strncmp(propval, "urn:uuid:", 9)) continue;
json_array_append_new(contactids, json_string(propval+9));
}
else if (!strcmp(name, "x-fm-otheraccount-member")) {
if (strncmp(propval, "urn:uuid:", 9)) continue;
struct vparse_param *param = vparse_get_param(ventry, "userid");
if (!param) continue;
json_t *object = json_object_get(otherids, param->value);
if (!object) {
object = json_array();
json_object_set_new(otherids, param->value, object);
}
json_array_append_new(object, json_string(propval+9));
}
}
json_object_set_new(obj, "contactIds", contactids);
json_object_set_new(obj, "otherAccountContactIds", otherids);
json_array_append_new(crock->array, obj);
return 0;
}
static int jmap_contacts_get(struct jmap_req *req, carddav_cb_t *cb,
int kind, const char *resname)
{
struct carddav_db *db = carddav_open_userid(req->userid);
if (!db) return -1;
char *mboxname = NULL;
json_t *abookid = json_object_get(req->args, "addressbookId");
if (abookid && json_string_value(abookid)) {
/* XXX - invalid arguments */
const char *addressbookId = json_string_value(abookid);
mboxname = carddav_mboxname(req->userid, addressbookId);
}
struct cards_rock rock;
int r = 0;
rock.array = json_pack("[]");
rock.props = NULL;
rock.mailbox = NULL;
json_t *want = json_object_get(req->args, "ids");
json_t *notFound = json_array();
if (want) {
int i;
int size = json_array_size(want);
for (i = 0; i < size; i++) {
rock.rows = 0;
const char *id = json_string_value(json_array_get(want, i));
if (!id) continue;
r = carddav_get_cards(db, mboxname, id, kind, cb, &rock);
if (r || !rock.rows) {
json_array_append_new(notFound, json_string(id));
}
}
}
else {
rock.rows = 0;
r = carddav_get_cards(db, mboxname, NULL, kind, cb, &rock);
}
if (r) goto done;
json_t *toplevel = json_pack("{}");
json_object_set_new(toplevel, "accountId", json_string(req->userid));
json_object_set_new(toplevel, "state", json_string(req->state));
json_object_set_new(toplevel, "list", rock.array);
if (json_array_size(notFound)) {
json_object_set_new(toplevel, "notFound", notFound);
}
else {
json_decref(notFound);
json_object_set_new(toplevel, "notFound", json_null());
}
json_t *item = json_pack("[]");
json_array_append_new(item, json_string(resname));
json_array_append_new(item, toplevel);
json_array_append_new(item, json_string(req->tag));
json_array_append_new(req->response, item);
done:
free(mboxname);
mailbox_close(&rock.mailbox);
carddav_close(db);
return r;
}
static int getContactGroups(struct jmap_req *req)
{
return jmap_contacts_get(req, &getgroups_cb, CARDDAV_KIND_GROUP, "contactGroups");
}
static const char *_json_object_get_string(const json_t *obj, const char *key)
{
const json_t *jval = json_object_get(obj, key);
if (!jval) return NULL;
const char *val = json_string_value(jval);
return val;
}
static const char *_json_array_get_string(const json_t *obj, size_t index)
{
const json_t *jval = json_array_get(obj, index);
if (!jval) return NULL;
const char *val = json_string_value(jval);
return val;
}
struct updates_rock {
json_t *changed;
json_t *removed;
size_t seen_records;
size_t max_records;
struct mailbox *mailbox;
short fetchmodseq;
modseq_t highestmodseq;
};
static void strip_spurious_deletes(struct updates_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 void updates_rock_update(struct updates_rock *rock,
struct dav_data dav,
const char *uid) {
/* Count, but don't process items that exceed the maximum record count. */
if (rock->max_records && ++(rock->seen_records) > rock->max_records) {
return;
}
/* Report item as updated or removed. */
if (dav.alive) {
json_array_append_new(rock->changed, json_string(uid));
}
else {
json_array_append_new(rock->removed, json_string(uid));
}
/* Fetch record to determine modseq. */
if (rock->fetchmodseq) {
struct index_record record;
int r;
if (!rock->mailbox || strcmp(rock->mailbox->name, dav.mailbox)) {
mailbox_close(&rock->mailbox);
r = mailbox_open_irl(dav.mailbox, &rock->mailbox);
if (r) {
syslog(LOG_INFO, "mailbox_open_irl(%s) failed: %s",
dav.mailbox, error_message(r));
return;
}
}
r = mailbox_find_index_record(rock->mailbox, dav.imap_uid, &record);
if (r) {
syslog(LOG_INFO, "mailbox_find_index_record(%s,%d) failed: %s",
rock->mailbox->name, dav.imap_uid, error_message(r));
mailbox_close(&rock->mailbox);
return;
}
if (record.modseq > rock->highestmodseq) {
rock->highestmodseq = record.modseq;
}
}
}
static int getcontactupdates_cb(void *rock, struct carddav_data *cdata)
{
struct updates_rock *urock = (struct updates_rock *) rock;
updates_rock_update(urock, cdata->dav, cdata->vcard_uid);
return 0;
}
static int geteventupdates_cb(void *rock, struct caldav_data *cdata)
{
struct updates_rock *urock = (struct updates_rock *) rock;
updates_rock_update(urock, cdata->dav, cdata->ical_uid);
return 0;
}
static int getContactGroupUpdates(struct jmap_req *req)
{
struct carddav_db *db = carddav_open_userid(req->userid);
if (!db) return -1;
int r = -1;
const char *since = _json_object_get_string(req->args, "sinceState");
if (!since) goto done;
modseq_t oldmodseq = str2uint64(since);
char *mboxname = NULL;
json_t *abookid = json_object_get(req->args, "addressbookId");
if (abookid && json_string_value(abookid)) {
/* XXX - invalid arguments */
const char *addressbookId = json_string_value(abookid);
mboxname = carddav_mboxname(req->userid, addressbookId);
}
struct updates_rock rock;
rock.changed = json_array();
rock.removed = json_array();
r = carddav_get_updates(db, oldmodseq, mboxname, CARDDAV_KIND_GROUP,
&getcontactupdates_cb, &rock);
if (r) goto done;
strip_spurious_deletes(&rock);
json_t *contactGroupUpdates = json_pack("{}");
json_object_set_new(contactGroupUpdates, "accountId",
json_string(req->userid));
json_object_set_new(contactGroupUpdates, "oldState",
json_string(since)); // XXX - just use refcounted
json_object_set_new(contactGroupUpdates, "newState",
json_string(req->state));
json_object_set(contactGroupUpdates, "changed", rock.changed);
json_object_set(contactGroupUpdates, "removed", rock.removed);
json_t *item = json_pack("[]");
json_array_append_new(item, json_string("contactGroupUpdates"));
json_array_append_new(item, contactGroupUpdates);
json_array_append_new(item, json_string(req->tag));
json_array_append_new(req->response, item);
json_t *dofetch = json_object_get(req->args, "fetchContactGroups");
if (dofetch && json_is_true(dofetch) && json_array_size(rock.changed)) {
struct jmap_req subreq = *req; // struct copy, woot
subreq.args = json_pack("{}");
json_object_set(subreq.args, "ids", rock.changed);
if (abookid) {
json_object_set(subreq.args, "addressbookId", abookid);
}
r = getContactGroups(&subreq);
json_decref(subreq.args);
}
json_decref(rock.changed);
json_decref(rock.removed);
done:
carddav_close(db);
return r;
}
static const char *_resolveid(struct jmap_req *req, const char *id)
{
const char *newid = hash_lookup(id, req->idmap);
if (newid) return newid;
return id;
}
static int _add_group_entries(struct jmap_req *req,
struct vparse_card *card, json_t *members)
{
vparse_delete_entries(card, NULL, "X-ADDRESSBOOKSERVER-MEMBER");
int r = 0;
size_t index;
struct buf buf = BUF_INITIALIZER;
for (index = 0; index < json_array_size(members); index++) {
const char *item = _json_array_get_string(members, index);
if (!item) continue;
const char *uid = _resolveid(req, item);
buf_setcstr(&buf, "urn:uuid:");
buf_appendcstr(&buf, uid);
vparse_add_entry(card, NULL,
"X-ADDRESSBOOKSERVER-MEMBER", buf_cstring(&buf));
}
buf_free(&buf);
return r;
}
static int _add_othergroup_entries(struct jmap_req *req,
struct vparse_card *card, json_t *members)
{
vparse_delete_entries(card, NULL, "X-FM-OTHERACCOUNT-MEMBER");
int r = 0;
struct buf buf = BUF_INITIALIZER;
const char *key;
json_t *arg;
json_object_foreach(members, key, arg) {
unsigned i;
for (i = 0; i < json_array_size(arg); i++) {
const char *item = json_string_value(json_array_get(arg, i));
if (!item)
return -1;
const char *uid = _resolveid(req, item);
buf_setcstr(&buf, "urn:uuid:");
buf_appendcstr(&buf, uid);
struct vparse_entry *entry =
vparse_add_entry(card, NULL,
"X-FM-OTHERACCOUNT-MEMBER", buf_cstring(&buf));
vparse_add_param(entry, "userid", key);
}
}
buf_free(&buf);
return r;
}
static int setContactGroups(struct jmap_req *req)
{
struct mailbox *mailbox = NULL;
struct mailbox *newmailbox = NULL;
struct carddav_db *db = carddav_open_userid(req->userid);
if (!db) return -1;
int r = 0;
json_t *jcheckState = json_object_get(req->args, "ifInState");
if (jcheckState) {
const char *checkState = json_string_value(jcheckState);
if (!checkState ||strcmp(req->state, checkState)) {
json_t *item = json_pack("[s, {s:s}, s]",
"error", "type", "stateMismatch", req->tag);
json_array_append_new(req->response, item);
goto done;
}
}
json_t *set = json_pack("{s:s,s:s}",
"oldState", req->state,
"accountId", req->userid);
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) {
const char *uid = makeuuid();
json_t *jname = json_object_get(arg, "name");
if (!jname) {
/* XXX - missingParameters should be an invalidProperties
* error. Fix this when the contacts error handling code gets
* merged with the calendar codebase. */
json_t *err = json_pack("{s:s}", "type", "missingParameters");
json_object_set_new(notCreated, key, err);
continue;
}
const char *name = json_string_value(jname);
if (!name) {
json_t *err = json_pack("{s:s}", "type", "invalidArguments");
json_object_set_new(notCreated, key, err);
continue;
}
// XXX - no name => notCreated
struct vparse_card *card = vparse_new_card("VCARD");
vparse_add_entry(card, NULL, "VERSION", "3.0");
vparse_add_entry(card, NULL, "FN", name);
vparse_add_entry(card, NULL, "UID", uid);
vparse_add_entry(card, NULL, "X-ADDRESSBOOKSERVER-KIND", "group");
/* it's legal to create an empty group */
json_t *members = json_object_get(arg, "contactIds");
if (members) {
r = _add_group_entries(req, card, members);
if (r) {
/* this one is legit -
it just means we'll be adding an error instead */
r = 0;
json_t *err = json_pack("{s:s}",
"type", "invalidContactId");
json_object_set_new(notCreated, key, err);
vparse_free_card(card);
continue;
}
}
/* it's legal to create an empty group */
json_t *others = json_object_get(arg, "otherAccountContactIds");
if (others) {
r = _add_othergroup_entries(req, card, others);
if (r) {
/* this one is legit -
it just means we'll be adding an error instead */
r = 0;
json_t *err = json_pack("{s:s}",
"type", "invalidContactId");
json_object_set_new(notCreated, key, err);
vparse_free_card(card);
continue;
}
}
const char *addressbookId = "Default";
json_t *abookid = json_object_get(arg, "addressbookId");
if (abookid && json_string_value(abookid)) {
/* XXX - invalid arguments */
addressbookId = json_string_value(abookid);
}
const char *mboxname = mboxname_abook(req->userid, addressbookId);
json_object_del(arg, "addressbookId");
addressbookId = NULL;
/* we need to create and append a record */
if (!mailbox || strcmp(mailbox->name, mboxname)) {
mailbox_close(&mailbox);
r = mailbox_open_iwl(mboxname, &mailbox);
}
syslog(LOG_NOTICE, "jmap: create group %s/%s/%s (%s)",
req->userid, mboxname, uid, name);
if (!r) r = carddav_store(mailbox, card, NULL, NULL, NULL,
req->userid, req->authstate, ignorequota);
vparse_free_card(card);
if (r) {
/* these are real "should never happen" errors */
goto done;
}
record = json_pack("{s:s}", "id", uid);
json_object_set_new(created, key, record);
/* hash_insert takes ownership of uid here, skanky I know */
hash_insert(key, xstrdup(uid), req->idmap);
}
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) {
struct carddav_data *cdata = NULL;
r = carddav_lookup_uid(db, uid, &cdata);
uint32_t olduid;
char *resource = NULL;
/* is it a valid group? */
if (r || !cdata || !cdata->dav.imap_uid || !cdata->dav.resource
|| cdata->kind != CARDDAV_KIND_GROUP) {
r = 0;
json_t *err = json_pack("{s:s}", "type", "notFound");
json_object_set_new(notUpdated, uid, err);
continue;
}
olduid = cdata->dav.imap_uid;
resource = xstrdup(cdata->dav.resource);
if (!mailbox || strcmp(mailbox->name, cdata->dav.mailbox)) {
mailbox_close(&mailbox);
r = mailbox_open_iwl(cdata->dav.mailbox, &mailbox);
if (r) {
syslog(LOG_ERR, "IOERROR: failed to open %s",
cdata->dav.mailbox);
goto done;
}
}
json_t *abookid = json_object_get(arg, "addressbookId");
if (abookid && json_string_value(abookid)) {
const char *mboxname =
mboxname_abook(req->userid, json_string_value(abookid));
if (strcmp(mboxname, cdata->dav.mailbox)) {
/* move */
r = mailbox_open_iwl(mboxname, &newmailbox);
if (r) {
syslog(LOG_ERR, "IOERROR: failed to open %s", mboxname);
goto done;
}
}
json_object_del(arg, "addressbookId");
}
/* XXX - this could definitely be refactored from here and mailbox.c */
struct buf msg_buf = BUF_INITIALIZER;
struct vparse_state vparser;
struct index_record record;
r = mailbox_find_index_record(mailbox,
cdata->dav.imap_uid, &record);
if (r) goto done;
/* Load message containing the resource and parse vcard data */
r = mailbox_map_record(mailbox, &record, &msg_buf);
if (r) goto done;
memset(&vparser, 0, sizeof(struct vparse_state));
vparser.base = buf_cstring(&msg_buf) + record.header_size;
vparse_set_multival(&vparser, "adr");
vparse_set_multival(&vparser, "org");
vparse_set_multival(&vparser, "n");
r = vparse_parse(&vparser, 0);
buf_free(&msg_buf);
if (r || !vparser.card || !vparser.card->objects) {
json_t *err = json_pack("{s:s}", "type", "parseError");
json_object_set_new(notUpdated, uid, err);
vparse_free(&vparser);
mailbox_close(&newmailbox);
continue;
}
struct vparse_card *card = vparser.card->objects;
json_t *namep = json_object_get(arg, "name");
if (namep) {
const char *name = json_string_value(namep);
if (!name) {
json_t *err = json_pack("{s:s}",
"type", "invalidArguments");
json_object_set_new(notUpdated, uid, err);
vparse_free(&vparser);
mailbox_close(&newmailbox);
continue;
}
struct vparse_entry *entry = vparse_get_entry(card, NULL, "FN");
if (entry) {
free(entry->v.value);
entry->v.value = xstrdup(name);
}
else {
vparse_add_entry(card, NULL, "FN", name);
}
}
json_t *members = json_object_get(arg, "contactIds");
if (members) {
r = _add_group_entries(req, card, members);
if (r) {
/* this one is legit -
it just means we'll be adding an error instead */
r = 0;
json_t *err = json_pack("{s:s}",
"type", "invalidContactId");
json_object_set_new(notUpdated, uid, err);
vparse_free(&vparser);
mailbox_close(&newmailbox);
continue;
}
}
json_t *others = json_object_get(arg, "otherAccountContactIds");
if (others) {
r = _add_othergroup_entries(req, card, others);
if (r) {
/* this one is legit -
it just means we'll be adding an error instead */
r = 0;
json_t *err = json_pack("{s:s}",
"type", "invalidContactId");
json_object_set_new(notUpdated, uid, err);
vparse_free(&vparser);
mailbox_close(&newmailbox);
continue;
}
}
syslog(LOG_NOTICE, "jmap: update group %s/%s",
req->userid, resource);
r = carddav_store(newmailbox ? newmailbox : mailbox, card, resource,
NULL, NULL, req->userid, req->authstate, ignorequota);
if (!r)
r = carddav_remove(mailbox, olduid, /*isreplace*/!newmailbox);
mailbox_close(&newmailbox);
vparse_free(&vparser);
free(resource);
if (r) goto done;
json_array_append_new(updated, json_string(uid));
}
if (json_array_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;
for (index = 0; index < json_array_size(destroy); index++) {
const char *uid = _json_array_get_string(destroy, index);
if (!uid) {
json_t *err = json_pack("{s:s}", "type", "invalidArguments");
json_object_set_new(notDestroyed, uid, err);
continue;
}
struct carddav_data *cdata = NULL;
uint32_t olduid;
r = carddav_lookup_uid(db, uid, &cdata);
/* is it a valid group? */
if (r || !cdata ||
!cdata->dav.imap_uid || cdata->kind != CARDDAV_KIND_GROUP) {
r = 0;
json_t *err = json_pack("{s:s}", "type", "notFound");
json_object_set_new(notDestroyed, uid, err);
continue;
}
olduid = cdata->dav.imap_uid;
if (!mailbox || strcmp(mailbox->name, cdata->dav.mailbox)) {
mailbox_close(&mailbox);
r = mailbox_open_iwl(cdata->dav.mailbox, &mailbox);
if (r) goto done;
}
/* XXX - alive check */
syslog(LOG_NOTICE, "jmap: destroy group %s (%s)", req->userid, uid);
r = carddav_remove(mailbox, olduid, /*isreplace*/0);
if (r) {
syslog(LOG_ERR,
"IOERROR: setContactGroups remove failed for %s %u",
mailbox->name, cdata->dav.imap_uid);
goto done;
}
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);
}
/* force modseq to stable */
if (mailbox) mailbox_unlock_index(mailbox, NULL);
/* read the modseq again every time, just in case something changed it
* in our actions */
struct buf buf = BUF_INITIALIZER;
const char *inboxname = mboxname_user_mbox(req->userid, NULL);
modseq_t modseq = mboxname_readmodseq(inboxname);
buf_printf(&buf, "%llu", modseq);
json_object_set_new(set, "newState", json_string(buf_cstring(&buf)));
buf_free(&buf);
json_t *item = json_pack("[]");
json_array_append_new(item, json_string("contactGroupsSet"));
json_array_append_new(item, set);
json_array_append_new(item, json_string(req->tag));
json_array_append_new(req->response, item);
done:
mailbox_close(&newmailbox);
mailbox_close(&mailbox);
carddav_close(db);
return r;
}
static int _wantprop(hash_table *props, const char *name)
{
if (!props) return 1;
if (hash_lookup(name, props)) return 1;
return 0;
}
/* convert YYYY-MM-DD to separate y,m,d */
static int _parse_date(const char *date, unsigned *y, unsigned *m, unsigned *d)
{
/* there isn't a convenient libc function that will let us convert parts of
* a string to integer and only take digit characters, so we just pull it
* apart ourselves */
/* format check. no need to strlen() beforehand, it will fall out of this */
if (date[0] < '0' || date[0] > '9' ||
date[1] < '0' || date[1] > '9' ||
date[2] < '0' || date[2] > '9' ||
date[3] < '0' || date[3] > '9' ||
date[4] != '-' ||
date[5] < '0' || date[5] > '9' ||
date[6] < '0' || date[6] > '9' ||
date[7] != '-' ||
date[8] < '0' || date[8] > '9' ||
date[9] < '0' || date[9] > '9' ||
date[10] != '\0')
return -1;
/* convert to integer. ascii digits are 0x30-0x37, so we can take bottom
* four bits and multiply */
*y =
(date[0] & 0xf) * 1000 +
(date[1] & 0xf) * 100 +
(date[2] & 0xf) * 10 +
(date[3] & 0xf);
*m =
(date[5] & 0xf) * 10 +
(date[6] & 0xf);
*d =
(date[8] & 0xf) * 10 +
(date[9] & 0xf);
return 0;
}
static void _date_to_jmap(struct vparse_entry *entry, struct buf *buf)
{
if (!entry)
goto no_date;
unsigned y, m, d;
if (_parse_date(entry->v.value, &y, &m, &d))
goto no_date;
if (y < 1604 || m > 12 || d > 31)
goto no_date;
const struct vparse_param *param;
for (param = entry->params; param; param = param->next) {
if (!strcasecmp(param->name, "x-apple-omit-year"))
/* XXX compare value with actual year? */
y = 0;
if (!strcasecmp(param->name, "x-fm-no-month"))
m = 0;
if (!strcasecmp(param->name, "x-fm-no-day"))
d = 0;
}
/* sigh, magic year 1604 has been seen without X-APPLE-OMIT-YEAR, making
* me wonder what the bloody point is */
if (y == 1604)
y = 0;
buf_reset(buf);
buf_printf(buf, "%04d-%02d-%02d", y, m, d);
return;
no_date:
buf_setcstr(buf, "0000-00-00");
}
static const char *_servicetype(const char *type)
{
/* add new services here */
if (!strcasecmp(type, "aim")) return "AIM";
if (!strcasecmp(type, "facebook")) return "Facebook";
if (!strcasecmp(type, "flickr")) return "Flickr";
if (!strcasecmp(type, "gadugadu")) return "GaduGadu";
if (!strcasecmp(type, "github")) return "GitHub";
if (!strcasecmp(type, "googletalk")) return "GoogleTalk";
if (!strcasecmp(type, "icq")) return "ICQ";
if (!strcasecmp(type, "jabber")) return "Jabber";
if (!strcasecmp(type, "linkedin")) return "LinkedIn";
if (!strcasecmp(type, "msn")) return "MSN";
if (!strcasecmp(type, "myspace")) return "MySpace";
if (!strcasecmp(type, "qq")) return "QQ";
if (!strcasecmp(type, "skype")) return "Skype";
if (!strcasecmp(type, "twitter")) return "Twitter";
if (!strcasecmp(type, "yahoo")) return "Yahoo";
syslog(LOG_NOTICE, "unknown service type %s", type);
return type;
}
static int getcontacts_cb(void *rock, struct carddav_data *cdata)
{
struct cards_rock *crock = (struct cards_rock *) rock;
struct index_record record;
strarray_t *empty = NULL;
int r = 0;
if (!crock->mailbox || strcmp(crock->mailbox->name, cdata->dav.mailbox)) {
mailbox_close(&crock->mailbox);
r = mailbox_open_irl(cdata->dav.mailbox, &crock->mailbox);
if (r) return r;
}
r = mailbox_find_index_record(crock->mailbox, cdata->dav.imap_uid, &record);
if (r) return r;
crock->rows++;
/* XXX - this could definitely be refactored from here and mailbox.c */
struct buf msg_buf = BUF_INITIALIZER;
struct vparse_state vparser;
/* Load message containing the resource and parse vcard data */
r = mailbox_map_record(crock->mailbox, &record, &msg_buf);
if (r) return r;
memset(&vparser, 0, sizeof(struct vparse_state));
vparser.base = buf_cstring(&msg_buf) + record.header_size;
vparse_set_multival(&vparser, "adr");
vparse_set_multival(&vparser, "org");
vparse_set_multival(&vparser, "n");
r = vparse_parse(&vparser, 0);
buf_free(&msg_buf);
if (r || !vparser.card || !vparser.card->objects) {
vparse_free(&vparser);
return r;
}
struct vparse_card *card = vparser.card->objects;
json_t *obj = json_pack("{}");
json_object_set_new(obj, "id", json_string(cdata->vcard_uid));
json_object_set_new(obj, "addressbookId",
json_string(strrchr(cdata->dav.mailbox, '.')+1));
if (_wantprop(crock->props, "isFlagged")) {
json_object_set_new(obj, "isFlagged",
record.system_flags & FLAG_FLAGGED ? json_true() :
json_false());
}
struct buf buf = BUF_INITIALIZER;
if (_wantprop(crock->props, "x-href")) {
_add_xhref(obj, cdata->dav.mailbox, cdata->dav.resource);
}
if (_wantprop(crock->props, "x-importance")) {
double val = 0;
const char *ns = DAV_ANNOT_NS "<" XML_NS_CYRUS ">importance";
buf_reset(&buf);
annotatemore_msg_lookup(crock->mailbox->name, record.uid,
ns, "", &buf);
if (buf.len)
val = strtod(buf_cstring(&buf), NULL);
json_object_set_new(obj, "x-importance", json_real(val));
}
const strarray_t *n = vparse_multival(card, "n");
const strarray_t *org = vparse_multival(card, "org");
if (!n) n = empty ? empty : (empty = strarray_new());
if (!org) org = empty ? empty : (empty = strarray_new());
/* name fields: Family; Given; Middle; Prefix; Suffix. */
if (_wantprop(crock->props, "lastName")) {
const char *family = strarray_safenth(n, 0);
const char *suffix = strarray_safenth(n, 4);
buf_setcstr(&buf, family);
if (*suffix) {
buf_putc(&buf, ' ');
buf_appendcstr(&buf, suffix);
}
json_object_set_new(obj, "lastName", json_string(buf_cstring(&buf)));
}
if (_wantprop(crock->props, "firstName")) {
const char *given = strarray_safenth(n, 1);
const char *middle = strarray_safenth(n, 2);
buf_setcstr(&buf, given);
if (*middle) {
buf_putc(&buf, ' ');
buf_appendcstr(&buf, middle);
}
json_object_set_new(obj, "firstName", json_string(buf_cstring(&buf)));
}
if (_wantprop(crock->props, "prefix")) {
const char *prefix = strarray_safenth(n, 3);
json_object_set_new(obj, "prefix",
json_string(prefix)); /* just prefix */
}
/* org fields */
if (_wantprop(crock->props, "company"))
json_object_set_new(obj, "company",
json_string(strarray_safenth(org, 0)));
if (_wantprop(crock->props, "department"))
json_object_set_new(obj, "department",
json_string(strarray_safenth(org, 1)));
if (_wantprop(crock->props, "jobTitle"))
json_object_set_new(obj, "jobTitle",
json_string(strarray_safenth(org, 2)));
/* XXX - position? */
/* address - we need to open code this, because it's repeated */
if (_wantprop(crock->props, "addresses")) {
json_t *adr = json_array();
struct vparse_entry *entry;
for (entry = card->properties; entry; entry = entry->next) {
if (strcasecmp(entry->name, "adr")) continue;
json_t *item = json_pack("{}");
/* XXX - type and label */
const strarray_t *a = entry->v.values;
const struct vparse_param *param;
const char *type = "other";
const char *label = NULL;
for (param = entry->params; param; param = param->next) {
if (!strcasecmp(param->name, "type")) {
if (!strcasecmp(param->value, "home")) {
type = "home";
}
else if (!strcasecmp(param->value, "work")) {
type = "work";
}
else if (!strcasecmp(param->value, "billing")) {
type = "billing";
}
else if (!strcasecmp(param->value, "postal")) {
type = "postal";
}
}
else if (!strcasecmp(param->name, "label")) {
label = param->value;
}
}
json_object_set_new(item, "type", json_string(type));
if (label) json_object_set_new(item, "label", json_string(label));
const char *pobox = strarray_safenth(a, 0);
const char *extended = strarray_safenth(a, 1);
const char *street = strarray_safenth(a, 2);
buf_reset(&buf);
if (*pobox) {
buf_appendcstr(&buf, pobox);
if (extended || street) buf_putc(&buf, '\n');
}
if (*extended) {
buf_appendcstr(&buf, extended);
if (street) buf_putc(&buf, '\n');
}
if (*street) {
buf_appendcstr(&buf, street);
}
json_object_set_new(item, "street",
json_string(buf_cstring(&buf)));
json_object_set_new(item, "locality",
json_string(strarray_safenth(a, 3)));
json_object_set_new(item, "region",
json_string(strarray_safenth(a, 4)));
json_object_set_new(item, "postcode",
json_string(strarray_safenth(a, 5)));
json_object_set_new(item, "country",
json_string(strarray_safenth(a, 6)));
json_array_append_new(adr, item);
}
json_object_set_new(obj, "addresses", adr);
}
/* address - we need to open code this, because it's repeated */
if (_wantprop(crock->props, "emails")) {
json_t *emails = json_array();
struct vparse_entry *entry;
int defaultIndex = -1;
int i = 0;
for (entry = card->properties; entry; entry = entry->next) {
if (strcasecmp(entry->name, "email")) continue;
json_t *item = json_pack("{}");
const struct vparse_param *param;
const char *type = "other";
const char *label = NULL;
for (param = entry->params; param; param = param->next) {
if (!strcasecmp(param->name, "type")) {
if (!strcasecmp(param->value, "home")) {
type = "personal";
}
else if (!strcasecmp(param->value, "work")) {
type = "work";
}
else if (!strcasecmp(param->value, "pref")) {
if (defaultIndex < 0)
defaultIndex = i;
}
}
else if (!strcasecmp(param->name, "label")) {
label = param->value;
}
}
json_object_set_new(item, "type", json_string(type));
if (label) json_object_set_new(item, "label", json_string(label));
json_object_set_new(item, "value", json_string(entry->v.value));
json_array_append_new(emails, item);
i++;
}
if (defaultIndex < 0)
defaultIndex = 0;
int size = json_array_size(emails);
for (i = 0; i < size; i++) {
json_t *item = json_array_get(emails, i);
json_object_set_new(item, "isDefault",
i == defaultIndex ? json_true() : json_false());
}
json_object_set_new(obj, "emails", emails);
}
/* address - we need to open code this, because it's repeated */
if (_wantprop(crock->props, "phones")) {
json_t *phones = json_array();
struct vparse_entry *entry;
for (entry = card->properties; entry; entry = entry->next) {
if (strcasecmp(entry->name, "tel")) continue;
json_t *item = json_pack("{}");
const struct vparse_param *param;
const char *type = "other";
const char *label = NULL;
for (param = entry->params; param; param = param->next) {
if (!strcasecmp(param->name, "type")) {
if (!strcasecmp(param->value, "home")) {
type = "home";
}
else if (!strcasecmp(param->value, "work")) {
type = "work";
}
else if (!strcasecmp(param->value, "cell")) {
type = "mobile";
}
else if (!strcasecmp(param->value, "mobile")) {
type = "mobile";
}
else if (!strcasecmp(param->value, "fax")) {
type = "fax";
}
else if (!strcasecmp(param->value, "pager")) {
type = "pager";
}
}
else if (!strcasecmp(param->name, "label")) {
label = param->value;
}
}
json_object_set_new(item, "type", json_string(type));
if (label) json_object_set_new(item, "label", json_string(label));
json_object_set_new(item, "value", json_string(entry->v.value));
json_array_append_new(phones, item);
}
json_object_set_new(obj, "phones", phones);
}
/* address - we need to open code this, because it's repeated */
if (_wantprop(crock->props, "online")) {
json_t *online = json_array();
struct vparse_entry *entry;
for (entry = card->properties; entry; entry = entry->next) {
if (!strcasecmp(entry->name, "url")) {
json_t *item = json_pack("{}");
const struct vparse_param *param;
const char *label = NULL;
for (param = entry->params; param; param = param->next) {
if (!strcasecmp(param->name, "label")) {
label = param->value;
}
}
json_object_set_new(item, "type", json_string("uri"));
if (label) json_object_set_new(item, "label", json_string(label));
json_object_set_new(item, "value", json_string(entry->v.value));
json_array_append_new(online, item);
}
if (!strcasecmp(entry->name, "impp")) {
json_t *item = json_pack("{}");
const struct vparse_param *param;
const char *label = NULL;
for (param = entry->params; param; param = param->next) {
if (!strcasecmp(param->name, "x-service-type")) {
label = _servicetype(param->value);
}
}
json_object_set_new(item, "type", json_string("username"));
if (label) json_object_set_new(item, "label", json_string(label));
json_object_set_new(item, "value", json_string(entry->v.value));
json_array_append_new(online, item);
}
if (!strcasecmp(entry->name, "x-social-profile")) {
json_t *item = json_pack("{}");
const struct vparse_param *param;
const char *label = NULL;
const char *value = NULL;
for (param = entry->params; param; param = param->next) {
if (!strcasecmp(param->name, "type")) {
label = _servicetype(param->value);
}
if (!strcasecmp(param->name, "x-user")) {
value = param->value;
}
}
json_object_set_new(item, "type", json_string("username"));
if (label) json_object_set_new(item, "label", json_string(label));
json_object_set_new(item, "value",
json_string(value ? value : entry->v.value));
json_array_append_new(online, item);
}
if (!strcasecmp(entry->name, "x-fm-online-other")) {
json_t *item = json_pack("{}");
const struct vparse_param *param;
const char *label = NULL;
for (param = entry->params; param; param = param->next) {
if (!strcasecmp(param->name, "label")) {
label = param->value;
}
}
json_object_set_new(item, "type", json_string("other"));
if (label) json_object_set_new(item, "label", json_string(label));
json_object_set_new(item, "value", json_string(entry->v.value));
json_array_append_new(online, item);
}
}
json_object_set_new(obj, "online", online);
}
if (_wantprop(crock->props, "nickname")) {
const char *item = vparse_stringval(card, "nickname");
json_object_set_new(obj, "nickname", json_string(item ? item : ""));
}
if (_wantprop(crock->props, "birthday")) {
struct vparse_entry *entry = vparse_get_entry(card, NULL, "bday");
_date_to_jmap(entry, &buf);
json_object_set_new(obj, "birthday", json_string(buf_cstring(&buf)));
}
if (_wantprop(crock->props, "notes")) {
const char *item = vparse_stringval(card, "note");
json_object_set_new(obj, "notes", json_string(item ? item : ""));
}
if (_wantprop(crock->props, "x-hasPhoto")) {
const char *item = vparse_stringval(card, "photo");
json_object_set_new(obj, "x-hasPhoto",
item ? json_true() : json_false());
}
/* XXX - other fields */
json_array_append_new(crock->array, obj);
if (empty) strarray_free(empty);
vparse_free(&vparser);
buf_free(&buf);
return 0;
}
static int getContacts(struct jmap_req *req)
{
return jmap_contacts_get(req, &getcontacts_cb, CARDDAV_KIND_CONTACT, "contacts");
}
static int getContactUpdates(struct jmap_req *req)
{
struct carddav_db *db = carddav_open_userid(req->userid);
if (!db) return -1;
int r = -1;
const char *since = _json_object_get_string(req->args, "sinceState");
if (!since) goto done;
modseq_t oldmodseq = str2uint64(since);
char *mboxname = NULL;
json_t *abookid = json_object_get(req->args, "addressbookId");
if (abookid && json_string_value(abookid)) {
/* XXX - invalid arguments */
const char *addressbookId = json_string_value(abookid);
mboxname = carddav_mboxname(req->userid, addressbookId);
}
struct updates_rock rock;
rock.changed = json_array();
rock.removed = json_array();
r = carddav_get_updates(db, oldmodseq, mboxname, CARDDAV_KIND_CONTACT,
&getcontactupdates_cb, &rock);
if (r) goto done;
strip_spurious_deletes(&rock);
json_t *contactUpdates = json_pack("{}");
json_object_set_new(contactUpdates, "accountId", json_string(req->userid));
json_object_set_new(contactUpdates, "oldState", json_string(since));
json_object_set_new(contactUpdates, "newState", json_string(req->state));
json_object_set(contactUpdates, "changed", rock.changed);
json_object_set(contactUpdates, "removed", rock.removed);
json_t *item = json_pack("[]");
json_array_append_new(item, json_string("contactUpdates"));
json_array_append_new(item, contactUpdates);
json_array_append_new(item, json_string(req->tag));
json_array_append_new(req->response, item);
json_t *dofetch = json_object_get(req->args, "fetchContacts");
json_t *doprops = json_object_get(req->args, "fetchContactProperties");
if (dofetch && json_is_true(dofetch) && json_array_size(rock.changed)) {
struct jmap_req subreq = *req;
subreq.args = json_pack("{}");
json_object_set(subreq.args, "ids", rock.changed);
if (doprops) json_object_set(subreq.args, "properties", doprops);
if (abookid) {
json_object_set(subreq.args, "addressbookId", abookid);
}
r = getContacts(&subreq);
json_decref(subreq.args);
}
json_decref(rock.changed);
json_decref(rock.removed);
done:
carddav_close(db);
return r;
}
static struct vparse_entry *_card_multi(struct vparse_card *card,
const char *name)
{
struct vparse_entry *res = vparse_get_entry(card, NULL, name);
if (!res) {
res = vparse_add_entry(card, NULL, name, NULL);
res->multivalue = 1;
res->v.values = strarray_new();
}
return res;
}
static int _emails_to_card(struct vparse_card *card, json_t *arg)
{
vparse_delete_entries(card, NULL, "email");
int i;
int size = json_array_size(arg);
for (i = 0; i < size; i++) {
json_t *item = json_array_get(arg, i);
const char *type = _json_object_get_string(item, "type");
if (!type) return -1;
/*optional*/
const char *label = _json_object_get_string(item, "label");
const char *value = _json_object_get_string(item, "value");
if (!value) return -1;
json_t *jisDefault = json_object_get(item, "isDefault");
struct vparse_entry *entry =
vparse_add_entry(card, NULL, "email", value);
if (!strcmpsafe(type, "personal"))
type = "home";
if (strcmpsafe(type, "other"))
vparse_add_param(entry, "type", type);
if (label)
vparse_add_param(entry, "label", label);
if (jisDefault && json_is_true(jisDefault))
vparse_add_param(entry, "type", "pref");
}
return 0;
}
static int _phones_to_card(struct vparse_card *card, json_t *arg)
{
vparse_delete_entries(card, NULL, "tel");
int i;
int size = json_array_size(arg);
for (i = 0; i < size; i++) {
json_t *item = json_array_get(arg, i);
const char *type = _json_object_get_string(item, "type");
if (!type) return -1;
/* optional */
const char *label = _json_object_get_string(item, "label");
const char *value = _json_object_get_string(item, "value");
if (!value) return -1;
struct vparse_entry *entry = vparse_add_entry(card, NULL, "tel", value);
if (!strcmp(type, "mobile"))
vparse_add_param(entry, "type", "cell");
else if (strcmp(type, "other"))
vparse_add_param(entry, "type", type);
if (label)
vparse_add_param(entry, "label", label);
}
return 0;
}
static int _is_im(const char *type)
{
/* add new services here */
if (!strcasecmp(type, "aim")) return 1;
if (!strcasecmp(type, "facebook")) return 1;
if (!strcasecmp(type, "gadugadu")) return 1;
if (!strcasecmp(type, "googletalk")) return 1;
if (!strcasecmp(type, "icq")) return 1;
if (!strcasecmp(type, "jabber")) return 1;
if (!strcasecmp(type, "msn")) return 1;
if (!strcasecmp(type, "qq")) return 1;
if (!strcasecmp(type, "skype")) return 1;
if (!strcasecmp(type, "twitter")) return 1;
if (!strcasecmp(type, "yahoo")) return 1;
return 0;
}
static int _online_to_card(struct vparse_card *card, json_t *arg)
{
vparse_delete_entries(card, NULL, "url");
vparse_delete_entries(card, NULL, "impp");
vparse_delete_entries(card, NULL, "x-social-profile");
vparse_delete_entries(card, NULL, "x-fm-online-other");
int i;
int size = json_array_size(arg);
for (i = 0; i < size; i++) {
json_t *item = json_array_get(arg, i);
const char *value = _json_object_get_string(item, "value");
if (!value) return -1;
const char *type = _json_object_get_string(item, "type");
if (!type) return -1;
const char *label = _json_object_get_string(item, "label");
if (!strcmp(type, "uri")) {
struct vparse_entry *entry =
vparse_add_entry(card, NULL, "url", value);
if (label)
vparse_add_param(entry, "label", label);
}
else if (!strcmp(type, "username")) {
if (label && _is_im(label)) {
struct vparse_entry *entry =
vparse_add_entry(card, NULL, "impp", value);
vparse_add_param(entry, "x-service-type", label);
}
else {
struct vparse_entry *entry =
vparse_add_entry(card, NULL, "x-social-profile", ""); // XXX - URL calculated, ick
if (label)
vparse_add_param(entry, "type", label);
vparse_add_param(entry, "x-user", value);
}
}
else if (!strcmp(type, "other")) {
struct vparse_entry *entry = vparse_add_entry(card, NULL, "x-fm-online-other", value);
if (label)
vparse_add_param(entry, "label", label);
}
}
return 0;
}
static int _addresses_to_card(struct vparse_card *card, json_t *arg)
{
vparse_delete_entries(card, NULL, "adr");
int i;
int size = json_array_size(arg);
for (i = 0; i < size; i++) {
json_t *item = json_array_get(arg, i);
const char *type = _json_object_get_string(item, "type");
if (!type) return -1;
/* optional */
const char *label = _json_object_get_string(item, "label");
const char *street = _json_object_get_string(item, "street");
if (!street) return -1;
const char *locality = _json_object_get_string(item, "locality");
if (!locality) return -1;
const char *region = _json_object_get_string(item, "region");
if (!region) return -1;
const char *postcode = _json_object_get_string(item, "postcode");
if (!postcode) return -1;
const char *country = _json_object_get_string(item, "country");
if (!country) return -1;
struct vparse_entry *entry = vparse_add_entry(card, NULL, "adr", NULL);
if (strcmpsafe(type, "other"))
vparse_add_param(entry, "type", type);
if (label)
vparse_add_param(entry, "label", label);
entry->multivalue = 1;
entry->v.values = strarray_new();
strarray_append(entry->v.values, ""); // PO Box
strarray_append(entry->v.values, ""); // Extended Address
strarray_append(entry->v.values, street);
strarray_append(entry->v.values, locality);
strarray_append(entry->v.values, region);
strarray_append(entry->v.values, postcode);
strarray_append(entry->v.values, country);
}
return 0;
}
static int _date_to_card(struct vparse_card *card,
const char *key, json_t *jval)
{
if (!jval)
return -1;
const char *val = json_string_value(jval);
if (!val)
return -1;
/* JMAP dates are always YYYY-MM-DD */
unsigned y, m, d;
if (_parse_date(val, &y, &m, &d))
return -1;
/* range checks. month and day just get basic sanity checks because we're
* not carrying a full calendar implementation here. JMAP says zero is valid
* so we'll allow that and deal with it later on */
if (m > 12 || d > 31)
return -1;
/* all years are valid in JMAP, but ISO8601 only allows Gregorian ie >= 1583.
* moreover, iOS uses 1604 as a magic number for "unknown", so we'll say 1605
* is the minimum */
if (y > 0 && y < 1605)
return -1;
/* everything in range. now comes the fun bit. vCard v3 says BDAY is
* YYYY-MM-DD. It doesn't reference ISO8601 (vCard v4 does) and make no
* provision for "unknown" date components, so there's no way to represent
* JMAP's "unknown" values. Apple worked around this for year by using the
* year 1604 and adding the parameter X-APPLE-OMIT-YEAR=1604 (value
* apparently ignored). We will use a similar hack for month and day so we
* can convert it back into a JMAP date */
int no_year = 0;
if (y == 0) {
no_year = 1;
y = 1604;
}
int no_month = 0;
if (m == 0) {
no_month = 1;
m = 1;
}
int no_day = 0;
if (d == 0) {
no_day = 1;
d = 1;
}
vparse_delete_entries(card, NULL, key);
/* no values, we're done! */
if (no_year && no_month && no_day)
return 0;
/* build the value */
static char buf[11];
snprintf(buf, sizeof(buf), "%04d-%02d-%02d", y, m, d);
struct vparse_entry *entry = vparse_add_entry(card, NULL, key, buf);
/* set all the round-trip flags, sigh */
if (no_year)
vparse_add_param(entry, "x-apple-omit-year", "1604");
if (no_month)
vparse_add_param(entry, "x-fm-no-month", "1");
if (no_day)
vparse_add_param(entry, "x-fm-no-day", "1");
return 0;
}
static int _kv_to_card(struct vparse_card *card, const char *key, json_t *jval)
{
if (!jval)
return -1;
const char *val = json_string_value(jval);
if (!val)
return -1;
vparse_replace_entry(card, NULL, key, val);
return 0;
}
static void _make_fn(struct vparse_card *card)
{
struct vparse_entry *n = vparse_get_entry(card, NULL, "n");
strarray_t *name = strarray_new();
const char *v;
if (n) {
v = strarray_safenth(n->v.values, 3); // prefix
if (*v) strarray_append(name, v);
v = strarray_safenth(n->v.values, 1); // first
if (*v) strarray_append(name, v);
v = strarray_safenth(n->v.values, 2); // middle
if (*v) strarray_append(name, v);
v = strarray_safenth(n->v.values, 0); // last
if (*v) strarray_append(name, v);
v = strarray_safenth(n->v.values, 4); // suffix
if (*v) strarray_append(name, v);
}
if (!strarray_size(name)) {
v = vparse_stringval(card, "nickname");
if (v && v[0]) strarray_append(name, v);
}
char *fn = NULL;
if (strarray_size(name))
fn = strarray_join(name, " ");
else
fn = xstrdup(" ");
strarray_free(name);
vparse_replace_entry(card, NULL, "fn", fn);
free(fn);
}
static int _json_to_card(struct vparse_card *card,
json_t *arg, strarray_t *flags,
struct entryattlist **annotsp)
{
const char *key;
json_t *jval;
struct vparse_entry *fn = vparse_get_entry(card, NULL, "fn");
int name_is_dirty = 0;
int record_is_dirty = 0;
/* we'll be updating you later anyway... create early so that it's
* at the top of the card */
if (!fn) {
fn = vparse_add_entry(card, NULL, "fn", "No Name");
name_is_dirty = 1;
}
json_object_foreach(arg, key, jval) {
if (!strcmp(key, "isFlagged")) {
if (json_is_true(jval)) {
strarray_add_case(flags, "\\Flagged");
}
else {
strarray_remove_all_case(flags, "\\Flagged");
}
}
else if (!strcmp(key, "x-importance")) {
double dval = json_number_value(jval);
const char *ns = DAV_ANNOT_NS "<" XML_NS_CYRUS ">importance";
const char *attrib = "value.shared";
struct buf buf = BUF_INITIALIZER;
if (dval) {
buf_printf(&buf, "%e", dval);
}
setentryatt(annotsp, ns, attrib, &buf);
buf_free(&buf);
}
else if (!strcmp(key, "avatar")) {
/* XXX - file handling */
}
else if (!strcmp(key, "prefix")) {
const char *val = json_string_value(jval);
if (!val)
return -1;
name_is_dirty = 1;
struct vparse_entry *n = _card_multi(card, "n");
strarray_set(n->v.values, 3, val);
}
else if (!strcmp(key, "firstName")) {
const char *val = json_string_value(jval);
if (!val)
return -1;
name_is_dirty = 1;
struct vparse_entry *n = _card_multi(card, "n");
strarray_set(n->v.values, 1, val);
}
else if (!strcmp(key, "lastName")) {
const char *val = json_string_value(jval);
if (!val)
return -1;
name_is_dirty = 1;
struct vparse_entry *n = _card_multi(card, "n");
strarray_set(n->v.values, 0, val);
}
else if (!strcmp(key, "suffix")) {
const char *val = json_string_value(jval);
if (!val)
return -1;
name_is_dirty = 1;
struct vparse_entry *n = _card_multi(card, "n");
strarray_set(n->v.values, 4, val);
}
else if (!strcmp(key, "nickname")) {
int r = _kv_to_card(card, "nickname", jval);
if (r) return r;
record_is_dirty = 1;
}
else if (!strcmp(key, "birthday")) {
int r = _date_to_card(card, "bday", jval);
if (r) return r;
record_is_dirty = 1;
}
else if (!strcmp(key, "anniversary")) {
int r = _kv_to_card(card, "anniversary", jval);
if (r) return r;
record_is_dirty = 1;
}
else if (!strcmp(key, "company")) {
const char *val = json_string_value(jval);
if (!val)
return -1;
struct vparse_entry *org = _card_multi(card, "org");
strarray_set(org->v.values, 0, val);
record_is_dirty = 1;
}
else if (!strcmp(key, "department")) {
const char *val = json_string_value(jval);
if (!val)
return -1;
struct vparse_entry *org = _card_multi(card, "org");
strarray_set(org->v.values, 1, val);
record_is_dirty = 1;
}
else if (!strcmp(key, "jobTitle")) {
const char *val = json_string_value(jval);
if (!val)
return -1;
struct vparse_entry *org = _card_multi(card, "org");
strarray_set(org->v.values, 2, val);
record_is_dirty = 1;
}
else if (!strcmp(key, "emails")) {
int r = _emails_to_card(card, jval);
if (r) return r;
record_is_dirty = 1;
}
else if (!strcmp(key, "phones")) {
int r = _phones_to_card(card, jval);
if (r) return r;
record_is_dirty = 1;
}
else if (!strcmp(key, "online")) {
int r = _online_to_card(card, jval);
if (r) return r;
record_is_dirty = 1;
}
else if (!strcmp(key, "addresses")) {
int r = _addresses_to_card(card, jval);
if (r) return r;
record_is_dirty = 1;
}
else if (!strcmp(key, "notes")) {
int r = _kv_to_card(card, "note", jval);
if (r) return r;
record_is_dirty = 1;
}
else {
/* INVALID PARAM */
return -1; /* XXX - need codes */
}
}
if (name_is_dirty) {
_make_fn(card);
record_is_dirty = 1;
}
if (!record_is_dirty)
return 204; /* no content */
return 0;
}
static int setContacts(struct jmap_req *req)
{
struct carddav_db *db = carddav_open_userid(req->userid);
if (!db) return -1;
struct mailbox *mailbox = NULL;
struct mailbox *newmailbox = NULL;
int r = 0;
json_t *jcheckState = json_object_get(req->args, "ifInState");
if (jcheckState) {
const char *checkState = json_string_value(jcheckState);
if (!checkState ||strcmp(req->state, checkState)) {
json_t *item = json_pack("[s, {s:s}, s]",
"error", "type", "stateMismatch",
req->tag);
json_array_append_new(req->response, item);
goto done;
}
}
json_t *set = json_pack("{s:s,s:s}",
"oldState", req->state,
"accountId", req->userid);
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) {
const char *uid = makeuuid();
strarray_t *flags = strarray_new();
struct entryattlist *annots = NULL;
const char *addressbookId = "Default";
json_t *abookid = json_object_get(arg, "addressbookId");
if (abookid && json_string_value(abookid)) {
/* XXX - invalid arguments */
addressbookId = json_string_value(abookid);
}
char *mboxname = mboxname_abook(req->userid, addressbookId);
json_object_del(arg, "addressbookId");
addressbookId = NULL;
struct vparse_card *card = vparse_new_card("VCARD");
vparse_add_entry(card, NULL, "VERSION", "3.0");
vparse_add_entry(card, NULL, "UID", uid);
/* we need to create and append a record */
if (!mailbox || strcmp(mailbox->name, mboxname)) {
mailbox_close(&mailbox);
r = mailbox_open_iwl(mboxname, &mailbox);
if (r) {
free(mboxname);
vparse_free_card(card);
goto done;
}
}
r = _json_to_card(card, arg, flags, &annots);
if (r) {
/* this is just a failure */
r = 0;
json_t *err = json_pack("{s:s}", "type", "invalidParameters");
json_object_set_new(notCreated, key, err);
strarray_free(flags);
freeentryatts(annots);
vparse_free_card(card);
continue;
}
syslog(LOG_NOTICE, "jmap: create contact %s/%s (%s)",
req->userid, mboxname, uid);
r = carddav_store(mailbox, card, NULL,
flags, annots, req->userid, req->authstate, ignorequota);
vparse_free_card(card);
free(mboxname);
strarray_free(flags);
freeentryatts(annots);
if (r) {
goto done;
}
record = json_pack("{s:s}", "id", uid);
json_object_set_new(created, key, record);
/* hash_insert takes ownership of uid here, skanky I know */
hash_insert(key, xstrdup(uid), req->idmap);
}
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) {
struct carddav_data *cdata = NULL;
r = carddav_lookup_uid(db, uid, &cdata);
uint32_t olduid;
char *resource = NULL;
if (r || !cdata || !cdata->dav.imap_uid
|| cdata->kind != CARDDAV_KIND_CONTACT) {
r = 0;
json_t *err = json_pack("{s:s}", "type", "notFound");
json_object_set_new(notUpdated, uid, err);
continue;
}
olduid = cdata->dav.imap_uid;
resource = xstrdup(cdata->dav.resource);
if (!mailbox || strcmp(mailbox->name, cdata->dav.mailbox)) {
mailbox_close(&mailbox);
r = mailbox_open_iwl(cdata->dav.mailbox, &mailbox);
if (r) {
syslog(LOG_ERR, "IOERROR: failed to open %s",
cdata->dav.mailbox);
goto done;
}
}
json_t *abookid = json_object_get(arg, "addressbookId");
if (abookid && json_string_value(abookid)) {
const char *mboxname =
mboxname_abook(req->userid, json_string_value(abookid));
if (strcmp(mboxname, cdata->dav.mailbox)) {
/* move */
r = mailbox_open_iwl(mboxname, &newmailbox);
if (r) {
syslog(LOG_ERR, "IOERROR: failed to open %s", mboxname);
goto done;
}
}
json_object_del(arg, "addressbookId");
}
/* XXX - this could definitely be refactored from here and mailbox.c */
struct buf msg_buf = BUF_INITIALIZER;
struct vparse_state vparser;
struct index_record record;
r = mailbox_find_index_record(mailbox, cdata->dav.imap_uid, &record);
if (r) goto done;
/* Load message containing the resource and parse vcard data */
r = mailbox_map_record(mailbox, &record, &msg_buf);
if (r) goto done;
strarray_t *flags =
mailbox_extract_flags(mailbox, &record, req->userid);
struct entryattlist *annots =
mailbox_extract_annots(mailbox, &record);
memset(&vparser, 0, sizeof(struct vparse_state));
vparser.base = buf_cstring(&msg_buf) + record.header_size;
vparse_set_multival(&vparser, "adr");
vparse_set_multival(&vparser, "org");
vparse_set_multival(&vparser, "n");
r = vparse_parse(&vparser, 0);
buf_free(&msg_buf);
if (r || !vparser.card || !vparser.card->objects) {
r = 0;
json_t *err = json_pack("{s:s}", "type", "parseError");
json_object_set_new(notUpdated, uid, err);
vparse_free(&vparser);
strarray_free(flags);
freeentryatts(annots);
mailbox_close(&newmailbox);
free(resource);
continue;
}
struct vparse_card *card = vparser.card->objects;
r = _json_to_card(card, arg, flags, &annots);
if (r == 204) {
r = 0;
if (!newmailbox) {
/* just bump the modseq
if in the same mailbox and no data change */
syslog(LOG_NOTICE, "jmap: touch contact %s/%s",
req->userid, resource);
if (strarray_find_case(flags, "\\Flagged", 0) >= 0)
record.system_flags |= FLAG_FLAGGED;
else
record.system_flags &= ~FLAG_FLAGGED;
annotate_state_t *state = NULL;
r = mailbox_get_annotate_state(mailbox, record.uid, &state);
annotate_state_set_auth(state, 0,
req->userid, req->authstate);
if (!r) r = annotate_state_store(state, annots);
if (!r) r = mailbox_rewrite_index_record(mailbox, &record);
goto finish;
}
}
if (r) {
/* this is just a failure to create the JSON, not an error */
r = 0;
json_t *err = json_pack("{s:s}", "type", "invalidParameters");
json_object_set_new(notUpdated, uid, err);
vparse_free(&vparser);
strarray_free(flags);
freeentryatts(annots);
mailbox_close(&newmailbox);
free(resource);
continue;
}
syslog(LOG_NOTICE, "jmap: update contact %s/%s",
req->userid, resource);
r = carddav_store(newmailbox ? newmailbox : mailbox, card, resource,
flags, annots, req->userid, req->authstate, ignorequota);
if (!r)
r = carddav_remove(mailbox, olduid, /*isreplace*/!newmailbox);
finish:
mailbox_close(&newmailbox);
strarray_free(flags);
freeentryatts(annots);
vparse_free(&vparser);
free(resource);
if (r) goto done;
json_array_append_new(updated, json_string(uid));
}
if (json_array_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;
for (index = 0; index < json_array_size(destroy); index++) {
const char *uid = _json_array_get_string(destroy, index);
if (!uid) {
json_t *err = json_pack("{s:s}", "type", "invalidArguments");
json_object_set_new(notDestroyed, uid, err);
continue;
}
struct carddav_data *cdata = NULL;
uint32_t olduid;
r = carddav_lookup_uid(db, uid, &cdata);
if (r || !cdata || !cdata->dav.imap_uid
|| cdata->kind != CARDDAV_KIND_CONTACT) {
r = 0;
json_t *err = json_pack("{s:s}", "type", "notFound");
json_object_set_new(notDestroyed, uid, err);
continue;
}
olduid = cdata->dav.imap_uid;
if (!mailbox || strcmp(mailbox->name, cdata->dav.mailbox)) {
mailbox_close(&mailbox);
r = mailbox_open_iwl(cdata->dav.mailbox, &mailbox);
if (r) goto done;
}
/* XXX - fricking mboxevent */
syslog(LOG_NOTICE, "jmap: remove contact %s/%s", req->userid, uid);
r = carddav_remove(mailbox, olduid, /*isreplace*/0);
if (r) {
syslog(LOG_ERR, "IOERROR: setContacts remove failed for %s %u",
mailbox->name, olduid);
goto done;
}
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);
}
/* force modseq to stable */
if (mailbox) mailbox_unlock_index(mailbox, NULL);
/* read the modseq again every time, just in case something changed it
* in our actions */
struct buf buf = BUF_INITIALIZER;
char *inboxname = mboxname_user_mbox(req->userid, NULL);
modseq_t modseq = mboxname_readmodseq(inboxname);
free(inboxname);
buf_printf(&buf, "%llu", modseq);
json_object_set_new(set, "newState", json_string(buf_cstring(&buf)));
buf_free(&buf);
json_t *item = json_pack("[]");
json_array_append_new(item, json_string("contactsSet"));
json_array_append_new(item, set);
json_array_append_new(item, json_string(req->tag));
json_array_append_new(req->response, item);
done:
mailbox_close(&newmailbox);
mailbox_close(&mailbox);
carddav_close(db);
return r;
}
/*********************** CALENDARS **********************/
/* 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. */
#define JMAP_EXC (1<<8) /* Current component is a VEVENT exception .*/
typedef struct calevent_rock {
struct jmap_req *req; /* The current JMAP request. */
int flags; /* Flags indicating the request context. */
const char *uid; /* The iCalendar UID of this event. */
const char *recurid; /* The LocalDate recurrence id of this event. */
int isAllDay; /* This event is a whole-day event. */
json_t *invalid; /* A JSON array of any invalid properties. */
icaltimezone *tzstart_old; /* The former startTimeZone. */
icaltimezone *tzstart; /* The current startTimeZone. */
icaltimezone *tzend_old; /* The former endTimeZone. */
icaltimezone *tzend; /* The current endTimeZone. */
icaltimezone **tzs; /* Timezones required as VTIMEZONEs. */
size_t n_tzs; /* The count of timezones. */
size_t s_tzs; /* The size of the timezone array. */
} calevent_rock;
/* Update the VEVENT comp with the properties of the JMAP calendar event.
* The VEVENT must have a VCALENDAR as parent and its timezones might get
* rewritten. If uid is non-zero, set the VEVENT uid and any recurrence
* exceptions to this UID. */
static void jmap_calendarevent_to_ical(icalcomponent *comp,
json_t *event,
calevent_rock *rock);
/* 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_ishidden(const char *uid) {
/* XXX - brong wrote to "check the specialuse magic on these instead" */
if (!strcmp(uid, "#calendars")) return 1;
/* XXX - could also check the schedule-inbox and outbox annotations,
* instead. But as long as these names are hardcoded in http_dav... */
/* SCHED_INBOX and SCHED_OUTBOX end in "/", so trim them */
if (!strncmp(uid, SCHED_INBOX, strlen(SCHED_INBOX)-1)) return 1;
if (!strncmp(uid, SCHED_OUTBOX, strlen(SCHED_OUTBOX)-1)) return 1;
if (!strncmp(uid, MANAGED_ATTACH, strlen(MANAGED_ATTACH)-1)) return 1;
return 0;
}
struct calendars_rock {
struct jmap_req *req;
json_t *array;
struct hash_table *props;
struct mailbox *mailbox;
int rows;
};
/* Set is_cal, if the userid's calendar mailbox named mboxname is able to store
* VEVENTs. Return non-zero on error. */
/* XXX Also make sure to check for this in getCalendarUpdates. Might want to
* check this also in the other calendar and calendar event methods. */
static int jmap_mboxname_is_calendar(const char *mboxname, const char *userid, int *is_cal)
{
struct buf attrib = BUF_INITIALIZER;
static const char *calcompset_annot =
DAV_ANNOT_NS "<" XML_NS_CALDAV ">supported-calendar-component-set";
unsigned long types = -1; /* ALL component types by default. */
int r = annotatemore_lookupmask(mboxname, calcompset_annot, userid, &attrib);
if (r) goto done;
if (attrib.len) {
types = strtoul(buf_cstring(&attrib), NULL, 10);
}
*is_cal = types & CAL_COMP_VEVENT;
done:
buf_free(&attrib);
return r;
}
static int getcalendars_cb(const mbentry_t *mbentry, void *rock)
{
struct calendars_rock *crock = (struct calendars_rock *)rock;
struct buf attrib = BUF_INITIALIZER;
int r;
/* Only calendars... */
if (!(mbentry->mbtype & MBTYPE_CALENDAR)) return 0;
/* ...which are at least readable or visible... */
int rights = httpd_myrights(crock->req->authstate, mbentry->acl);
/* XXX - What if just READFB is set? */
if (!(rights & (DACL_READ|DACL_READFB))) {
return 0;
}
/* ...and contain VEVENTs. */
int is_cal = 0;
r = jmap_mboxname_is_calendar(mbentry->name, httpd_userid, &is_cal);
if (r || !is_cal) {
goto done;
}
/* OK, we want this one */
const char *collection = strrchr(mbentry->name, '.') + 1;
/* unless it's one of the special names... XXX - check
* the specialuse magic on these instead */
if (jmap_calendar_ishidden(collection)) return 0;
if (!strcmp(collection, "#calendars")) return 0;
if (!strcmp(collection, "Inbox")) return 0;
if (!strcmp(collection, "Outbox")) return 0;
crock->rows++;
json_t *obj = json_pack("{}");
json_object_set_new(obj, "id", json_string(collection));
if (_wantprop(crock->props, "x-href")) {
_add_xhref(obj, mbentry->name, NULL);
}
if (_wantprop(crock->props, "name")) {
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, collection);
json_object_set_new(obj, "name", json_string(buf_cstring(&attrib)));
buf_reset(&attrib);
}
if (_wantprop(crock->props, "color")) {
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_reset(&attrib);
}
if (_wantprop(crock->props, "sortOrder")) {
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_reset(&attrib);
}
if (_wantprop(crock->props, "isVisible")) {
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_reset(&attrib);
}
if (_wantprop(crock->props, "mayReadFreeBusy")) {
int bool = rights & DACL_READFB;
json_object_set_new(obj, "mayReadFreeBusy", bool ? json_true() : json_false());
}
if (_wantprop(crock->props, "mayReadItems")) {
int bool = rights & DACL_READ;
json_object_set_new(obj, "mayReadItems", bool ? json_true() : json_false());
}
if (_wantprop(crock->props, "mayAddItems")) {
int bool = rights & DACL_WRITECONT;
json_object_set_new(obj, "mayAddItems", bool ? json_true() : json_false());
}
if (_wantprop(crock->props, "mayModifyItems")) {
int bool = rights & DACL_WRITECONT;
json_object_set_new(obj, "mayModifyItems", bool ? json_true() : json_false());
}
if (_wantprop(crock->props, "mayRemoveItems")) {
int bool = rights & DACL_RMRSRC;
json_object_set_new(obj, "mayRemoveItems", bool ? json_true() : json_false());
}
if (_wantprop(crock->props, "mayRename")) {
int bool = rights & DACL_RMCOL;
json_object_set_new(obj, "mayRename", bool ? json_true() : json_false());
}
if (_wantprop(crock->props, "mayDelete")) {
int bool = rights & DACL_RMCOL;
json_object_set_new(obj, "mayDelete", bool ? json_true() : json_false());
}
json_array_append_new(crock->array, obj);
done:
buf_free(&attrib);
return r;
}
/* jmap calendar APIs */
/* Check if ifInState matches the current mailbox state for mailbox type
* mbtype, if so return zero. Otherwise, append a stateMismatch error to the
* JMAP response. */
static int jmap_checkstate(struct jmap_req *req, int mbtype) {
json_t *jIfInState = json_object_get(req->args, "ifInState");
if (jIfInState) {
const char *ifInState = json_string_value(jIfInState);
if (!ifInState) {
return -1;
}
modseq_t clientState = str2uint64(ifInState);
if (mbtype == MBTYPE_CALENDAR && clientState == req->counters.caldavmodseq) {
return 0;
} else if (mbtype == MBTYPE_ADDRESSBOOK && clientState == req->counters.carddavmodseq) {
return 0;
} else if (clientState == req->counters.mailmodseq) {
/* XXX - What about notesmodseq? */
return 0;
} else {
json_t *item = json_pack("[s, {s:s}, s]",
"error", "type", "stateMismatch",
req->tag);
json_array_append_new(req->response, item);
return -3;
}
}
return 0;
}
/* Set the state token named name for the JMAP type mbtype in response res.
* If refresh is true, refresh the current mailbox counters in req
* If bump is true, update the state of this JMAP type before setting name. */
static int jmap_setstate(struct jmap_req *req,
json_t *res,
const char *name,
int mbtype,
int refresh,
int bump) {
struct buf buf = BUF_INITIALIZER;
char *mboxname;
int r = 0;
modseq_t modseq;
mboxname = mboxname_user_mbox(req->userid, NULL);
/* Read counters. */
if (refresh) {
r = mboxname_read_counters(mboxname, &req->counters);
if (r) goto done;
}
/* Determine current counter by mailbox type. */
switch (mbtype) {
case MBTYPE_CALENDAR:
modseq = req->counters.caldavmodseq;
break;
case MBTYPE_ADDRESSBOOK:
modseq = req->counters.carddavmodseq;
break;
default:
/* XXX - What about notesmodseq? */
modseq = req->counters.highestmodseq;
}
/* Bump current counter. */
if (bump) {
modseq = mboxname_nextmodseq(mboxname, modseq, mbtype);
}
/* Set newState field. */
buf_printf(&buf, "%llu", modseq);
json_object_set_new(res, name, json_string(buf_cstring(&buf)));
buf_free(&buf);
done:
free(mboxname);
return r;
}
/* Read the property named name into dst, formatted according to the json
* unpack format fmt. If unpacking failed, or name is mandatory and not found
* in root, append name (prefixed by any non-NULL prefix) to invalid.
*
* Return a negative value for a missing or invalid property.
* Return a positive value if a property was read, zero otherwise. */
static int jmap_readprop_full(json_t *root,
const char *prefix,
const char *name,
int mandatory,
json_t *invalid,
const char *fmt,
void *dst)
{
json_t *jval = json_object_get(root, name);
if (!jval && mandatory) {
json_array_append_new(invalid, json_string(name));
return -1;
}
if (jval) {
json_error_t err;
if (json_unpack_ex(jval, &err, 0, fmt, dst)) {
if (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 {
json_array_append_new(invalid, json_string(name));
}
return -2;
}
return 1;
}
return 0;
}
static int jmap_readprop(json_t *root, const char *name, int mandatory,
json_t *invalid, const char *fmt, void *dst) {
return jmap_readprop_full(root, NULL, name, mandatory, invalid, fmt, dst);
}
/* Update the calendar properties in the calendar mailbox named mboxname.
* NULL values and negative integers are ignored. Return 0 on success. */
static int jmap_update_calendar(const char *mboxname,
const struct jmap_req *req,
const char *name,
const char *color,
int sortOrder,
int isVisible)
{
struct mailbox *mbox = NULL;
int rights;
annotate_state_t *astate = NULL;
struct buf val = BUF_INITIALIZER;
int r;
r = mailbox_open_iwl(mboxname, &mbox);
if (r) {
syslog(LOG_ERR, "mailbox_open_iwl(%s) failed: %s",
mboxname, error_message(r));
return r;
}
rights = mbox->acl ? cyrus_acl_myrights(req->authstate, mbox->acl) : 0;
if (!(rights & DACL_READ)) {
r = IMAP_MAILBOX_NONEXISTENT;
} else if (!(rights & DACL_WRITE)) {
r = IMAP_PERMISSION_DENIED;
}
if (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_printf(&val, "%s", 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_printf(&val, "%s", 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_printf(&val, "%s", 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 jmap_delete_calendar(const char *mboxname, const struct jmap_req *req) {
struct mailbox *mbox = NULL;
int r;
r = mailbox_open_irl(mboxname, &mbox);
if (r) {
syslog(LOG_ERR, "mailbox_open_irl(%s) failed: %s",
mboxname, error_message(r));
return r;
}
int rights = mbox->acl ? cyrus_acl_myrights(req->authstate, mbox->acl) : 0;
mailbox_close(&mbox);
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;
}
r = caldav_delmbox(db, mboxname);
if (r) {
syslog(LOG_ERR, "failed to delete mailbox from caldav_db: %s",
error_message(r));
return r;
}
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 */);
}
int rr = caldav_close(db);
if (!r) r = rr;
return r;
}
static int getCalendars(struct jmap_req *req)
{
struct calendars_rock rock;
int r = 0;
r = caldav_create_defaultcalendars(req->userid);
if (r) return r;
rock.array = json_pack("[]");
rock.req = req;
rock.props = NULL;
rock.rows = 0;
json_t *properties = json_object_get(req->args, "properties");
if (properties) {
rock.props = xzmalloc(sizeof(struct hash_table));
construct_hash_table(rock.props, 1024, 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) {
int i;
int size = json_array_size(want);
for (i = 0; i < size; i++) {
const char *id = json_string_value(json_array_get(want, i));
rock.rows = 0;
char *mboxname = caldav_mboxname(req->userid, id);
r = mboxlist_mboxtree(mboxname, &getcalendars_cb, &rock, MBOXTREE_SKIP_CHILDREN);
free(mboxname);
if (r) goto done;
if (!rock.rows) {
json_array_append_new(notfound, json_string(id));
}
}
}
else {
r = mboxlist_usermboxtree(req->userid, &getcalendars_cb, &rock, /*flags*/0);
if (r) goto done;
}
json_t *calendars = json_pack("{}");
r = jmap_setstate(req, calendars, "state", MBTYPE_CALENDAR, 1 /*refresh*/, 0 /*bump*/);
if (r) goto done;
json_incref(rock.array);
json_object_set_new(calendars, "accountId", json_string(req->userid));
json_object_set_new(calendars, "list", rock.array);
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("calendars"));
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.array);
return r;
}
static int getCalendarUpdates(struct jmap_req *req)
{
int r, pe;
json_t *invalid;
struct caldav_db *db;
const char *since = NULL;
int dofetch = 0;
struct buf buf = BUF_INITIALIZER;
modseq_t oldmodseq;
r = caldav_create_defaultcalendars(req->userid);
if (r) goto done;
db = caldav_open_userid(req->userid);
if (!db) {
syslog(LOG_ERR, "caldav_open_mailbox failed for user %s", req->userid);
r = IMAP_INTERNAL;
goto done;
}
/* Parse and validate arguments. */
invalid = json_pack("[]");
pe = jmap_readprop(req->args, "sinceState", 1 /*mandatory*/, invalid, "s", &since);
if (pe > 0) {
oldmodseq = str2uint64(since);
if (!oldmodseq) {
json_array_append_new(invalid, json_string("sinceState"));
}
}
jmap_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);
if (oldmodseq != req->counters.caldavmodseq) {
/* XXX - This is where we would report calendar changes. But since it's
* not clear yet, how to report purged mailboxes as removed, let's force
* the client to flush its cache. */
json_t *err = json_pack("{s:s}", "type", "cannotCalculateChanges");
json_array_append_new(req->response, json_pack("[s,o,s]", "error", err, req->tag));
} else {
/* Create response without any updates. */
json_t *eventUpdates = json_pack("{}");
json_object_set_new(eventUpdates, "accountId", json_string(req->userid));
json_object_set_new(eventUpdates, "oldState", json_string(since));
json_object_set_new(eventUpdates, "newState", json_string(since));
json_object_set_new(eventUpdates, "hasMoreUpdates", json_false());
json_object_set_new(eventUpdates, "changed", json_pack("[]"));
json_object_set_new(eventUpdates, "removed", json_pack("[]"));
json_t *item = json_pack("[]");
json_array_append_new(item, json_string("calendarUpdates"));
json_array_append_new(item, eventUpdates);
json_array_append_new(item, json_string(req->tag));
json_array_append_new(req->response, item);
}
done:
buf_free(&buf);
if (db) caldav_close(db);
return r;
}
static int setCalendars(struct jmap_req *req)
{
int r = jmap_checkstate(req, MBTYPE_CALENDAR);
if (r) return 0;
json_t *set = json_pack("{s:s}", "accountId", req->userid);
r = jmap_setstate(req, set, "oldState", MBTYPE_CALENDAR, 0 /*refresh*/, 0 /*bump*/);
if (r) goto done;
r = caldav_create_defaultcalendars(req->userid);
if (r) goto done;
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 = jmap_readprop(arg, "name", 1, invalid, "s", &name);
if (pe > 0 && strnlen(name, 256) == 256) {
json_array_append_new(invalid, json_string("name"));
}
/* XXX - wait for CalConnect/Neil feedback on how to validate */
jmap_readprop(arg, "color", 1, invalid, "s", &color);
pe = jmap_readprop(arg, "sortOrder", 1, invalid, "i", &sortOrder);
if (pe > 0 && sortOrder < 0) {
json_array_append_new(invalid, json_string("sortOrder"));
}
pe = jmap_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; jmap_readprop(arg, "mayReadFreeBusy", 0, invalid, "b", &flag);
if (!flag) {
json_array_append_new(invalid, json_string("mayReadFreeBusy"));
}
flag = 1; jmap_readprop(arg, "mayReadItems", 0, invalid, "b", &flag);
if (!flag) {
json_array_append_new(invalid, json_string("mayReadItems"));
}
flag = 1; jmap_readprop(arg, "mayAddItems", 0, invalid, "b", &flag);
if (!flag) {
json_array_append_new(invalid, json_string("mayAddItems"));
}
flag = 1; jmap_readprop(arg, "mayModifyItems", 0, invalid, "b", &flag);
if (!flag) {
json_array_append_new(invalid, json_string("mayModifyItems"));
}
flag = 1; jmap_readprop(arg, "mayRemoveItems", 0, invalid, "b", &flag);
if (!flag) {
json_array_append_new(invalid, json_string("mayRemoveItems"));
}
flag = 1; jmap_readprop(arg, "mayRename", 0, invalid, "b", &flag);
if (!flag) {
json_array_append_new(invalid, json_string("mayRename"));
}
flag = 1; jmap_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);
/* Create a calendar named uid. */
char *uid = xstrdup(makeuuid());
char *mboxname = caldav_mboxname(req->userid, uid);
char rights[100];
struct buf acl = BUF_INITIALIZER;
buf_reset(&acl);
cyrus_acl_masktostr(ACL_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);
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 = jmap_update_calendar(mboxname, req, name, color, sortOrder, isVisible);
if (r) {
free(uid);
int rr = mboxlist_delete(mboxname, 1 /* force */);
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);
}
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 (!strlen(uid)) {
json_t *err= json_pack("{s:s}", "type", "invalidArguments");
json_object_set_new(notUpdated, uid, err);
continue;
}
if (*uid == '#') {
const char *t = hash_lookup(uid, req->idmap);
if (!t) {
json_t *err = json_pack("{s:s}", "type", "invalidArguments");
json_object_set_new(notUpdated, uid, err);
continue;
}
uid = t;
}
if (jmap_calendar_ishidden(uid)) {
json_t *err = json_pack("{s:s}", "type", "notFound");
json_object_set_new(notUpdated, uid, 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 = -1;
int flag;
int pe = 0; /* parse error */
pe = jmap_readprop(arg, "name", 0, invalid, "s", &name);
if (pe > 0 && strnlen(name, 256) == 256) {
json_array_append_new(invalid, json_string("name"));
}
pe = jmap_readprop(arg, "color", 0, invalid, "s", &color);
if (pe > 0) {
/* XXX - wait for CalConnect/Neil feedback on how to validate */
}
pe = jmap_readprop(arg, "sortOrder", 0, invalid, "i", &sortOrder);
if (pe > 0 && sortOrder < 0) {
json_array_append_new(invalid, json_string("sortOrder"));
}
jmap_readprop(arg, "isVisible", 0, invalid, "b", &isVisible);
/* The mayFoo properties are immutable and MUST NOT set. */
pe = jmap_readprop(arg, "mayReadFreeBusy", 0, invalid, "b", &flag);
if (pe > 0) {
json_array_append_new(invalid, json_string("mayReadFreeBusy"));
}
pe = jmap_readprop(arg, "mayReadItems", 0, invalid, "b", &flag);
if (pe > 0) {
json_array_append_new(invalid, json_string("mayReadItems"));
}
pe = jmap_readprop(arg, "mayAddItems", 0, invalid, "b", &flag);
if (pe > 0) {
json_array_append_new(invalid, json_string("mayAddItems"));
}
pe = jmap_readprop(arg, "mayModifyItems", 0, invalid, "b", &flag);
if (pe > 0) {
json_array_append_new(invalid, json_string("mayModifyItems"));
}
pe = jmap_readprop(arg, "mayRemoveItems", 0, invalid, "b", &flag);
if (pe > 0) {
json_array_append_new(invalid, json_string("mayRemoveItems"));
}
pe = jmap_readprop(arg, "mayRename", 0, invalid, "b", &flag);
if (pe > 0) {
json_array_append_new(invalid, json_string("mayRename"));
}
pe = jmap_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);
/* Update the calendar named uid. */
char *mboxname = caldav_mboxname(req->userid, uid);
r = jmap_update_calendar(mboxname, req, 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);
continue;
}
else if (r == IMAP_PERMISSION_DENIED) {
json_t *err = json_pack("{s:s}", "type", "accountReadOnly");
json_object_set_new(notUpdated, uid, err);
continue;
}
/* Report calendar as updated. */
json_array_append_new(updated, json_string(uid));
}
if (json_array_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. JMAP destroy does not allow reference uids. */
const char *uid = json_string_value(juid);
if (!strlen(uid) || *uid == '#' || jmap_calendar_ishidden(uid)) {
json_t *err = json_pack("{s:s}", "type", "notFound");
json_object_set_new(notDestroyed, uid, err);
continue;
}
/* Do not allow to remove the default calendar. */
char *mboxname = caldav_mboxname(req->userid, 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, httpd_userid, &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);
/* Destroy calendar. */
mboxname = caldav_mboxname(req->userid, uid);
r = jmap_delete_calendar(mboxname, req);
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);
continue;
} else if (r == IMAP_PERMISSION_DENIED) {
json_t *err = json_pack("{s:s}", "type", "accountReadOnly");
json_object_set_new(notDestroyed, uid, err);
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. */
r = jmap_setstate(req, set, "newState", MBTYPE_CALENDAR,
1 /*refresh*/,
json_object_get(set, "created") ||
json_object_get(set, "updated") ||
json_object_get(set, "destroyed"));
if (r) goto done;
json_incref(set);
json_t *item = json_pack("[]");
json_array_append_new(item, json_string("calendarsSet"));
json_array_append_new(item, set);
json_array_append_new(item, json_string(req->tag));
json_array_append_new(req->response, item);
done:
json_decref(set);
return r;
}
/* Convert time to a RFC3339 formatted localdate string. Return the number
* of bytes written to buf sized size, excluding the terminating null byte. */
static int jmap_timet_to_localdate(time_t t, char* buf, size_t size) {
int n = time_to_rfc3339(t, buf, size);
if (n && buf[n-1] == 'Z') {
buf[n-1] = '\0';
n--;
}
return n;
}
/* Convert the JMAP datetime in buf to tm time. Return 0 on success. */
static int jmap_date_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 UTC date. */
const char *p = strptime(buf, "%Y-%m-%dT%H:%M:%SZ", tm);
if (!p || *p) {
return -1;
}
return 0;
}
/* Convert the JMAP datetime formatted buf into ical datetime dt.
* Return 0 on success. */
static int jmap_date_to_icaltime(const char *buf,
icaltimetype *dt,
int isAllDay) {
struct tm tm;
int r;
icaltimetype tmp;
r = jmap_date_to_tm(buf, &tm);
if (r) return r;
if (isAllDay && (tm.tm_sec || tm.tm_min || tm.tm_hour)) {
return 1;
}
tmp = icaltime_from_timet_with_zone(mktime(&tm), 0, icaltimezone_get_utc_timezone());
tmp.is_date = isAllDay;
*dt = tmp;
return 0;
}
/* Convert the JMAP local datetime in buf to tm time. Return 0 on success. */
static int jmap_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 -1;
}
return 0;
}
/* Convert the JMAP local datetime formatted buf into ical datetime dt
* using timezone tz. Return 0 on success. */
static int jmap_localdate_to_icaltime(const char *buf,
icaltimetype *dt,
icaltimezone *tz,
int isAllDay) {
struct tm tm;
int r;
char *s = NULL;
icaltimetype tmp;
r = jmap_localdate_to_tm(buf, &tm);
if (r) return r;
if (isAllDay && (tm.tm_sec || tm.tm_min || tm.tm_hour)) {
return 1;
}
/* 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 = xzmalloc(16);
strftime(s, 16, "%Y%m%dT%H%M%S", &tm);
tmp = icaltime_from_string(s);
free(s);
if (icaltime_is_null_time(tmp)) {
return -1;
}
tmp.zone = tz;
tmp.is_date = isAllDay;
*dt = tmp;
return 0;
}
/* Convert icaltime to a RFC3339 formatted localdate string. The returned
* string is owned by the caller. Return NULL on error. */
static char* jmap_icaltime_to_localdate_r(icaltimetype icaltime) {
char *s;
time_t t;
s = xmalloc(RFC3339_DATETIME_MAX);
t = icaltime_as_timet(icaltime);
if (!jmap_timet_to_localdate(t, s, RFC3339_DATETIME_MAX)) {
return NULL;
}
return s;
}
/* Compare int in ascending order. */
static int jmap_intcmp(const void *aa, const void *bb)
{
const int *a = aa, *b = bb;
return (*a < *b) ? -1 : (*a > *b);
}
/* Compare time_t in ascending order. */
static int jmap_timetcmp(const void *aa, const void *bb)
{
const time_t *a = aa, *b = bb;
return (*a < *b) ? -1 : (*a > *b);
}
/* Return the identity of i. This is a helper for recur_byX. */
static int jmap_intident(int i) {
return i;
}
/* Convert libicals internal by_day encoding to JMAP byday. */
static int jmap_icalbyday_to_byday(int i) {
int w = icalrecurrencetype_day_position(i);
int d = icalrecurrencetype_day_day_of_week(i);
if (d) {
/* We could encounter libical's special ANY day here. But they don't
* care about it in the pos*7+dow computation. See more in the inline
* doc for icalrecurrencetype_day_day_of_week in icalrecur.c */
d--;
}
return d + 7*w;
}
/* Convert libicals internal by_month encoding to JMAP byday. */
static int jmap_icalbymonth_to_bymonth(int i) {
return i-1;
}
/* Convert at most nmemb entries in the ical recurrence byDay/Month/etc array
* named byX using conv. Return a new JSON array, sorted in ascending order. */
static json_t* jmap_recurrence_byX_from_ical(short byX[], size_t nmemb, int (*conv)(int)) {
json_t *jbd = json_pack("[]");
size_t i;
int tmp[nmemb];
for (i = 0; i < nmemb && byX[i] != ICAL_RECURRENCE_ARRAY_MAX; i++) {
tmp[i] = conv(byX[i]);
}
size_t n = i;
qsort(tmp, n, sizeof(int), jmap_intcmp);
for (i = 0; i < n; i++) {
json_array_append_new(jbd, json_pack("i", tmp[i]));
}
return jbd;
}
/* Convert the ical recurrence recur to a JMAP structure encoded in JSON using
* timezone id tzid for localdate conversions. */
static json_t* jmap_recurrence_from_ical(struct icalrecurrencetype recur, const char *tzid) {
json_t *jrecur = json_pack("{}");
/* frequency */
/* XXX - icalrecur depends on a recent change to libical. Might need to
* add support for this to Cyrus imap/ical_support.h for backward
* compatibility. */
char *s = xstrdup(icalrecur_freq_to_string(recur.freq));
char *p = s; for ( ; *p; ++p) *p = tolower(*p);
json_object_set_new(jrecur, "frequency", json_string(s));
free(s);
if (recur.interval > 1) {
json_object_set_new(jrecur, "interval", json_pack("i", recur.interval));
}
/* firstDayOfWeek */
short day = recur.week_start - 1;
if (day >= 0 && day != 1) {
json_object_set_new(jrecur, "firstDayOfWeek", json_pack("i", day));
}
if (recur.by_day[0] != ICAL_RECURRENCE_ARRAY_MAX) {
json_object_set_new(jrecur, "byDay",
jmap_recurrence_byX_from_ical(recur.by_day,
ICAL_BY_DAY_SIZE, &jmap_icalbyday_to_byday));
}
if (recur.by_month_day[0] != ICAL_RECURRENCE_ARRAY_MAX) {
json_object_set_new(jrecur, "byDate",
jmap_recurrence_byX_from_ical(recur.by_month_day,
ICAL_BY_MONTHDAY_SIZE, &jmap_intident));
}
if (recur.by_month[0] != ICAL_RECURRENCE_ARRAY_MAX) {
json_object_set_new(jrecur, "byMonth",
jmap_recurrence_byX_from_ical(recur.by_month,
ICAL_BY_MONTH_SIZE, &jmap_icalbymonth_to_bymonth));
}
if (recur.by_year_day[0] != ICAL_RECURRENCE_ARRAY_MAX) {
json_object_set_new(jrecur, "byYearDay",
jmap_recurrence_byX_from_ical(recur.by_year_day,
ICAL_BY_YEARDAY_SIZE, &jmap_intident));
}
if (recur.by_month[0] != ICAL_RECURRENCE_ARRAY_MAX) {
json_object_set_new(jrecur, "byWeekNo",
jmap_recurrence_byX_from_ical(recur.by_month,
ICAL_BY_MONTH_SIZE, &jmap_intident));
}
if (recur.by_hour[0] != ICAL_RECURRENCE_ARRAY_MAX) {
json_object_set_new(jrecur, "byHour",
jmap_recurrence_byX_from_ical(recur.by_hour,
ICAL_BY_HOUR_SIZE, &jmap_intident));
}
if (recur.by_minute[0] != ICAL_RECURRENCE_ARRAY_MAX) {
json_object_set_new(jrecur, "byMinute",
jmap_recurrence_byX_from_ical(recur.by_minute,
ICAL_BY_MINUTE_SIZE, &jmap_intident));
}
if (recur.by_second[0] != ICAL_RECURRENCE_ARRAY_MAX) {
json_object_set_new(jrecur, "bySecond",
jmap_recurrence_byX_from_ical(recur.by_second,
ICAL_BY_SECOND_SIZE, &jmap_intident));
}
if (recur.by_set_pos[0] != ICAL_RECURRENCE_ARRAY_MAX) {
json_object_set_new(jrecur, "bySetPosition",
jmap_recurrence_byX_from_ical(recur.by_set_pos,
ICAL_BY_SETPOS_SIZE, &jmap_intident));
}
if (recur.count != 0) {
/* Recur count takes precedence over until. */
json_object_set_new(jrecur, "count", json_pack("i", recur.count));
} else if (!icaltime_is_null_time(recur.until)) {
icaltimezone *tz = icaltimezone_get_builtin_timezone(tzid);
icaltimetype dtloc = icaltime_convert_to_zone(recur.until, tz);
char *until = jmap_icaltime_to_localdate_r(dtloc);
json_object_set_new(jrecur, "until", json_string(until));
free(until);
}
return jrecur;
}
/* Convert a VEVENT ical component to CalendarEvent attachments. */
static json_t* jmap_attachments_from_ical(icalcomponent *comp) {
icalproperty* prop;
json_t *ret = json_pack("[]");
for (prop = icalcomponent_get_first_property(comp, ICAL_ATTACH_PROPERTY);
prop;
prop = icalcomponent_get_next_property(comp, ICAL_ATTACH_PROPERTY)) {
icalattach *attach = icalproperty_get_attach(prop);
icalparameter *param = NULL;
json_t *file = NULL;
/* Ignore ATTACH properties with value BINARY. */
if (!attach || !icalattach_get_is_url(attach)) {
continue;
}
/* blobId */
/* XXX Bron: for now the blobId is the attachment URL. */
const char *url = icalattach_get_url(attach);
if (!url || !strlen(url)) {
continue;
}
file = json_pack("{s:s}", "blobId", url);
/* type */
param = icalproperty_get_first_parameter(prop, ICAL_FMTTYPE_PARAMETER);
if (param) {
const char *type = icalparameter_get_fmttype(param);
json_object_set_new(file, "type",
type && strlen(type) ? json_string(type) : json_null());
}
/* name */
/* XXX ALways null. */
json_object_set_new(file, "name", json_null());
/* size */
json_int_t size = -1;
param = icalproperty_get_first_parameter(prop, ICAL_SIZE_PARAMETER);
if (param) {
const char *s = icalparameter_get_size(param);
if (s) {
char *ptr;
size = strtol(s, &ptr, 10);
json_object_set_new(file, "size",
ptr && *ptr == '\0' ? json_integer(size) : json_null());
}
}
json_array_append_new(ret, file);
}
if (!json_array_size(ret)) {
json_decref(ret);
ret = json_null();
}
return ret;
}
/* Convert a VEVENT ical component to CalendarEvent inclusions. */
static json_t* jmap_inclusions_from_ical(icalcomponent *comp) {
icalproperty* prop;
size_t sincl = 8;
size_t nincl = 0;
time_t *incl = xmalloc(sincl * sizeof(time_t));
json_t *ret;
size_t i;
char timebuf[RFC3339_DATETIME_MAX];
/* Collect all RDATE occurrences as datetimes into incl. */
for(prop = icalcomponent_get_first_property(comp, ICAL_RDATE_PROPERTY);
prop;
prop = icalcomponent_get_next_property(comp, ICAL_RDATE_PROPERTY)) {
struct icaldatetimeperiodtype rdate;
time_t t;
rdate = icalproperty_get_rdate(prop);
if (!icalperiodtype_is_null_period(rdate.period)) {
continue;
}
if (icaltime_is_null_time(rdate.time)) {
continue;
}
t = icaltime_as_timet_with_zone(rdate.time, rdate.time.zone ?
rdate.time.zone : icaltimezone_get_utc_timezone());
if (nincl == sincl) {
sincl <<= 1;
incl = xrealloc(incl, sincl * sizeof(time_t));
}
incl[nincl++] = t;
}
if (!nincl) {
ret = json_null();
goto done;
}
/* Sort ascending. */
qsort(incl, nincl, sizeof(time_t), &jmap_timetcmp);
/* Convert incl to JMAP LocalDate. */
ret = json_pack("[]");
for (i = 0; i < nincl; ++i) {
int n = jmap_timet_to_localdate(incl[i], timebuf, RFC3339_DATETIME_MAX);
if (!n) continue;
json_array_append_new(ret, json_string(timebuf));
}
done:
free(incl);
return ret;
}
/* Convert the VALARMS in the VEVENT comp to CalendarEvent alerts. */
static json_t* jmap_alerts_from_ical(icalcomponent *comp) {
json_t* ret = json_pack("[]");
icalcomponent* alarm;
for (alarm = icalcomponent_get_first_component(comp, ICAL_VALARM_COMPONENT);
alarm;
alarm = icalcomponent_get_next_component(comp, ICAL_VALARM_COMPONENT)) {
icalproperty* prop;
icalvalue* val;
const char *type;
struct icaltriggertype trigger;
json_int_t diff;
/* type */
prop = icalcomponent_get_first_property(alarm, ICAL_ACTION_PROPERTY);
if (!prop) {
continue;
}
val = icalproperty_get_value(prop);
if (!val) {
continue;
}
enum icalproperty_action action = icalvalue_get_action(val);
if (action == ICAL_ACTION_EMAIL) {
type = "email";
} else {
type = "alert";
}
/* minutesBefore */
prop = icalcomponent_get_first_property(alarm, ICAL_TRIGGER_PROPERTY);
if (!prop) {
continue;
}
trigger = icalproperty_get_trigger(prop);
if (!icaldurationtype_is_null_duration(trigger.duration)) {
diff = icaldurationtype_as_int(trigger.duration) / -60;
} else {
icaltimetype tgtime = icaltime_convert_to_zone(trigger.time,
icaltimezone_get_utc_timezone());
time_t tg = icaltime_as_timet(tgtime);
icaltimetype dtstart = icaltime_convert_to_zone(
icalcomponent_get_dtstart(comp),
icaltimezone_get_utc_timezone());
time_t dt = icaltime_as_timet(dtstart);
diff = difftime(dt, tg) / (json_int_t) 60;
}
json_array_append_new(ret, json_pack("{s:s, s:i}",
"type", type, "minutesBefore", diff));
}
if (!json_array_size(ret)) {
json_decref(ret);
ret = json_null();
}
return ret;
}
/* Set isyou if userid matches the user looked up by caladdr. Return 0 on
* success or a Cyrus error on failure. */
static int jmap_isyou(const char *caladdr, const char *userid, short *isyou) {
struct sched_param sparam;
if (userid) {
sparam.userid = NULL;
int r = caladdress_lookup(caladdr, &sparam, userid);
if (r && r != HTTP_NOT_FOUND) {
syslog(LOG_ERR, "caladdress_lookup: failed to lookup caladdr %s: %s",
caladdr, error_message(r));
return r;
}
if (r != HTTP_NOT_FOUND && sparam.userid) {
*isyou = !strcmp(userid, sparam.userid) ;
} else {
*isyou = 0;
}
sched_param_free(&sparam);
}
return 0;
}
/* Convert the ical ORGANIZER/ATTENDEEs in comp to CalendarEvent
* participants, and store them in the pointers pointed to by
* organizer and attendees, or NULL. The participant isYou field
* is set, if this participant's caladdress belongs to userid. */
static void jmap_participants_from_ical(icalcomponent *comp,
json_t **organizer,
json_t **attendees,
const char *userid) {
icalproperty *prop;
icalparameter *param;
json_t *org = NULL;
json_t *atts = NULL;
const char *email;
short isYou;
struct hash_table *hatts = NULL;
int r;
/* Lookup ORGANIZER. */
prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
if (!prop) {
goto done;
}
org = json_pack("{}");
/* name */
param = icalproperty_get_first_parameter(prop, ICAL_CN_PARAMETER);
json_object_set_new(org, "name",
param ? json_string(icalparameter_get_cn(param)) : json_null());
/* email */
email = icalproperty_get_value_as_string(prop);
if (!strncmp(email, "mailto:", 7)) email += 7;
json_object_set_new(org, "email", json_string(email));
/* isYou */
r = jmap_isyou(email, userid, &isYou);
if (r) goto done;
json_object_set_new(org, "isYou", json_boolean(isYou));
/* Collect all attendees in a map so we can lookup delegates. */
hatts = xzmalloc(sizeof(struct hash_table));
construct_hash_table(hatts, 32, 0);
for (prop = icalcomponent_get_first_property(comp, ICAL_ATTENDEE_PROPERTY);
prop;
prop = icalcomponent_get_next_property(comp, ICAL_ATTENDEE_PROPERTY)) {
hash_insert(icalproperty_get_value_as_string(prop), prop, hatts);
}
if (!hash_numrecords(hatts)) {
goto done;
}
/* Convert all ATTENDEES. */
atts = json_pack("[]");
for (prop = icalcomponent_get_first_property(comp, ICAL_ATTENDEE_PROPERTY);
prop;
prop = icalcomponent_get_next_property(comp, ICAL_ATTENDEE_PROPERTY)) {
json_t *att = json_pack("{}");
/* name */
param = icalproperty_get_first_parameter(prop, ICAL_CN_PARAMETER);
json_object_set_new(att, "name",
param ? json_string(icalparameter_get_cn(param)) : json_null());
/* email */
email = icalproperty_get_value_as_string(prop);
if (!strncmp(email, "mailto:", 7)) email += 7;
json_object_set_new(att, "email", json_string(email));
/* rsvp */
const char *rsvp = NULL;
short depth = 0;
while (!rsvp) {
param = icalproperty_get_first_parameter(prop, ICAL_PARTSTAT_PARAMETER);
icalparameter_partstat pst = icalparameter_get_partstat(param);
switch (pst) {
case ICAL_PARTSTAT_ACCEPTED:
rsvp = "yes";
break;
case ICAL_PARTSTAT_DECLINED:
rsvp = "no";
break;
case ICAL_PARTSTAT_TENTATIVE:
rsvp = "maybe";
break;
case ICAL_PARTSTAT_DELEGATED:
param = icalproperty_get_first_parameter(prop, ICAL_DELEGATEDTO_PARAMETER);
if (param) {
const char *to = icalparameter_get_delegatedto(param);
prop = hash_lookup(to, hatts);
if (prop) {
/* Determine PARTSTAT from delegate. */
if (++depth > 64) {
/* This is a pathological case: libical does
* not check for inifite DELEGATE chains, so we
* make sure not to fall in an endless loop. */
syslog(LOG_ERR, "delegates exceed maximum recursion depth, ignoring rsvp");
rsvp = "";
}
continue;
}
}
/* fallthrough */
default:
rsvp = "";
}
}
json_object_set_new(att, "rsvp", json_string(rsvp));
/* isYou */
r = jmap_isyou(email, userid, &isYou);
if (r) goto done;
json_object_set_new(att, "isYou", json_boolean(isYou));
if (json_object_size(att)) {
json_array_append(atts, att);
}
json_decref(att);
}
done:
if (hatts) {
free_hash_table(hatts, NULL);
free(hatts);
}
if (org && atts) {
*organizer = org;
*attendees = atts;
json_incref(org);
json_incref(atts);
} else {
*organizer = NULL;
*attendees = NULL;
}
if (org) json_decref(org);
if (atts) json_decref(atts);
}
/* Determine the Olson TZID, if any, of the ical property prop. */
static const char *jmap_tzid_from_icalprop(icalproperty *prop, int guess) {
const char *tzid = NULL;
icalparameter *param = NULL;
if (prop) param = icalproperty_get_first_parameter(prop, ICAL_TZID_PARAMETER);
if (param) tzid = icalparameter_get_tzid(param);
/* Check if the tzid already corresponds to an Olson name. */
if (tzid) {
icaltimezone *tz = icaltimezone_get_builtin_timezone(tzid);
if (!tz && guess) {
/* Try to guess the timezone. */
icalvalue *val = icalproperty_get_value(prop);
icaltimetype dt = icalvalue_get_datetime(val);
tzid = dt.zone ? icaltimezone_get_location((icaltimezone*) dt.zone) : NULL;
tzid = tzid && icaltimezone_get_builtin_timezone(tzid) ? tzid : NULL;
}
}
return tzid;
}
/* Determine the Olson TZID, if any, of the ical property kind in component comp. */
static const char *jmap_tzid_from_ical(icalcomponent *comp,
icalproperty_kind kind) {
icalproperty *prop = icalcomponent_get_first_property(comp, kind);
if (!prop) {
return NULL;
}
return jmap_tzid_from_icalprop(prop, 1/*guess*/);
}
/* Convert the libical VEVENT comp to a CalendarEvent, excluding the
* exceptions property. If exc is true, only convert properties that are
* valid for exceptions. If userid is not NULL it will be used to identify
* participants.
* Only convert the properties named in props. */
static json_t* jmap_calendarevent_from_ical(icalcomponent *comp,
struct hash_table *props,
short exc,
const char *userid) {
icalproperty* prop;
json_t *obj;
obj = json_pack("{}");
/* Always determine isAllDay to set start, end and timezone fields. */
int isAllDay = icaltime_is_date(icalcomponent_get_dtstart(comp));
if (_wantprop(props, "isAllDay") && !exc) {
json_object_set_new(obj, "isAllDay", json_boolean(isAllDay));
}
/* Convert properties. */
/* summary */
if (_wantprop(props, "summary")) {
prop = icalcomponent_get_first_property(comp, ICAL_SUMMARY_PROPERTY);
if (prop || !exc) {
json_object_set_new(obj, "summary",
prop ? json_string(icalproperty_get_value_as_string(prop)) : json_string(""));
}
}
/* description */
if (_wantprop(props, "description")) {
prop = icalcomponent_get_first_property(comp, ICAL_DESCRIPTION_PROPERTY);
if (prop || !exc) {
json_object_set_new(obj, "description",
prop ? json_string(icalproperty_get_value_as_string(prop)) : json_string(""));
}
}
/* location */
if (_wantprop(props, "location")) {
prop = icalcomponent_get_first_property(comp, ICAL_LOCATION_PROPERTY);
if (prop || !exc) {
json_object_set_new(obj, "location",
prop ? json_string(icalproperty_get_value_as_string(prop)) : json_string(""));
}
}
/* showAsFree */
if (_wantprop(props, "showAsFree")) {
prop = icalcomponent_get_first_property(comp, ICAL_TRANSP_PROPERTY);
if (prop || !exc) {
json_object_set_new(obj, "showAsFree",
json_boolean(prop &&
!strcmp(icalproperty_get_value_as_string(prop), "TRANSPARENT")));
}
}
/* start */
if (_wantprop(props, "start")) {
struct icaltimetype dt = icalcomponent_get_dtstart(comp);
char *s = jmap_icaltime_to_localdate_r(dt);
json_object_set_new(obj, "start", json_string(s));
free(s);
}
/* end */
if (_wantprop(props, "end")) {
struct icaltimetype dt = icalcomponent_get_dtend(comp);
if (icaltime_is_null_time(dt) && !exc) {
dt = icalcomponent_get_dtstart(comp);
}
if (!icaltime_is_null_time(dt)) {
char *s = jmap_icaltime_to_localdate_r(dt);
json_object_set_new(obj, "end", json_string(s));
free(s);
}
}
/* Always determine the event's start timezone. */
const char *tzidstart = jmap_tzid_from_ical(comp, ICAL_DTSTART_PROPERTY);
/* startTimeZone */
if (_wantprop(props, "startTimeZone")) {
json_object_set_new(obj, "startTimeZone",
tzidstart && !isAllDay ? json_string(tzidstart) : json_null());
}
/* endTimeZone */
if (_wantprop(props, "endTimeZone")) {
const char *tzidend = jmap_tzid_from_ical(comp, ICAL_DTEND_PROPERTY);
if (!tzidend) {
tzidend = tzidstart;
}
json_object_set_new(obj, "endTimeZone",
tzidend && !isAllDay ? json_string(tzidend) : json_null());
}
/* recurrence */
if (_wantprop(props, "recurrence") && !exc) {
json_t *recur = NULL;
prop = icalcomponent_get_first_property(comp, ICAL_RRULE_PROPERTY);
if (prop) {
recur = jmap_recurrence_from_ical(icalproperty_get_rrule(prop), tzidstart);
}
json_object_set_new(obj, "recurrence", recur ? recur : json_null());
}
/* inclusions */
if (_wantprop(props, "inclusions") && !exc) {
json_object_set_new(obj, "inclusions", jmap_inclusions_from_ical(comp));
}
/* Do not convert exceptions. */
/* alerts */
if (_wantprop(props, "alerts")) {
json_object_set_new(obj, "alerts", jmap_alerts_from_ical(comp));
}
/* organizer and attendees */
if (_wantprop(props, "organizer") || _wantprop(props, "attendees")) {
json_t *organizer, *attendees;
jmap_participants_from_ical(comp, &organizer, &attendees, userid);
if (organizer && _wantprop(props, "organizer")) {
json_object_set_new(obj, "organizer", organizer);
}
if (attendees && _wantprop(props, "attendees")) {
json_object_set_new(obj, "attendees", attendees);
}
}
/* attachments */
if (_wantprop(props, "attachments") && !exc) {
json_object_set_new(obj, "attachments", jmap_attachments_from_ical(comp));
}
return obj;
}
static int getcalendarevents_cb(void *rock, struct caldav_data *cdata)
{
struct calendars_rock *crock = (struct calendars_rock *)rock;
struct index_record record;
int r = 0;
icalcomponent* ical = NULL;
icalcomponent* comp;
icalproperty* prop;
const char *userid = crock->req->userid;
json_t *obj;
if (!cdata->dav.alive) {
return 0;
}
/* Open calendar mailbox. */
if (!crock->mailbox || strcmp(crock->mailbox->name, cdata->dav.mailbox)) {
mailbox_close(&crock->mailbox);
r = mailbox_open_irl(cdata->dav.mailbox, &crock->mailbox);
if (r) goto done;
}
/* Locate calendar event ical data in mailbox. */
r = mailbox_find_index_record(crock->mailbox, cdata->dav.imap_uid, &record);
if (r) goto done;
crock->rows++;
/* Load VEVENT from record. */
ical = record_to_ical(crock->mailbox, &record);
if (!ical) {
syslog(LOG_ERR, "record_to_ical failed for record %u:%s",
cdata->dav.imap_uid, crock->mailbox->name);
r = IMAP_INTERNAL;
goto done;
}
/* Locate the main VEVENT. */
for (comp = icalcomponent_get_first_component(ical, ICAL_VEVENT_COMPONENT);
comp;
comp = icalcomponent_get_next_component(ical, ICAL_VEVENT_COMPONENT)) {
if (!icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY)) {
break;
}
}
if (!comp) {
syslog(LOG_ERR, "no VEVENT in record %u:%s",
cdata->dav.imap_uid, crock->mailbox->name);
r = IMAP_INTERNAL;
goto done;
}
/* Convert main VEVENT to JMAP. */
obj = jmap_calendarevent_from_ical(comp, crock->props, 0 /* exc */, userid);
if (!obj) goto done;
json_object_set_new(obj, "id", json_string(cdata->ical_uid));
/* Add optional exceptions. */
if (_wantprop(crock->props, "exceptions")) {
json_t* excobj = json_pack("{}");
/* Add all EXDATEs as null value. */
for (prop = icalcomponent_get_first_property(comp, ICAL_EXDATE_PROPERTY);
prop;
prop = icalcomponent_get_next_property(comp, ICAL_EXDATE_PROPERTY)) {
struct icaltimetype exdate = icalproperty_get_exdate(prop);
if (icaltime_is_null_time(exdate)) {
continue;
}
char *s = jmap_icaltime_to_localdate_r(exdate);
json_object_set_new(excobj, s, json_null());
free(s);
}
/* Add VEVENTs with RECURRENCE-ID. */
for (comp = icalcomponent_get_first_component(ical, ICAL_VEVENT_COMPONENT);
comp;
comp = icalcomponent_get_next_component(ical, ICAL_VEVENT_COMPONENT)) {
if (!icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY)) {
continue;
}
json_t *exc = jmap_calendarevent_from_ical(comp, crock->props, 1 /* exc */, userid);
if (!exc) {
continue;
}
struct icaltimetype dtstart = icalcomponent_get_dtstart(comp);
char *s = jmap_icaltime_to_localdate_r(dtstart);
json_object_set_new(excobj, s, exc);
free(s);
}
if (json_object_size(excobj)) {
json_object_set(obj, "exceptions", excobj);
}
json_decref(excobj);
}
/* Add JMAP-only fields. */
if (_wantprop(crock->props, "x-href")) {
_add_xhref(obj, cdata->dav.mailbox, cdata->dav.resource);
}
if (_wantprop(crock->props, "calendarId")) {
json_object_set_new(obj, "calendarId", json_string(strrchr(cdata->dav.mailbox, '.')+1));
}
json_array_append_new(crock->array, obj);
done:
if (ical) icalcomponent_free(ical);
return r;
}
static int getCalendarEvents(struct jmap_req *req)
{
struct calendars_rock rock;
int r = 0;
r = caldav_create_defaultcalendars(req->userid);
if (r) return r;
rock.array = json_pack("[]");
rock.req = req;
rock.props = NULL;
rock.rows = 0;
rock.mailbox = NULL;
json_t *properties = json_object_get(req->args, "properties");
if (properties) {
rock.props = xzmalloc(sizeof(struct hash_table));
construct_hash_table(rock.props, 1024, 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->userid);
if (!db) {
syslog(LOG_ERR, "caldav_open_mailbox failed for user %s", req->userid);
r = IMAP_INTERNAL;
goto done;
}
json_t *want = json_object_get(req->args, "ids");
json_t *notfound = json_array();
if (want) {
int i;
int size = json_array_size(want);
for (i = 0; i < size; i++) {
rock.rows = 0;
const char *id = json_string_value(json_array_get(want, i));
r = caldav_get_events(db, NULL, id, &getcalendarevents_cb, &rock);
if (r || !rock.rows) {
json_array_append_new(notfound, json_string(id));
}
}
} else {
rock.rows = 0;
r = caldav_get_events(db, NULL, NULL, &getcalendarevents_cb, &rock);
if (r) goto done;
}
json_t *events = json_pack("{}");
r = jmap_setstate(req, events, "state", MBTYPE_CALENDAR, 1 /* refresh */, 0 /* bump */);
if (r) goto done;
json_incref(rock.array);
json_object_set_new(events, "accountId", json_string(req->userid));
json_object_set_new(events, "list", rock.array);
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("calendarEvents"));
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.array);
if (db) caldav_close(db);
if (rock.mailbox) mailbox_close(&rock.mailbox);
return r;
}
/* Add tz to the rocks timezone cache, only if it doesn't point to a previously
* cached timezone. Compare by pointers, which works for builtin timezones. */
static void calevent_rock_add_tz(calevent_rock *rock, icaltimezone *tz) {
/* Yes, we could use a map here, but we don't expect the number of
* timezones per VEVENT to be more than a handful. */
size_t i;
for (i = 0; i < rock->n_tzs; i++) {
if (rock->tzs[i] == tz) {
return;
}
}
if (rock->n_tzs == rock->s_tzs) {
rock->s_tzs = rock->s_tzs ? rock->s_tzs * 2 : 1;
rock->tzs = xrealloc(rock->tzs, sizeof(icaltimezone*) * rock->s_tzs);
}
rock->tzs[rock->n_tzs++] = tz;
}
static void calevent_rock_free(struct calevent_rock *rock) {
/* All other fields are allocated outside our scope. */
free(rock->tzs);
}
/* Add or overwrite the datetime property kind in comp. If tz is not NULL, set
* the TZID parameter on the property. */
static void jmap_update_dtprop_bykind(icalcomponent *comp,
icaltimetype dt,
icaltimezone *tz,
int purge,
enum icalproperty_kind kind) {
icalproperty *prop;
if (purge) {
/* Purge the existing property. */
prop = icalcomponent_get_first_property(comp, kind);
if (prop) icalcomponent_remove_property(comp, prop);
icalproperty_free(prop);
}
/* Set the new property. */
prop = icalproperty_new(kind);
icalproperty_set_value(prop, icalvalue_new_datetime(dt));
if (tz) {
icalparameter *param = icalproperty_get_first_parameter(prop, ICAL_TZID_PARAMETER);
const char *tzid = icaltimezone_get_location(tz);
if (param) {
icalparameter_set_tzid(param, tzid);
} else {
icalproperty_add_parameter(prop,icalparameter_new_tzid(tzid));
}
}
icalcomponent_add_property(comp, prop);
}
/* Return non-zero if the ical property TZID parameter matches the
* location of tz, or if both are in floating time. */
static int jmap_dtprop_is_in_timezone(icalproperty *prop, icaltimezone *tz) {
icalparameter *param = icalproperty_get_first_parameter(prop, ICAL_TZID_PARAMETER);
const char *tzid = param ? icalparameter_get_tzid(param) : NULL;
if (!tz && !tzid) {
/* Check if the DATETIME value is in UTC. */
icalvalue *val = icalproperty_get_value(prop);
if (!val) {
return 0;
}
icaltimetype dt = icalvalue_get_datetime(val);
if (icaltime_is_null_time(dt)) {
return 0;
}
/* Return true for floating time. */
return dt.zone == NULL;
}
if (tz && tzid) {
/* Check if they both match the same singleton builtin timezone. */
icaltimezone *a = icaltimezone_get_builtin_timezone(tzid);
icaltimezone *b = icaltimezone_get_builtin_timezone(icaltimezone_get_location(tz));
return a == b;
}
return 0;
}
/* Update the TZID parameter of prop to the TZID of tz, or remove any TZID
* parameter from prop if tz is NULL. */
static void jmap_dtprop_update_tzid(icalproperty *prop, icaltimezone *tz) {
const char *tzid = tz ? icaltimezone_get_location(tz) : NULL;
icalparameter *param = icalproperty_get_first_parameter(prop, ICAL_TZID_PARAMETER);
if (param) {
icalproperty_remove_parameter_by_ref(prop, param);
}
if (tzid) {
param = icalparameter_new_tzid(tzid);
icalproperty_add_parameter(prop, param);
}
}
/* Create or update the ORGANIZER/ATTENDEEs in the VEVENT component comp as
* defined by the JMAP organizer and attendees. Purge any participants that
* are not updated. */
static void jmap_participants_to_ical(icalcomponent *comp,
json_t *organizer,
json_t *attendees,
calevent_rock *rock) {
int create = rock->flags & JMAP_CREATE;
json_t *invalid = rock->invalid;
const char *name = NULL;
const char *email = NULL;
const char *rsvp = NULL;
struct buf buf = BUF_INITIALIZER;
size_t i;
icalproperty *prop, *next;
json_t *att;
hash_table cache;
/* XXX - Integrate iTIP, once all the semantics JMAP<->iTIP are agreed. */
/* Purge existing ORGANIZER and ATTENDEEs only if instructed to do so. */
if (organizer == json_null() && attendees == json_null()) {
prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
icalcomponent_remove_property(comp, prop);
icalproperty_free(prop);
for (prop = icalcomponent_get_first_property(comp, ICAL_ATTENDEE_PROPERTY);
prop;
prop = next) {
next = icalcomponent_get_next_property(comp, ICAL_ATTENDEE_PROPERTY);
icalcomponent_remove_property(comp, prop);
icalproperty_free(prop);
}
return;
}
/* organizer */
jmap_readprop_full(organizer, "organizer", "name", create, invalid, "s", &name);
jmap_readprop_full(organizer, "organizer", "email", create, invalid, "s", &email);
if (name && email) {
prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
if (prop) {
/* Remove but keep property to preserve ical parameters. */
icalcomponent_remove_property(comp, prop);
buf_printf(&buf, "mailto:%s", email);
icalproperty_set_value_from_string(prop, buf_cstring(&buf), "NO");
buf_reset(&buf);
} else {
prop = icalproperty_new_organizer(email);
}
icalparameter *param = icalproperty_get_first_parameter(prop, ICAL_CN_PARAMETER);
if (param) {
icalproperty_remove_parameter_by_ref(prop, param);
}
param = icalparameter_new_cn(name);
icalproperty_add_parameter(prop, param);
icalcomponent_add_property(comp, prop);
}
if (!json_array_size(attendees)) {
return;
}
/* Move all current ATTENDEEs with a mailto caladdr to the cache. */
construct_hash_table(&cache, json_array_size(attendees), 0);
for (prop = icalcomponent_get_first_property(comp, ICAL_ATTENDEE_PROPERTY);
prop;
prop = next) {
next = icalcomponent_get_next_property(comp, ICAL_ATTENDEE_PROPERTY);
const char *val = icalproperty_get_value_as_string(prop);
if (!val) {
continue;
}
if (strncasecmp(val, "mailto:", 7)) {
continue;
}
val += 7;
if (!*val) {
continue;
}
icalcomponent_remove_property(comp, prop);
hash_insert(val, prop, &cache);
}
/* Iterate the JMAP attendees to create or update the iCalendar ATTENDEES. */
json_array_foreach(attendees, i, att) {
char *prefix;
icalparameter_partstat pst = ICAL_PARTSTAT_NONE;
name = NULL; email = NULL;
rsvp = NULL;
buf_printf(&buf, "attendees[%llu]", (long long unsigned) i);
prefix = buf_newcstring(&buf);
buf_reset(&buf);
jmap_readprop_full(att, prefix, "name", create, invalid, "s", &name);
jmap_readprop_full(att, prefix, "email", create, invalid, "s", &email);
jmap_readprop_full(att, prefix, "rsvp", create, invalid, "s", &rsvp);
if (rsvp) {
if (!strcmp(rsvp, "")) {
pst = ICAL_PARTSTAT_NEEDSACTION;
} else if (!strcmp(rsvp, "yes")) {
pst = ICAL_PARTSTAT_ACCEPTED;
} else if (!strcmp(rsvp, "maybe")) {
pst = ICAL_PARTSTAT_TENTATIVE;
} else if (!strcmp(rsvp, "no")) {
pst = ICAL_PARTSTAT_DECLINED;
} else {
buf_printf(&buf, "%s.%s", prefix, "rsvp");
json_array_append_new(invalid, json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
}
if (name && email && pst != ICAL_PARTSTAT_NONE) {
/* Move the attendee either from the cache or create a new one. */
prop = (icalproperty*) hash_lookup(email, &cache);
if (prop) hash_del(email, &cache);
if (!prop) {
buf_printf(&buf, "mailto:%s", email);
prop = icalproperty_new_attendee(buf_cstring(&buf));
buf_reset(&buf);
}
icalparameter *param;
/* name */
param = icalproperty_get_first_parameter(prop, ICAL_CN_PARAMETER);
if (param) {
icalproperty_remove_parameter_by_ref(prop, param);
}
param = icalparameter_new_cn(name);
icalproperty_add_parameter(prop, param);
/* partstat */
param = icalproperty_get_first_parameter(prop, ICAL_PARTSTAT_PARAMETER);
if (param) {
icalproperty_remove_parameter_by_ref(prop, param);
}
param = icalparameter_new_partstat(pst);
icalproperty_add_parameter(prop, param);
icalcomponent_add_property(comp, prop);
}
free(prefix);
}
free_hash_table(&cache, (void(*)(void*)) icalproperty_free);
buf_free(&buf);
}
static void jmap_byday_to_ical(struct buf *buf, int val) {
int day = 0;
int week = 0;
if (val >= 0) {
day = val % 7;
week = val / 7;
} else {
day = (7 + (val % 7)) % 7;
week = (val - 6) / 7;
}
if (week) {
buf_printf(buf, "%+d", week);
}
buf_printf(buf, icalrecur_weekday_to_string(day+1));
}
static void jmap_month_to_ical(struct buf *buf, int val) {
buf_printf(buf, "%d", val+1);
}
static void jmap_int_to_ical(struct buf *buf, int val) {
buf_printf(buf, "%d", val);
}
/* Convert and print the JMAP byX recurrence value to ical into buf, otherwise
* report the erroneous fieldName as invalid. If lower or upper is not NULL,
* make sure that every byX value is within these bounds. */
static void jmap_recurrence_byX_to_ical(json_t *byX,
struct buf *buf,
const char *tag,
int *lower,
int *upper,
int allowZero,
const char *fieldName,
json_t *invalid,
void(*conv)(struct buf*, int)) {
/* Make sure there is at least on entry. */
if (!json_array_size(byX)) {
json_array_append_new(invalid, json_string(fieldName));
return;
}
/* Convert the array. */
buf_printf(buf, ";%s=", tag);
size_t i;
for (i = 0; i < json_array_size(byX); i++) {
int val;
int err = json_unpack(json_array_get(byX, i), "i", &val);
if (!err && !allowZero && !val) {
err = 1;
}
if (!err && ((lower && val < *lower) || (upper && val > *upper))) {
err = 2;
}
if (err) {
struct buf b = BUF_INITIALIZER;
buf_printf(&b, "%s[%llu]", fieldName, (long long unsigned) i);
json_array_append_new(invalid, json_string(buf_cstring(&b)));
buf_free(&b);
continue;
}
/* Prepend leading comma, if not first parameter value. */
if (i) {
buf_printf(buf, "%c", ',');
}
/* Convert the byX value to ical. */
conv(buf, val);
}
}
/* Update the TZID parameters of VEVENT comp's EXDATEs and any ot its
* exceptions. */
static void jmap_exceptions_update_tz(icalcomponent *comp,
calevent_rock *rock) {
const char *tzid;
icaltimezone *tz;
/* Change the TZID of all EXDATEs that are in the former startTimezone. */
icalproperty *prop;
for (prop = icalcomponent_get_first_property(comp, ICAL_EXDATE_PROPERTY);
prop;
prop = icalcomponent_get_next_property(comp, ICAL_EXDATE_PROPERTY)) {
if (jmap_dtprop_is_in_timezone(prop, rock->tzstart_old)) {
jmap_dtprop_update_tzid(prop, rock->tzstart);
} else {
tzid = jmap_tzid_from_icalprop(prop, 1 /*guess*/);
if (tzid) tz = icaltimezone_get_builtin_timezone(tzid);
if (tz) calevent_rock_add_tz(rock, tz);
}
}
/* Update the TZIDs of each VEVENT with RECURRENCE-ID. */
icalcomponent *excomp;
icalcomponent *ical = icalcomponent_get_parent(comp);
for (excomp = icalcomponent_get_first_component(ical, ICAL_VEVENT_COMPONENT);
excomp;
excomp = icalcomponent_get_next_component(ical, ICAL_VEVENT_COMPONENT)) {
prop = icalcomponent_get_first_property(excomp, ICAL_RECURRENCEID_PROPERTY);
if (!prop) continue;
/* Rewrite TZID of RECURRENCE-ID. */
if (jmap_dtprop_is_in_timezone(prop, rock->tzstart_old)) {
jmap_dtprop_update_tzid(prop, rock->tzstart);
} else {
tzid = jmap_tzid_from_icalprop(prop, 1 /*guess*/);
if (tzid) tz = icaltimezone_get_builtin_timezone(tzid);
if (tz) calevent_rock_add_tz(rock, tz);
}
/* Rewrite TZID of DTSTART. */
if ((prop = icalcomponent_get_first_property(excomp, ICAL_DTSTART_PROPERTY))) {
if (jmap_dtprop_is_in_timezone(prop, rock->tzstart_old)) {
jmap_dtprop_update_tzid(prop, rock->tzstart);
} else {
tzid = jmap_tzid_from_icalprop(prop, 1 /*guess*/);
if (tzid) tz = icaltimezone_get_builtin_timezone(tzid);
if (tz) calevent_rock_add_tz(rock, tz);
}
}
/* Rewrite TZID of DTEND. */
if ((prop = icalcomponent_get_first_property(excomp, ICAL_DTEND_PROPERTY))) {
if (jmap_dtprop_is_in_timezone(prop, rock->tzend_old)) {
jmap_dtprop_update_tzid(prop, rock->tzend);
} else {
tzid = jmap_tzid_from_icalprop(prop, 1 /*guess*/);
if (tzid) tz = icaltimezone_get_builtin_timezone(tzid);
if (tz) calevent_rock_add_tz(rock, tz);
}
}
}
}
/* Create or overwrite the VEVENT exceptions for VEVENT component comp as
* defined by the JMAP exceptions. */
static void jmap_exceptions_to_ical(icalcomponent *comp,
json_t *exceptions,
calevent_rock *rock) {
json_t *invalid = rock->invalid;
/* Purge existing EXDATEs. */
icalproperty *prop, *next;
for (prop = icalcomponent_get_first_property(comp, ICAL_EXDATE_PROPERTY);
prop;
prop = next) {
next = icalcomponent_get_next_property(comp, ICAL_EXDATE_PROPERTY);
icalcomponent_remove_property(comp, prop);
icalproperty_free(prop);
}
if (exceptions == json_null()) {
return;
}
const char *key;
json_t *exc;
struct buf buf = BUF_INITIALIZER;
icalcomponent *ical = icalcomponent_get_parent(comp);
icalcomponent *excomp, *excomp_next;
hash_table excs;
/* Move VEVENT exceptions to a temporary cache, keyed by recurrence id. */
construct_hash_table(&excs, json_object_size(exceptions), 0);
for (excomp = icalcomponent_get_first_component(ical, ICAL_VEVENT_COMPONENT);
excomp;
excomp = excomp_next) {
excomp_next = icalcomponent_get_next_component(ical, ICAL_VEVENT_COMPONENT);
prop = icalcomponent_get_first_property(excomp, ICAL_RECURRENCEID_PROPERTY);
if (!prop) {
continue;
}
/* We only key by LocalDate (instead of LocalDate+TZID) to identify
* changes to existing exceptions that have their timezone changed.
* This means that there can't be two exceptions at the same LocalDate
* in two different timezones. */
const char *val = icalproperty_get_value_as_string(prop);
if (val) {
hash_insert(val, excomp, &excs);
}
icalcomponent_remove_component(ical, excomp);
}
/* Add updated or new exceptions back to the VCALENDAR component. */
json_object_foreach(exceptions, key, exc) {
char *prefix;
buf_printf(&buf, "exceptions[%s]", key);
prefix = xstrdup(buf_cstring(&buf));
buf_reset(&buf);
/* Parse key as LocalDate. */
icaltimetype dt;
if (jmap_localdate_to_icaltime(key, &dt, rock->tzstart, rock->isAllDay)) {
json_array_append_new(invalid, json_string(prefix));
free(prefix);
continue;
}
if (exc != json_null()) {
json_t *invalidexc = json_pack("[]");
size_t i;
json_t *v;
excomp = NULL;
/* Check if the cache already contains this recurrence id. */
const char *val = icaltime_as_ical_string(dt);
excomp = (icalcomponent*) hash_lookup(val, &excs);
if (excomp) hash_del(val, &excs);
/* If not found, create a new one. */
if (!excomp) excomp = icalcomponent_new_vevent();
/* Add exceptional VEVENT component to the VCALENDAR. */
icalcomponent_add_component(ical, excomp);
/* Make sure not to overwrite the main timezone rock. Since an
* exception must not contain other exceptions, there can't
* be any timezones added (and hence realloced) to the rock. */
calevent_rock myrock = *rock;
myrock.flags = JMAP_EXC;
myrock.recurid = key;
jmap_calendarevent_to_ical(excomp, exc, &myrock);
/* XXX - that's ugly. Need to make sure that the rocks timezone
* array still points to latest realloced memory block. */
rock->tzs = myrock.tzs;
rock->n_tzs = myrock.n_tzs;
rock->s_tzs = myrock.s_tzs;
/* Prepend prefix to any invalid properties. */
json_array_foreach(invalidexc, i, v) {
buf_printf(&buf, "%s.%s", prefix, json_string_value(v));
json_array_append_new(invalid, json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
json_decref(invalidexc);
} else {
/* Add EXDATE to the VEVENT. */
/* iCalendar allows to set multiple EXDATEs. */
jmap_update_dtprop_bykind(comp, dt, rock->tzstart, 0 /*purge*/, ICAL_EXDATE_PROPERTY);
}
free(prefix);
}
/* Purge any remaining VEVENTs from the cache. */
free_hash_table(&excs, (void(*)(void*)) icalcomponent_free);
buf_free(&buf);
}
/* Set the TZID parameters for all RDATE properties. */
static void jmap_inclusions_update_tz(icalcomponent *comp,
calevent_rock *rock) {
icalproperty *prop;
for (prop = icalcomponent_get_first_property(comp, ICAL_RDATE_PROPERTY);
prop;
prop = icalcomponent_get_next_property(comp, ICAL_RDATE_PROPERTY)) {
icalparameter *param = icalproperty_get_first_parameter(prop, ICAL_TZID_PARAMETER);
if (param) {
if (rock->tzstart) {
const char *tzid = icaltimezone_get_location(rock->tzstart);
if (tzid) {
icalparameter_set_tzid(param, tzid);
}
} else {
icalproperty_remove_parameter(prop, ICAL_TZID_PARAMETER);
}
}
}
}
/* Create or overwrite the RDATEs in the VEVENT component comp as defined by the
* JMAP recurrence. Use tz as timezone for LocalDate conversions. */
static void jmap_inclusions_to_ical(icalcomponent *comp,
json_t *inclusions,
calevent_rock *rock) {
size_t i;
json_t *incl;
struct buf buf = BUF_INITIALIZER;
icalproperty *prop, *next;
json_t *invalid = rock->invalid;
/* Purge existing RDATEs. */
for (prop = icalcomponent_get_first_property(comp, ICAL_RDATE_PROPERTY);
prop;
prop = next) {
next = icalcomponent_get_next_property(comp, ICAL_RDATE_PROPERTY);
icalcomponent_remove_property(comp, prop);
icalproperty_free(prop);
}
if (!inclusions || inclusions == json_null()) {
return;
}
/* Add RDATEs.*/
json_array_foreach(inclusions, i, incl) {
icaltimetype dt;
/* Parse incl as LocalDate. */
if (jmap_localdate_to_icaltime(json_string_value(incl), &dt, rock->tzstart, rock->isAllDay)) {
buf_printf(&buf, "inclusions[%llu]", (long long unsigned) i);
json_array_append_new(invalid, json_string(buf_cstring(&buf)));
buf_reset(&buf);
continue;
}
/* Create and add RDATE property. */
jmap_update_dtprop_bykind(comp, dt, rock->tzstart, 0 /*purge*/, ICAL_RDATE_PROPERTY);
}
buf_free(&buf);
}
/* Create or overwrite the VEVENT attachments for VEVENT component comp as
* defined by the JMAP exceptions. */
static void jmap_attachments_to_ical(icalcomponent *comp,
json_t *attachments,
calevent_rock *rock) {
hash_table atts;
icalproperty *prop, *next;
struct buf buf = BUF_INITIALIZER;
json_t *invalid = rock->invalid;
/* Move existing URL attachments to a temporary cache. */
construct_hash_table(&atts, json_array_size(attachments) + 1, 0);
for (prop = icalcomponent_get_first_property(comp, ICAL_ATTACH_PROPERTY);
prop;
prop = next) {
next = icalcomponent_get_next_property(comp, ICAL_ATTACH_PROPERTY);
icalattach *attach = icalproperty_get_attach(prop);
/* Ignore binary attachments. */
if (!attach || !icalattach_get_is_url(attach)) {
continue;
}
/* Ignore malformed URLs. */
const char *url = icalattach_get_url(attach);
if (!url || !strlen(url)) {
continue;
}
icalcomponent_remove_property(comp, prop);
hash_insert(url, prop, &atts);
}
/* Create or update attachments. */
size_t i;
json_t *attachment;
json_array_foreach(attachments, i, attachment) {
int pe;
const char *blobId = NULL;
const char *type = NULL;
const char *name = NULL;
json_int_t size = -1;
char *prefix;
buf_printf(&buf, "attachments[%llu]", (long long unsigned) i);
prefix = buf_newcstring(&buf);
buf_reset(&buf);
/* Parse and validate JMAP File object. */
pe = jmap_readprop_full(attachment, prefix, "blobId", 1, invalid, "s", &blobId);
if (pe > 0) {
if (!strlen(blobId)) {
buf_printf(&buf, "%s.%s", prefix, "blobId");
json_array_append_new(invalid, json_string(buf_cstring(&buf)));
buf_reset(&buf);
blobId = NULL;
}
}
if (json_object_get(attachment, "type") != json_null()) {
jmap_readprop_full(attachment, prefix, "type", 0, invalid, "s", &type);
}
if (json_object_get(attachment, "name") != json_null()) {
jmap_readprop_full(attachment, prefix, "name", 0, invalid, "s", &name);
}
if (json_object_get(attachment, "size") != json_null()) {
pe = jmap_readprop_full(attachment, prefix, "size", 0, invalid, "I", &size);
if (pe > 0 && size < 0) {
buf_printf(&buf, "%s.%s", prefix, "size");
json_array_append_new(invalid, json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
}
if (blobId && !json_array_size(invalid)) {
/* blobId */
prop = (icalproperty*) hash_lookup(blobId, &atts);
if (prop) {
hash_del(blobId, &atts);
} else {
icalattach *icalatt = icalattach_new_from_url(blobId);
prop = icalproperty_new_attach(icalatt);
icalattach_unref(icalatt);
}
/* type */
icalparameter *param = icalproperty_get_first_parameter(prop, ICAL_FMTTYPE_PARAMETER);
if (param) icalproperty_remove_parameter_by_ref(prop, param);
if (type) {
icalproperty_add_parameter(prop, icalparameter_new_fmttype(type));
}
/* name */
/* XXX Could use Microsoft's X-FILENAME parameter to store name,
* but that's only for binary attachments. For now, ignore name. */
/* size */
param = icalproperty_get_first_parameter(prop, ICAL_SIZE_PARAMETER);
if (param) icalproperty_remove_parameter_by_ref(prop, param);
if (size >= 0) {
buf_printf(&buf, "%lld", (long long) size);
icalproperty_add_parameter(prop, icalparameter_new_size(buf_cstring(&buf)));
buf_reset(&buf);
}
/* Add ATTACH property. */
icalcomponent_add_property(comp, prop);
}
free(prefix);
buf_free(&buf);
}
/* Purge any remaining URL attachments from the cache. */
free_hash_table(&atts, (void(*)(void*)) icalproperty_free);
}
/* Rewrite the UTC-formatted UNTIL dates in the RRULE of VEVENT comp. */
static void jmap_recurrence_update_tz(icalcomponent *comp,
calevent_rock *rock) {
icalproperty *prop = icalcomponent_get_first_property(comp, ICAL_RRULE_PROPERTY);
if (!prop) {
return;
}
struct icalrecurrencetype rrule = icalproperty_get_rrule(prop);
if (icaltime_is_null_time(rrule.until)) {
return;
}
icaltimezone *utc = icaltimezone_get_utc_timezone();
icaltimetype dt = icaltime_convert_to_zone(rrule.until, rock->tzstart_old);
dt.zone = rock->tzstart;
rrule.until = icaltime_convert_to_zone(dt, utc);
icalproperty_set_rrule(prop, rrule);
}
/* Create or overwrite the RRULE in the VEVENT component comp as defined by the
* JMAP recurrence. */
static void jmap_recurrence_to_ical(icalcomponent *comp,
json_t *recur,
calevent_rock *rock) {
const char *prefix = "recurrence";
const char *freq = NULL;
struct buf buf = BUF_INITIALIZER;
int pe;
icalproperty *prop, *next;
json_t *invalid = rock->invalid;
/* Purge existing RRULE. */
for (prop = icalcomponent_get_first_property(comp, ICAL_RRULE_PROPERTY);
prop;
prop = next) {
next = icalcomponent_get_next_property(comp, ICAL_RRULE_PROPERTY);
icalcomponent_remove_property(comp, prop);
icalproperty_free(prop);
}
if (!recur || recur == json_null()) {
return;
}
/* frequency */
pe = jmap_readprop_full(recur, prefix, "frequency", 1, invalid, "s", &freq);
if (pe > 0) {
char *s = xstrndup(freq, 64);
char *p = s; for ( ; *p; ++p) *p = toupper(*p);
buf_printf(&buf, "FREQ=%s", s);
free(s);
}
/* interval */
int interval = 1;
pe = jmap_readprop_full(recur, prefix, "interval", 0, invalid, "i", &interval);
if (pe > 0) {
if (interval > 1) {
buf_printf(&buf, ";INTERVAL=%d", interval);
} else {
json_array_append_new(invalid, json_string("recurrence.interval"));
}
}
/* firstDayOfWeek */
int day = 1;
pe = jmap_readprop_full(recur, prefix, "firstDayOfWeek", 0, invalid, "i", &day);
if (pe > 0) {
if (day == 0) {
buf_printf(&buf, ";WKST=SU");
} else if (day > 1 && day <= 6) {
buf_printf(&buf, ";WKST=%s", icalrecur_weekday_to_string(day+1));
} else {
json_array_append_new(invalid, json_string("recurrence.firstDayOfWeek"));
}
}
/* byDay */
int lower, upper;
json_t *byday = NULL;
pe = jmap_readprop_full(recur, prefix, "byDay", 0, invalid, "o", &byday);
if (pe > 0) {
jmap_recurrence_byX_to_ical(byday, &buf, "BYDAY",
NULL /* lower */, NULL /* upper */, 1 /* allowZero */,
"recurrence.byDay", invalid, jmap_byday_to_ical);
}
/* byDate */
json_t *bydate = NULL;
lower = -31;
upper = 31;
pe = jmap_readprop_full(recur, prefix, "byDate", 0, invalid, "o", &bydate);
if (pe > 0) {
jmap_recurrence_byX_to_ical(bydate, &buf, "BYDATE",
&lower, &upper, 0 /* allowZero */,
"recurrence.byDate", invalid, jmap_int_to_ical);
}
/* byMonth */
json_t *bymonth = NULL;
lower = 0;
upper = 11;
pe = jmap_readprop_full(recur, prefix, "byMonth", 0, invalid, "o", &bymonth);
if (pe > 0) {
jmap_recurrence_byX_to_ical(bymonth, &buf, "BYMONTH",
&lower, &upper, 0 /* allowZero */,
"recurrence.byMonth", invalid, jmap_month_to_ical);
}
/* byYearDay */
json_t *byyearday = NULL;
lower = -366;
upper = 366;
pe = jmap_readprop_full(recur, prefix, "byYearDay", 0, invalid, "o", &byyearday);
if (pe > 0) {
jmap_recurrence_byX_to_ical(byyearday, &buf, "BYYEARDAY",
&lower, &upper, 0 /* allowZero */,
"recurrence.byYearDay", invalid, jmap_int_to_ical);
}
/* byWeekNo */
json_t *byweekno = NULL;
lower = -53;
upper = 53;
pe = jmap_readprop_full(recur, prefix, "byWeekNo", 0, invalid, "o", &byweekno);
if (pe > 0) {
jmap_recurrence_byX_to_ical(byweekno, &buf, "BYWEEKNO",
&lower, &upper, 0 /* allowZero */,
"recurrence.byWeekNo", invalid, jmap_int_to_ical);
}
/* byHour */
json_t *byhour = NULL;
lower = 0;
upper = 23;
pe = jmap_readprop_full(recur, prefix, "byHour", 0, invalid, "o", &byhour);
if (pe > 0) {
jmap_recurrence_byX_to_ical(byhour, &buf, "BYHOUR",
&lower, &upper, 1 /* allowZero */,
"recurrence.byHour", invalid, jmap_int_to_ical);
}
/* byMinute */
json_t *byminute = NULL;
lower = 0;
upper = 59;
pe = jmap_readprop_full(recur, prefix, "byMinute", 0, invalid, "o", &byminute);
if (pe > 0) {
jmap_recurrence_byX_to_ical(byminute, &buf, "BYMINUTE",
&lower, &upper, 1 /* allowZero */,
"recurrence.byMinute", invalid, jmap_int_to_ical);
}
/* bySecond */
json_t *bysecond = NULL;
lower = 0;
upper = 59;
pe = jmap_readprop_full(recur, prefix, "bySecond", 0, invalid, "o", &bysecond);
if (pe > 0) {
jmap_recurrence_byX_to_ical(bysecond, &buf, "BYSECOND",
&lower, &upper, 1 /* allowZero */,
"recurrence.bySecond", invalid, jmap_int_to_ical);
}
/* bySetPos */
json_t *bysetpos = NULL;
lower = 0;
upper = 59;
pe = jmap_readprop_full(recur, prefix, "bySetPos", 0, invalid, "o", &bysetpos);
if (pe > 0) {
jmap_recurrence_byX_to_ical(bysetpos, &buf, "BYSETPOS",
&lower, &upper, 1 /* allowZero */,
"recurrence.bySetPos", invalid, jmap_int_to_ical);
}
if (json_object_get(recur, "count") && json_object_get(recur, "until")) {
json_array_append_new(invalid, json_string("recurrence.count"));
json_array_append_new(invalid, json_string("recurrence.until"));
}
/* count */
int count;
pe = jmap_readprop_full(recur, prefix, "count", 0, invalid, "i", &count);
if (pe > 0) {
if (count > 0) {
buf_printf(&buf, ";COUNT=%d", count);
} else {
json_array_append_new(invalid, json_string("recurrence.count"));
}
}
/* until */
const char *until;
pe = jmap_readprop_full(recur, prefix, "until", 0, invalid, "s", &until);
if (pe > 0) {
icaltimetype dtloc;
if (!jmap_localdate_to_icaltime(until, &dtloc, rock->tzstart, rock->isAllDay)) {
icaltimezone *utc = icaltimezone_get_utc_timezone();
icaltimetype dt = icaltime_convert_to_zone(dtloc, utc);
buf_printf(&buf, ";UNTIL=%s", icaltime_as_ical_string(dt));
} else {
json_array_append_new(invalid, json_string("until"));
}
}
if (json_array_size(invalid)) {
buf_free(&buf);
return;
}
/* Parse buf to make sure is valid. */
struct icalrecurrencetype rt = icalrecurrencetype_from_string(buf_cstring(&buf));
if (rt.freq == ICAL_NO_RECURRENCE) {
/* We somehow broke the RRULE value. Report the recurrence as invalid
* so we won't save it, but most probably the error is on our side. */
syslog(LOG_ERR, "Could not parse RRULE %s: %s",
buf_cstring(&buf), icalerror_strerror(icalerrno));
json_array_append_new(invalid, json_string("recurrence"));
buf_free(&buf);
return;
}
/* Add RRULE property to comp. */
icalcomponent_add_property(comp, icalproperty_new_rrule(rt));
buf_free(&buf);
}
/* Create or update the VALARMs in the VEVENT component comp as defined by the
* JMAP alerts. */
static void jmap_alerts_to_ical(icalcomponent *comp,
json_t *alerts,
calevent_rock *rock) {
size_t i;
json_t *alert;
struct buf buf = BUF_INITIALIZER;
json_t *invalid = rock->invalid;
/* Purge all VALARMs. */
icalcomponent *alarm, *next;
for (alarm = icalcomponent_get_first_component(comp, ICAL_VALARM_COMPONENT);
alarm;
alarm = next) {
next = icalcomponent_get_next_component(comp, ICAL_VALARM_COMPONENT);
icalcomponent_remove_component(comp, alarm);
icalcomponent_free(alarm);
}
if (alerts == json_null()) {
return;
}
json_array_foreach(alerts, i, alert) {
enum icalproperty_action action = ICAL_ACTION_NONE;
const char *type = NULL;
int diff = 0;
char *prefix;
int pe;
buf_reset(&buf);
buf_printf(&buf, "alerts[%llu]", (long long unsigned) i);
prefix = buf_newcstring(&buf);
buf_reset(&buf);
/* type */
pe = jmap_readprop_full(alert, prefix, "type", 1, invalid, "s", &type);
if (pe > 0) {
if (!strncmp(type, "email", 6)) {
action = ICAL_ACTION_EMAIL;
} else if (!strncmp(type, "alert", 6)) {
action = ICAL_ACTION_DISPLAY;
} else {
buf_printf(&buf, "%s.type", prefix);
json_array_append(invalid, json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
}
/* minutesBefore */
pe = jmap_readprop_full(alert, prefix, "minutesBefore", 1, invalid, "i", &diff);
if (pe > 0 && action != ICAL_ACTION_NONE) {
struct icaltriggertype trigger = icaltriggertype_from_int(diff * -60);
icalcomponent *alarm = icalcomponent_new_valarm();
icalproperty *prop;
/* action */
prop = icalproperty_new_action(action);
icalcomponent_add_property(alarm, prop);
/* trigger */
prop = icalproperty_new_trigger(trigger);
icalcomponent_add_property(alarm, prop);
/* alert contents */
/* XXX - how to determine these properties? */
if (action == ICAL_ACTION_EMAIL) {
prop = icalproperty_new_description("the body of an email alert");
icalcomponent_add_property(alarm, prop);
prop = icalproperty_new_summary("the subject of an email alert");
icalcomponent_add_property(alarm, prop);
buf_printf(&buf, "MAILTO:%s", rock->req->userid);
prop = icalproperty_new_attendee(buf_cstring(&buf));
buf_reset(&buf);
icalcomponent_add_property(alarm, prop);
} else {
prop = icalproperty_new_description("a display alert");
icalcomponent_add_property(alarm, prop);
}
/* Add VALARM to VEVENT. */
icalcomponent_add_component(comp, alarm);
}
free(prefix);
}
buf_free(&buf);
}
static void jmap_timezones_to_ical_cb(icalcomponent *comp,
struct icaltime_span *span,
void *periodrock) {
struct icalperiodtype *period = (struct icalperiodtype *) periodrock;
int is_date = icaltime_is_date(icalcomponent_get_dtstart(comp));
icaltimezone *utc = icaltimezone_get_utc_timezone();
struct icaltimetype start =
icaltime_from_timet_with_zone(span->start, is_date, utc);
struct icaltimetype end =
icaltime_from_timet_with_zone(span->end, is_date, utc);
if (icaltime_compare(start, period->start) < 0)
memcpy(&period->start, &start, sizeof(struct icaltimetype));
if (icaltime_compare(end, period->end) > 0)
memcpy(&period->end, &end, sizeof(struct icaltimetype));
}
/* Determine the UTC time span of all components within ical of type kind. */
static struct icalperiodtype jmap_get_utc_timespan(icalcomponent *ical,
icalcomponent_kind kind) {
/* XXX This is almost identical to what's done in caldav_db's writeentry
* function. But here, we want to collect also the timezone IDs in our
* custom timezone rock. This might warrant some recfactoring, but let's
* keep them separated for now. */
struct icalperiodtype span;
icalcomponent *comp = icalcomponent_get_first_component(ical, kind);
int recurring = 0;
/* Initialize span to be nothing */
span.start = icaltime_from_timet_with_zone(caldav_eternity, 0, NULL);
span.end = icaltime_from_timet_with_zone(caldav_epoch, 0, NULL);
span.duration = icaldurationtype_null_duration();
do {
struct icalperiodtype period;
icalproperty *rrule;
icalproperty *purged_rrule = NULL;
/* Get base dtstart and dtend */
caldav_get_period(comp, kind, &period);
/* See if its a recurring event */
rrule = icalcomponent_get_first_property(comp, ICAL_RRULE_PROPERTY);
if (rrule ||
icalcomponent_get_first_property(comp, ICAL_RDATE_PROPERTY) ||
icalcomponent_get_first_property(comp, ICAL_EXDATE_PROPERTY)) {
/* Recurring - find widest time range that includes events */
int expand = recurring = 1;
if (rrule) {
struct icalrecurrencetype recur = icalproperty_get_rrule(rrule);
if (!icaltime_is_null_time(recur.until)) {
/* Recurrence ends - calculate dtend of last recurrence */
struct icaldurationtype duration;
icaltimezone *utc = icaltimezone_get_utc_timezone();
duration = icaltime_subtract(period.end, period.start);
period.end =
icaltime_add(icaltime_convert_to_zone(recur.until, utc),
duration);
/* Do RDATE expansion only */
/* Temporarily remove RRULE to allow for expansion of
* remaining recurrences. */
icalcomponent_remove_property(comp, rrule);
purged_rrule = rrule;
}
else if (!recur.count) {
/* Recurrence never ends - set end of span to eternity */
span.end =
icaltime_from_timet_with_zone(caldav_eternity, 0, NULL);
/* Skip RRULE & RDATE expansion */
expand = 0;
}
}
/* Expand (remaining) recurrences */
if (expand) {
icalcomponent_foreach_recurrence(
comp,
icaltime_from_timet_with_zone(caldav_epoch, 0, NULL),
icaltime_from_timet_with_zone(caldav_eternity, 0, NULL),
jmap_timezones_to_ical_cb, &span);
}
/* Add RRULE again, if we had removed it before. */
if (purged_rrule) {
icalcomponent_add_property(comp, purged_rrule);
}
}
/* Check our dtstart and dtend against span */
if (icaltime_compare(period.start, span.start) < 0)
memcpy(&span.start, &period.start, sizeof(struct icaltimetype));
if (icaltime_compare(period.end, span.end) > 0)
memcpy(&span.end, &period.end, sizeof(struct icaltimetype));
} while ((comp = icalcomponent_get_next_component(ical, kind)));
return span;
}
/* Convert the calendar event rocks timezones to VTIMEZONEs in the
* VCALENDAR component ical. */
static void jmap_timezones_to_ical(icalcomponent *ical,
calevent_rock *tzrock) {
icalcomponent *tzcomp, *next;
icalproperty *prop;
struct icalperiodtype span;
/* Determine recurrence span. */
span = jmap_get_utc_timespan(ical, ICAL_VEVENT_COMPONENT);
/* Remove all VTIMEZONE components for known TZIDs. This operation is
* a bit hairy: we could expunge a timezone which is in use by an ical
* property that is unknown to us. But since we don't know what to
* look for, we can't make sure to preserve these timezones. */
for (tzcomp = icalcomponent_get_first_component(ical, ICAL_VTIMEZONE_COMPONENT);
tzcomp;
tzcomp = next) {
next = icalcomponent_get_next_component(ical,
ICAL_VTIMEZONE_COMPONENT);
prop = icalcomponent_get_first_property(tzcomp, ICAL_TZID_PROPERTY);
if (prop) {
const char *tzid = icalproperty_get_tzid(prop);
if (icaltimezone_get_builtin_timezone(tzid)) {
icalcomponent_remove_component(ical, tzcomp);
icalcomponent_free(tzcomp);
}
}
}
/* Add the start and end timezones to the rock. */
if (tzrock->tzstart) {
calevent_rock_add_tz(tzrock, tzrock->tzstart);
}
if (tzrock->tzend) {
calevent_rock_add_tz(tzrock, tzrock->tzend);
}
/* Now add each timezone in the rock, truncated by this events span. */
size_t i;
for (i = 0; i < tzrock->n_tzs; i++) {
icaltimezone *tz = tzrock->tzs[i];
/* Clone tz to overwrite its TZID property. */
icalcomponent *tzcomp = icalcomponent_new_clone(icaltimezone_get_component(tz));
icalproperty *tzprop = icalcomponent_get_first_property(tzcomp, ICAL_TZID_PROPERTY);
icalproperty_set_tzid(tzprop, icaltimezone_get_location(tz));
/* Truncate the timezone to the events timespan. */
icaltimetype tzdtstart = icaltime_convert_to_zone(span.start, tz);
icaltimetype tzdtend = icaltime_convert_to_zone(span.end, tz);
tzdist_truncate_vtimezone(tzcomp, &tzdtstart, &tzdtend);
/* Add the truncated timezone. */
icalcomponent_add_component(ical, tzcomp);
}
}
static void jmap_calendarevent_dt_to_ical(icalcomponent *comp,
json_t *event,
calevent_rock *rock) {
const char *tzid;
int pe;
const char *val = NULL;
struct icaltimetype dtstart = icaltime_null_time();
struct icaltimetype dtend = icaltime_null_time();
int create = rock->flags & JMAP_CREATE;
int exc = rock->flags & JMAP_EXC;
json_t *invalid = rock->invalid;
/* startTimeZone */
/* Determine the current timezone, if any. */
tzid = jmap_tzid_from_ical(comp, ICAL_DTSTART_PROPERTY);
if (tzid) rock->tzstart_old = icaltimezone_get_builtin_timezone(tzid);
/* Read the new timezone, if any. */
if (json_object_get(event, "startTimeZone") != json_null()) {
pe = jmap_readprop(event, "startTimeZone", create&&!exc, invalid, "s", &val);
if (pe > 0) {
/* Lookup the new timezone. */
rock->tzstart = icaltimezone_get_builtin_timezone(val);
if (!rock->tzstart) {
json_array_append_new(invalid, json_string("startTimeZone"));
}
} else if (!pe && !exc && !rock->tzstart) {
/* If this is not a create and no startTimezone was read,
* keep the current timezone. */
rock->tzstart = rock->tzstart_old;
}
} else {
/* The startTimeZone is explicitly set to null. */
rock->tzstart = NULL;
}
if (create) {
/* If this is a create, then initialize also the old timezone to this
* new event's timezone. Some conversion routines will look at it. */
rock->tzstart_old = rock->tzstart;
}
if (rock->isAllDay && rock->tzstart) {
/* Validate that if isAllDay is set, no timezone must be set. */
json_array_append_new(invalid, json_string("startTimeZone"));
}
/* endTimeZone */
tzid = jmap_tzid_from_ical(comp, ICAL_DTEND_PROPERTY);
if (!tzid) tzid = jmap_tzid_from_ical(comp, ICAL_DTSTART_PROPERTY);
if (tzid) rock->tzend_old = icaltimezone_get_builtin_timezone(tzid);
if (json_object_get(event, "endTimeZone") != json_null()) {
pe = jmap_readprop(event, "endTimeZone", create&&!exc, invalid, "s", &val);
if (pe > 0) {
rock->tzend = icaltimezone_get_builtin_timezone(val);
if (!rock->tzend) {
json_array_append_new(invalid, json_string("endTimeZone"));
}
} else if (!pe && !exc && !rock->tzend) {
rock->tzend = rock->tzend_old;
}
} else {
rock->tzend = NULL;
}
if (create) {
rock->tzend_old = rock->tzend;
}
if (rock->isAllDay && rock->tzend) {
json_array_append_new(invalid, json_string("endTimeZone"));
}
/* start */
pe = jmap_readprop(event, "start", create&&!exc, invalid, "s", &val);
if (!pe && exc) {
if (!icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY)) {
/* Its OK for an exception to not define the start field. But in
* this case we need to infer DTSTART and RECURRENCEID from the
* JMAP LocalDate recurrence id of this exception. */
val = rock->recurid;
pe = 1;
}
}
if (pe > 0) {
if (!jmap_localdate_to_icaltime(val, &dtstart, rock->tzstart, rock->isAllDay)) {
jmap_update_dtprop_bykind(comp, dtstart, rock->tzstart, 1 /*purge*/, ICAL_DTSTART_PROPERTY);
if (exc) {
jmap_update_dtprop_bykind(comp, dtstart, rock->tzstart, 1 /*purge*/, ICAL_RECURRENCEID_PROPERTY);
}
} else {
json_array_append_new(invalid, json_string("start"));
}
}
if (!pe && !create && rock->tzstart_old != rock->tzstart) {
/* The client changed the startTimeZone but not the start time. */
icaltimetype dt = icalcomponent_get_dtstart(comp);
if (!icaltime_is_null_time(dt)) {
dt.zone = rock->tzstart;
jmap_update_dtprop_bykind(comp, dt, rock->tzstart, 1 /*purge*/, ICAL_DTSTART_PROPERTY);
}
}
/* end */
pe = jmap_readprop(event, "end", create&&!exc, invalid, "s", &val);
if (pe > 0) {
if (!jmap_localdate_to_icaltime(val, &dtend, rock->tzend, rock->isAllDay)) {
jmap_update_dtprop_bykind(comp, dtend, rock->tzend, 1 /*purge*/, ICAL_DTEND_PROPERTY);
} else {
json_array_append_new(invalid, json_string("end"));
}
} else if (!pe && !create && rock->tzend_old != rock->tzend) {
/* The client changed the endTimeZone but not the end time. */
icaltimetype dt = icalcomponent_get_dtend(comp);
if (!icaltime_is_null_time(dt)) {
dt.zone = rock->tzend;
jmap_update_dtprop_bykind(comp, dt, rock->tzend, 1 /*purge*/, ICAL_DTEND_PROPERTY);
}
}
/* The end date MUST be equal to or after the start date when both are
* converted to UTC time. */
dtstart = icalcomponent_get_dtstart(comp);
dtend = icalcomponent_get_dtend(comp);
if (icaltime_is_null_time(dtend)) dtend = dtstart;
if (icaltime_compare(dtstart, dtend) > 0) {
json_array_append_new(invalid, json_string("end"));
}
}
/* Create or overwrite the iCalendar properties in VEVENT comp based on the
* properties the JMAP calendar event. Collect all required timezone ids in
* rock. */
static void jmap_calendarevent_to_ical(icalcomponent *comp,
json_t *event,
calevent_rock *rock) {
int pe; /* parse error */
const char *val = NULL;
int showAsFree = 0;
icalproperty *prop = NULL;
int create = rock->flags & JMAP_CREATE;
int exc = rock->flags & JMAP_EXC;
json_t *invalid = rock->invalid;
/* uid */
icalcomponent_set_uid(comp, rock->uid);
/* summary */
pe = jmap_readprop(event, "summary", create&&!exc, invalid, "s", &val);
if (pe > 0) {
icalcomponent_set_summary(comp, val);
}
/* description */
pe = jmap_readprop(event, "description", create&&!exc, invalid, "s", &val);
if (pe > 0) {
icalcomponent_set_description(comp, val);
}
/* location */
pe = jmap_readprop(event, "location", create&&!exc, invalid, "s", &val);
if (pe > 0) {
icalcomponent_set_location(comp, val);
}
/* showAsFree */
pe = jmap_readprop(event, "showAsFree", create&&!exc, invalid, "b", &showAsFree);
if (pe > 0) {
enum icalproperty_transp v = showAsFree ? ICAL_TRANSP_TRANSPARENT : ICAL_TRANSP_OPAQUE;
prop = icalcomponent_get_first_property(comp, ICAL_TRANSP_PROPERTY);
if (prop) {
icalproperty_set_transp(prop, v);
} else {
icalcomponent_add_property(comp, icalproperty_new_transp(v));
}
}
/* isAllDay */
jmap_readprop(event, "isAllDay", create&&!exc, invalid, "b", &rock->isAllDay);
/* start */
/* end */
/* startTimeZone */
/* endTimeZone */
jmap_calendarevent_dt_to_ical(comp, event, rock);
/* organizer and attendees */
json_t *organizer = NULL;
json_t *attendees = NULL;
jmap_readprop(event, "organizer", 0, invalid, "o", &organizer);
jmap_readprop(event, "attendees", 0, invalid, "o", &attendees);
if (organizer == json_null() && attendees == json_null()) {
/* Remove both organizer and attendees from event. */
jmap_participants_to_ical(comp, organizer, attendees, rock);
} else if (organizer && attendees && json_array_size(attendees)) {
/* Add or update both organizer and attendees. */
jmap_participants_to_ical(comp, organizer, attendees, rock);
} else if (organizer || attendees) {
/* Any other combination is an error. */
json_array_append_new(invalid, json_string("attendees"));
}
/* alerts */
json_t *alerts = NULL;
pe = jmap_readprop(event, "alerts", 0, invalid, "o", &alerts);
if (pe > 0) {
if (alerts == json_null() || json_array_size(alerts)) {
jmap_alerts_to_ical(comp, alerts, rock);
} else {
json_array_append_new(invalid, json_string("alerts"));
}
} else if (!pe && !create && rock->tzstart_old != rock->tzstart) {
/* The start timezone has changed but none of the alerts. */
/* This is where we would like to update the timezones of any VALARMs
* that have a TRIGGER value type of DATETIME (instead of the usual
* DURATION type). Unfortunately, these DATETIMEs are stored in UTC.
* Hence we can't tell, if the event owner really wants to wake up
* at e.g. 1am UTC or if it just was close to a local datetime during
* creation of the iCalendar file. For now, do nothing about that. */
}
/* recurrence */
json_t *recurrence = NULL;
pe = jmap_readprop(event, "recurrence", 0, invalid, "o", &recurrence);
if (pe > 0) {
if (!exc) {
jmap_recurrence_to_ical(comp, recurrence, rock);
} else {
json_array_append_new(invalid, json_string("recurrence"));
}
} else if (!pe && !create && rock->tzstart_old != rock->tzstart) {
/* The start timezone has changed but none of the recurrences. */
jmap_recurrence_update_tz(comp, rock);
}
/* inclusions */
json_t *inclusions = NULL;
pe = jmap_readprop(event, "inclusions", 0, invalid, "o", &inclusions);
if (pe > 0) {
if (!exc && (inclusions == json_null() || json_array_size(inclusions))) {
jmap_inclusions_to_ical(comp, inclusions, rock);
} else {
json_array_append_new(invalid, json_string("inclusions"));
}
} else if (!pe && !create && rock->tzstart_old != rock->tzstart) {
/* The start timezone has changed but none of the inclusions. */
jmap_inclusions_update_tz(comp, rock);
}
/* exceptions */
json_t *exceptions = NULL;
pe = jmap_readprop(event, "exceptions", 0, invalid, "o", &exceptions);
if (pe > 0) {
if (!exc && (exceptions == json_null() || json_object_size(exceptions))) {
jmap_exceptions_to_ical(comp, exceptions, rock);
} else {
json_array_append_new(invalid, json_string("exceptions"));
}
} else if (!pe && !create && (rock->tzstart_old != rock->tzstart || rock->tzend_old != rock->tzend)) {
/* The start or end timezone has changed but none of the exceptions. */
jmap_exceptions_update_tz(comp, rock);
}
/* attachments */
json_t *attachments = NULL;
pe = jmap_readprop(event, "attachments", 0, invalid, "o", &attachments);
if (pe > 0) {
if (!exc && (attachments == json_null() || json_array_size(attachments))) {
jmap_attachments_to_ical(comp, attachments, rock);
} else {
json_array_append_new(invalid, json_string("attachments"));
}
}
if (json_array_size(invalid)) {
return;
}
/* Check JMAP specification conditions on the generated iCalendar file, so
* this also doubles as a sanity check. Note that we *could* report a
* property here as invalid, which had only been set by the client in a
* previous request. */
/* If recurrence is null, inclusions and exceptions MUST also be null. */
if (!exc && !icalcomponent_get_first_property(comp, ICAL_RRULE_PROPERTY)) {
if (icalcomponent_get_first_property(comp, ICAL_RDATE_PROPERTY)) {
json_array_append_new(invalid, json_string("inclusions"));
}
if (icalcomponent_get_first_property(comp, ICAL_EXDATE_PROPERTY)) {
json_array_append_new(invalid, json_string("exceptions"));
}
if (!json_array_size(invalid)) {
icalcomponent *ical = icalcomponent_get_parent(comp);
icalcomponent *iter;
for (iter = icalcomponent_get_first_component(ical, ICAL_VEVENT_COMPONENT);
iter;
iter = icalcomponent_get_next_component(ical, ICAL_VEVENT_COMPONENT)) {
if (icalcomponent_get_first_property(iter, ICAL_RECURRENCEID_PROPERTY)) {
json_array_append_new(invalid, json_string("exceptions"));
break;
}
}
}
}
/* Either both organizer and attendees are null, or neither are. */
if ((icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY) == NULL) !=
(icalcomponent_get_first_property(comp, ICAL_ATTENDEE_PROPERTY) == NULL)) {
json_array_append_new(invalid, json_string("organizer"));
json_array_append_new(invalid, json_string("attendees"));
}
}
static int jmap_schedule_ical(const char *userid,
icalcomponent *oldical,
icalcomponent *ical,
int mode)
{
struct sched_param sparam;
int r;
const char *organizer = NULL;
icalcomponent *comp = NULL;
/* Determine if any scheduling is required. */
icalcomponent *src = mode&JMAP_DESTROY ? oldical : ical;
comp = icalcomponent_get_first_component(src, ICAL_VEVENT_COMPONENT);
icalproperty *prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
if (prop) organizer = icalproperty_get_organizer(prop);
if (!organizer) {
r = 0;
goto done;
}
/* Lookup the organizer. */
memset(&sparam, 0, sizeof(struct sched_param));
r = caladdress_lookup(organizer, &sparam, userid);
if (r == HTTP_NOT_FOUND) {
r = 0; /* Skip non-local organizer. */ /* XXX need to handle non-local organizers? */
goto done;
} else if (r) {
syslog(LOG_ERR, "failed to process scheduling message (org=%s)",
organizer);
goto done;
}
/* Validate create/update. */
if (mode & (JMAP_CREATE|JMAP_UPDATE)) {
/* Don't allow ORGANIZER to be changed */
const char *oldorganizer = NULL;
if (oldical) {
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) {
const char *p = organizer;
if (!strncasecmp(p, "mailto:", 7)) p += 7;
if (strcmp(oldorganizer, p)) {
/* XXX This should become a set error. */
r = IMAP_INTERNAL;
goto done;
}
}
}
if (organizer && /* XXX Hack for Outlook */
icalcomponent_get_first_invitee(comp)) {
/* Send scheduling message. */
if (!strcmpsafe(sparam.userid, userid)) {
/* Organizer scheduling object resource */
sched_request(userid, organizer, &sparam, oldical, ical, 0);
} else {
/* Attendee scheduling object resource */
sched_reply(userid, oldical, ical);
}
}
done:
sched_param_free(&sparam);
return r;
}
/* Create, update or destroy the JMAP calendar event. Mode must be one of
* JMAP_CREATE, JMAP_UPDATE or JMAP_DESTROY. Return 0 for success and non-
* fatal errors. */
static int jmap_write_calendarevent(json_t *event,
struct caldav_db *db,
const char *uid,
int mode,
json_t *notWritten,
struct jmap_req *req)
{
int create = mode & JMAP_CREATE;
int update = mode & JMAP_UPDATE;
int destroy = mode & JMAP_DESTROY;
int r, rights, 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;
icalcomponent *comp;
struct index_record record;
struct calevent_rock rock;
json_t *invalid = json_pack("[]");
const char *calendarId = NULL;
if (!destroy) {
/* Look up the calendarId property. */
pe = jmap_readprop(event, "calendarId", create /*mandatory*/, invalid, "s", &calendarId);
if (pe > 0 && !strlen(calendarId)) {
json_array_append_new(invalid, json_string("calendarId"));
} else if (pe > 0 && *calendarId == '#') {
const char *id = (const char *) hash_lookup(calendarId, req->idmap);
if (id != NULL) {
calendarId = id;
} else {
json_array_append_new(invalid, json_string("calendarId"));
}
}
if (calendarId && jmap_calendar_ishidden(calendarId)) {
json_t *err = json_pack("{s:s}", "type", "calendarNotFound");
json_object_set_new(notWritten, uid, err);
r = 0; goto done;
}
}
/* Handle any calendarId 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(notWritten, uid, err);
r = 0; goto done;
}
/* Determine mailbox and resource name of calendar event. */
if (update || destroy) {
r = caldav_lookup_uid(db, uid, &cdata);
if (r && r != CYRUSDB_NOTFOUND) {
syslog(LOG_ERR, "caldav_lookup_uid(%s) failed: %s",
uid, error_message(r));
goto done;
}
if (r == CYRUSDB_NOTFOUND || !cdata->dav.alive ||
!cdata->dav.rowid || !cdata->dav.imap_uid) {
json_t *err = json_pack("{s:s}", "type", "notFound");
json_object_set_new(notWritten, uid, err);
r = 0; goto done;
}
mboxname = xstrdup(cdata->dav.mailbox);
resource = xstrdup(cdata->dav.resource);
} else if (create) {
struct buf buf = BUF_INITIALIZER;
mboxname = caldav_mboxname(req->userid, calendarId);
buf_printf(&buf, "%s.ics", uid);
resource = buf_newcstring(&buf);
buf_free(&buf);
}
/* Open mailbox for writing */
r = mailbox_open_iwl(mboxname, &mbox);
if (r == IMAP_MAILBOX_NONEXISTENT) {
json_t *err = json_pack("{s:s}", "type", "calendarNotFound");
json_object_set_new(notWritten, uid, err);
r = 0; goto done;
} else if (r) {
syslog(LOG_ERR, "mailbox_open_iwl(%s) failed: %s",
mboxname, error_message(r));
goto done;
}
/* Check permissions. */
rights = httpd_myrights(req->authstate, mbox->acl);
if (!(rights & needrights)) {
/* Pretend this mailbox does not exist. */
json_t *err = json_pack("{s:s}", "type", "notFound");
json_object_set_new(notWritten, uid, err);
r = 0; goto done;
}
if (!create) {
/* 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_t *err = json_pack("{s:s}", "type", "notFound");
json_object_set_new(notWritten, uid, err);
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;
}
}
if (!create) {
/* Load VEVENT from record. */
ical = record_to_ical(mbox, &record);
if (!ical) {
syslog(LOG_ERR, "record_to_ical failed for record %u:%s",
cdata->dav.imap_uid, mbox->name);
r = IMAP_INTERNAL;
goto done;
}
/* Locate the main VEVENT. */
for (comp = icalcomponent_get_first_component(ical, ICAL_VEVENT_COMPONENT);
comp;
comp = icalcomponent_get_next_component(ical, ICAL_VEVENT_COMPONENT)) {
if (!icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY)) {
break;
}
}
if (!comp) {
syslog(LOG_ERR, "no VEVENT in record %u:%s",
cdata->dav.imap_uid, mbox->name);
r = IMAP_INTERNAL;
goto done;
}
if (update) {
oldical = icalcomponent_new_clone(ical);
} else if (destroy) {
oldical = ical;
ical = NULL;
}
} else {
/* Create a new VCALENDAR and its VEVENT. */
ical = icalcomponent_new_vcalendar();
icalcomponent_add_property(ical, icalproperty_new_version("2.0"));
icalcomponent_add_property(ical, icalproperty_new_calscale("GREGORIAN"));
comp = icalcomponent_new_vevent();
icalcomponent_add_component(ical, comp);
}
if (!destroy) {
/* Convert the JMAP calendar event to ical. */
memset(&rock, 0, sizeof(struct calevent_rock));
rock.flags = create ? JMAP_CREATE : JMAP_UPDATE;
rock.req = req;
rock.invalid = invalid;
rock.uid = uid;
jmap_calendarevent_to_ical(comp, event, &rock);
jmap_timezones_to_ical(ical, &rock);
calevent_rock_free(&rock);
/* Handle 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(notWritten, uid, err);
r = 0; goto done;
}
}
if (update && calendarId) {
/* Check, if we need to move the event. */
dstmboxname = caldav_mboxname(req->userid, calendarId);
if (strcmp(mbox->name, dstmboxname)) {
/* Open destination mailbox for writing. */
r = mailbox_open_iwl(dstmboxname, &dstmbox);
if (r == IMAP_MAILBOX_NONEXISTENT) {
json_t *err = json_pack("{s:s}", "type", "calendarNotFound");
json_object_set_new(notWritten, uid, err);
r = 0; goto done;
} else if (r) {
syslog(LOG_ERR, "mailbox_open_iwl(%s) failed: %s",
dstmboxname, error_message(r));
goto done;
}
/* Check permissions. */
rights = httpd_myrights(req->authstate, dstmbox->acl);
if (!(rights & (DACL_WRITE))) {
json_t *err = json_pack("{s:s}", "type", "calendarNotFound");
json_object_set_new(notWritten, uid, err);
r = 0; goto done;
}
}
}
/* Handle scheduling. */
if (0) {
/* XXX This currently segfaults in Cassandane tests when trying to call
* sendmail. */
r = jmap_schedule_ical(req->userid, oldical, ical, mode);
if (r) goto done;
}
if (destroy || (update && 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);
if (destroy) {
/* Keep the VEVENT in the database but set alive to 0, to report
* with getCalendarEventUpdates. */
cdata->dav.alive = 0;
cdata->dav.modseq = record.modseq;
cdata->dav.imap_uid = record.uid;
r = caldav_write(db, cdata);
goto done;
} else {
/* Close the mailbox we moved the event from. */
mailbox_close(&mbox);
mbox = dstmbox;
dstmbox = NULL;
free(mboxname);
mboxname = dstmboxname;
dstmboxname = NULL;
}
}
if (!destroy) {
/* Store the updated VEVENT. */
struct transaction_t txn;
memset(&txn, 0, sizeof(struct transaction_t));
txn.req_hdrs = spool_new_hdrcache();
r = caldav_store_resource(&txn, ical, mbox, resource, db, 0);
spool_free_hdrcache(txn.req_hdrs);
buf_free(&txn.buf);
if (r && r != HTTP_CREATED && r != HTTP_NO_CONTENT) {
json_t *err = json_pack("{s:s}", "type", "unknownError");
json_object_set_new(notWritten, uid, err);
goto done;
}
r = 0;
}
done:
if (mbox) mailbox_close(&mbox);
if (mboxname) free(mboxname);
if (dstmbox) mailbox_close(&dstmbox);
if (dstmboxname) free(dstmboxname);
if (resource) free(resource);
if (ical) icalcomponent_free(ical);
if (oldical) icalcomponent_free(oldical);
json_decref(invalid);
return r;
}
static int setCalendarEvents(struct jmap_req *req)
{
struct caldav_db *db = NULL;
int r;
r = jmap_checkstate(req, MBTYPE_CALENDAR);
if (r) return 0;
json_t *set = json_pack("{s:s}", "accountId", req->userid);
r = jmap_setstate(req, set, "oldState", MBTYPE_CALENDAR, 0 /*refresh*/, 0 /*bump*/);
if (r) goto done;
r = caldav_create_defaultcalendars(req->userid);
if (r) goto done;
db = caldav_open_userid(req->userid);
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;
}
uid = xstrdup(makeuuid());
/* Create the calendar event. */
size_t error_count = json_object_size(notCreated);
r = jmap_write_calendarevent(arg, db, uid, JMAP_CREATE, notCreated, req);
if (r) {
free(uid);
goto done;
}
if (error_count != json_object_size(notCreated)) {
/* Bail out for any setErrors. */
free(uid);
continue;
}
/* Report calendar event as created. */
json_object_set_new(created, key, json_pack("{s:s}", "id", uid));
hash_insert(key, uid, req->idmap);
}
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. JMAP update does not allow creation uids here. */
if (!strlen(uid) || *uid == '#') {
json_t *err = json_pack("{s:s}", "type", "notFound");
json_object_set_new(notUpdated, uid, err);
continue;
}
/* Update the calendar event. */
size_t error_count = json_object_size(notUpdated);
r = jmap_write_calendarevent(arg, db, uid, JMAP_UPDATE, notUpdated, req);
if (r) goto done;
if (error_count != json_object_size(notUpdated)) {
/* Bail out for any setErrors. */
continue;
}
/* Report calendar event as updated. */
json_array_append_new(updated, json_string(uid));
}
if (json_array_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) {
size_t error_count;
/* Validate uid. JMAP destroy does not allow reference uids. */
const char *uid = json_string_value(juid);
if (!strlen(uid) || *uid == '#') {
json_t *err = json_pack("{s:s}", "type", "notFound");
json_object_set_new(notDestroyed, uid, err);
continue;
}
/* Destroy the calendar event. */
error_count = json_object_size(notDestroyed);
r = jmap_write_calendarevent(NULL, db, uid, JMAP_DESTROY, notDestroyed, req);
if (r) goto done;
if (error_count != json_object_size(notDestroyed)) {
/* Bail out for any setErrors. */
continue;
}
/* 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. */
r = jmap_setstate(req, set, "newState", MBTYPE_CALENDAR,
1 /*refresh*/,
json_object_get(set, "created") ||
json_object_get(set, "updated") ||
json_object_get(set, "destroyed"));
if (r) goto done;
json_incref(set);
json_t *item = json_pack("[]");
json_array_append_new(item, json_string("calendarsEventsSet"));
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);
json_decref(set);
return r;
}
static int getCalendarEventUpdates(struct jmap_req *req)
{
int r, pe;
json_t *invalid;
struct caldav_db *db;
const char *since;
modseq_t oldmodseq;
json_int_t maxChanges = 0;
int dofetch = 0;
struct updates_rock rock;
struct buf buf = BUF_INITIALIZER;
/* Initialize rock. */
memset(&rock, 0, sizeof(struct updates_rock));
db = caldav_open_userid(req->userid);
if (!db) {
syslog(LOG_ERR, "caldav_open_mailbox failed for user %s", req->userid);
r = IMAP_INTERNAL;
goto done;
}
/* Parse and validate arguments. */
invalid = json_pack("[]");
pe = jmap_readprop(req->args, "sinceState", 1 /*mandatory*/, invalid, "s", &since);
if (pe > 0) {
oldmodseq = str2uint64(since);
if (!oldmodseq) {
json_array_append_new(invalid, json_string("sinceState"));
}
}
pe = jmap_readprop(req->args, "maxChanges", 0 /*mandatory*/, invalid, "i", &maxChanges);
if (pe > 0) {
if (maxChanges <= 0) {
json_array_append_new(invalid, json_string("maxChanges"));
}
}
jmap_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 updates. */
rock.fetchmodseq = 1;
rock.changed = json_array();
rock.removed = json_array();
rock.max_records = maxChanges;
r = caldav_get_updates(db, oldmodseq, NULL /*mboxname*/, CAL_COMP_VEVENT,
maxChanges ? maxChanges + 1 : -1, &geteventupdates_cb, &rock);
mailbox_close(&rock.mailbox);
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 = req->counters.caldavmodseq;
}
/* Create response. */
json_t *eventUpdates = json_pack("{}");
json_object_set_new(eventUpdates, "accountId", json_string(req->userid));
json_object_set_new(eventUpdates, "oldState", json_string(since));
buf_printf(&buf, "%llu", 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("calendarEventUpdates"));
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_array_size(rock.changed)) {
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 (db) caldav_close(db);
return r;
}
enum calevent_filter_kind {
JMAP_CALFILTER_KIND_COND = 0,
JMAP_CALFILTER_KIND_OPER
};
enum calevent_filter_op {
JMAP_CALFILTER_OP_NONE = 0,
JMAP_CALFILTER_OP_AND,
JMAP_CALFILTER_OP_OR,
JMAP_CALFILTER_OP_NOT
};
typedef struct calevent_filter {
enum calevent_filter_kind kind;
enum calevent_filter_op op;
struct calevent_filter **conditions;
size_t n_conditions;
hash_table *calendars;
icaltimetype after;
icaltimetype before;
const char *text;
const char *summary;
const char *description;
const char *location;
const char *organizer;
const char *attendee;
} calevent_filter;
/* Match text with icalproperty kind in VEVENT comp and its recurrences. */
static int calevent_filter_matchprop(icalcomponent *comp,
const char *text,
icalproperty_kind kind) {
icalproperty *prop;
icalcomponent *ical;
if (icalcomponent_isa(comp) != ICAL_VEVENT_COMPONENT) {
return 0;
}
/* Look for text in comp. */
for (prop = icalcomponent_get_first_property(comp, kind);
prop;
prop = icalcomponent_get_next_property(comp, kind)) {
const char *val = icalproperty_get_value_as_string(prop);
/* XXX This isn't really according the JMAP spec, which requires phrase
* matching and some other magic. Besides, it's really not efficient.
* Check what other options we have to search text. */
if (val && stristr(val, text)) {
return 1;
}
}
ical = icalcomponent_get_parent(comp);
if (!ical || icalcomponent_isa(ical) != ICAL_VCALENDAR_COMPONENT) {
return 0;
}
/* Look for text in any recurrence of comp. */
for (comp = icalcomponent_get_first_component(ical, ICAL_VEVENT_COMPONENT);
comp;
comp = icalcomponent_get_next_component(ical, ICAL_VEVENT_COMPONENT)) {
if (!icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY)) {
continue;
}
for (prop = icalcomponent_get_first_property(comp, kind);
prop;
prop = icalcomponent_get_next_property(comp, kind)) {
const char *val = icalproperty_get_value_as_string(prop);
/* XXX string matching */
if (val && stristr(val, text)) {
return 1;
}
}
}
return 0;
}
/* Match the VEVENTs contained in VCALENDAR component ical against filter. */
static int calevent_filter_matches(calevent_filter *f,
struct caldav_data *cdata,
icalcomponent *ical)
{
if (f->kind == JMAP_CALFILTER_KIND_OPER) {
size_t i;
for (i = 0; i < f->n_conditions; i++) {
int m = calevent_filter_matches(f->conditions[i], cdata, ical);
if (m && f->op == JMAP_CALFILTER_OP_OR) {
return 1;
} else if (m && f->op == JMAP_CALFILTER_OP_NOT) {
return 0;
} else if (!m && f->op == JMAP_CALFILTER_OP_AND) {
return 0;
}
}
return f->op == JMAP_CALFILTER_OP_AND || f->op == JMAP_CALFILTER_OP_NOT;
} else {
/* Locate main VEVENT. */
/* XXX Might save comp and dtend, dtstart in the rock to avoid
* recalculating them. First wait for the decision if we want to
* optimize here or move filtering to the SQL layer. */
icalcomponent *comp;
for (comp = icalcomponent_get_first_component(ical, ICAL_VEVENT_COMPONENT);
comp;
comp = icalcomponent_get_next_component(ical, ICAL_VEVENT_COMPONENT)) {
if (!icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY)) {
break;
}
}
if (!comp) {
return 0;
}
/* calendars */
if (f->calendars && !hash_lookup(cdata->dav.mailbox, f->calendars)) {
return 0;
}
/* after */
if (!icaltime_is_null_time(f->after)) {
icaltimetype dtend = icaltime_from_string(cdata->dtend);
if (icaltime_compare(dtend, f->after) <= 0) {
return 0;
}
}
/* before */
if (!icaltime_is_null_time(f->before)) {
icaltimetype dtstart = icaltime_from_string(cdata->dtstart);
if (icaltime_compare(dtstart, f->before) >= 0) {
return 0;
}
}
/* text */
if (f->text) {
int m = calevent_filter_matchprop(comp, f->text, ICAL_SUMMARY_PROPERTY);
if (!m) calevent_filter_matchprop(comp, f->text, ICAL_DESCRIPTION_PROPERTY);
if (!m) calevent_filter_matchprop(comp, f->text, ICAL_LOCATION_PROPERTY);
if (!m) calevent_filter_matchprop(comp, f->text, ICAL_ORGANIZER_PROPERTY);
if (!m) calevent_filter_matchprop(comp, f->text, ICAL_ATTENDEE_PROPERTY);
if (!m) {
return 0;
}
}
if ((f->summary && !calevent_filter_matchprop(comp, f->summary, ICAL_SUMMARY_PROPERTY)) ||
(f->description && !calevent_filter_matchprop(comp, f->description, ICAL_DESCRIPTION_PROPERTY)) ||
(f->location && !calevent_filter_matchprop(comp, f->location, ICAL_LOCATION_PROPERTY)) ||
(f->organizer && !calevent_filter_matchprop(comp, f->organizer, ICAL_ORGANIZER_PROPERTY)) ||
(f->attendee && !calevent_filter_matchprop(comp, f->attendee, ICAL_ATTENDEE_PROPERTY))) {
return 0;
}
/* All matched. */
return 1;
}
}
/* Free the memory allocated by this calendar event filter. */
static void calevent_filter_free(calevent_filter **ff)
{
size_t i;
calevent_filter *f = *ff;
for (i = 0; i < f->n_conditions; i++) {
calevent_filter_free(&f->conditions[i]);
}
if (f->conditions) free(f->conditions);
if (f->calendars) {
free_hash_table(f->calendars, NULL);
free(f->calendars);
}
free(f);
*ff = NULL;
}
/* Parse the JMAP calendar event FilterOperator or FilterCondition in arg.
* Report any invalid properties in invalid, prefixed by prefix.
* Return NULL on error. */
static calevent_filter *calevent_filter_parse(json_t *arg,
const char *prefix,
json_t *invalid)
{
calevent_filter *f = (calevent_filter *) xzmalloc(sizeof(struct calevent_filter));
int pe;
const char *val;
struct buf buf = BUF_INITIALIZER;
int iscond = 1;
/* operator */
pe = jmap_readprop_full(arg, prefix, "operator", 0 /*mandatory*/, invalid, "s", &val);
if (pe > 0) {
f->kind = JMAP_CALFILTER_KIND_OPER;
if (!strncmp("AND", val, 3)) {
f->op = JMAP_CALFILTER_OP_AND;
} else if (!strncmp("OR", val, 2)) {
f->op = JMAP_CALFILTER_OP_OR;
} else if (!strncmp("NOT", val, 3)) {
f->op = JMAP_CALFILTER_OP_NOT;
} else {
buf_printf(&buf, "%s.%s", prefix, "operator");
json_array_append_new(invalid, json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
}
iscond = f->kind == JMAP_CALFILTER_KIND_COND;
/* conditions */
json_t *conds = json_object_get(arg, "conditions");
if (conds && !iscond && json_array_size(conds)) {
f->n_conditions = json_array_size(conds);
f->conditions = xmalloc(sizeof(struct calevent_filter*) * f->n_conditions);
size_t i;
for (i = 0; i < f->n_conditions; i++) {
json_t *cond = json_array_get(conds, i);
buf_printf(&buf, "%s.conditions[%zu]", prefix, i);
f->conditions[i] = calevent_filter_parse(cond, buf_cstring(&buf), invalid);
buf_reset(&buf);
}
} else if (conds && conds != json_null()) {
buf_printf(&buf, "%s.%s", prefix, "conditions");
json_array_append_new(invalid, json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
/* inCalendars */
json_t *cals = json_object_get(arg, "inCalendars");
if (cals && iscond && json_array_size(cals)) {
f->calendars = xmalloc(sizeof(hash_table));
construct_hash_table(f->calendars, json_array_size(cals), 0);
size_t i;
json_t *uid;
json_array_foreach(cals, i, uid) {
const char *id = json_string_value(uid);
if (id && strlen(id) && (*id != '#')) {
hash_insert(id, (void *)1, f->calendars);
} else {
buf_printf(&buf, "%s.calendars[%zu]", prefix, i);
json_array_append_new(invalid, json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
}
} else if (cals && cals != json_null()) {
buf_printf(&buf, "%s.%s", prefix, "inCalendars");
json_array_append_new(invalid, json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
/* after */
if (json_object_get(arg, "after") != json_null()) {
pe = jmap_readprop_full(arg, prefix, "after", 0 /*mandatory*/, invalid, "s", &val);
if (pe > 0) {
if (jmap_date_to_icaltime(val, &f->after, 0 /*isAllDay*/) || !iscond) {
buf_printf(&buf, "%s.%s", prefix, "after");
json_array_append_new(invalid, json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
}
}
/* before */
if (json_object_get(arg, "before") != json_null()) {
pe = jmap_readprop_full(arg, prefix, "before", 0 /*mandatory*/, invalid, "s", &val);
if (pe > 0) {
if (jmap_date_to_icaltime(val, &f->before, 0 /*isAllDay*/) || !iscond) {
buf_printf(&buf, "%s.%s", prefix, "before");
json_array_append_new(invalid, json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
}
}
/* text */
if (json_object_get(arg, "text") != json_null()) {
pe = jmap_readprop_full(arg, prefix, "text", 0 /*mandatory */, invalid, "s", &f->text);
if (pe > 0 && !iscond) {
buf_printf(&buf, "%s.%s", prefix, "text");
json_array_append_new(invalid, json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
}
/* summary */
if (json_object_get(arg, "summary") != json_null()) {
pe = jmap_readprop_full(arg, prefix, "summary", 0 /*mandatory */, invalid, "s", &f->summary);
if (pe > 0 && !iscond) {
buf_printf(&buf, "%s.%s", prefix, "summary");
json_array_append_new(invalid, json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
}
/* description */
if (json_object_get(arg, "description") != json_null()) {
pe = jmap_readprop_full(arg, prefix, "description", 0 /*mandatory */, invalid, "s", &f->description);
if (pe > 0 && !iscond) {
buf_printf(&buf, "%s.%s", prefix, "description");
json_array_append_new(invalid, json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
}
/* location */
if (json_object_get(arg, "location") != json_null()) {
pe = jmap_readprop_full(arg, prefix, "location", 0 /*mandatory */, invalid, "s", &f->location);
if (pe > 0 && !iscond) {
buf_printf(&buf, "%s.%s", prefix, "location");
json_array_append_new(invalid, json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
}
/* organizer */
if (json_object_get(arg, "organizer") != json_null()) {
pe = jmap_readprop_full(arg, prefix, "organizer", 0 /*mandatory */, invalid, "s", &f->organizer);
if (pe > 0 && !iscond) {
buf_printf(&buf, "%s.%s", prefix, "organizer");
json_array_append_new(invalid, json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
}
/* attendee */
if (json_object_get(arg, "attendee") != json_null()) {
pe = jmap_readprop_full(arg, prefix, "attendee", 0 /*mandatory */, invalid, "s", &f->attendee);
if (pe > 0 && !iscond) {
buf_printf(&buf, "%s.%s", prefix, "attendee");
json_array_append_new(invalid, json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
}
if (json_array_size(invalid)) {
calevent_filter_free(&f);
}
buf_free(&buf);
return f;
}
struct caleventlist_rock {
calevent_filter *filter;
size_t position;
size_t limit;
size_t total;
json_t *events;
struct mailbox *mailbox;
};
static int getcalendareventlist_cb(void *rock, struct caldav_data *cdata) {
struct caleventlist_rock *crock = (struct caleventlist_rock*) rock;
struct index_record record;
icalcomponent *ical = NULL;
int r = 0;
if (!cdata->dav.alive || !cdata->dav.rowid || !cdata->dav.imap_uid) {
return 0;
}
/* Open mailbox. */
if (!crock->mailbox || strcmp(crock->mailbox->name, cdata->dav.mailbox)) {
mailbox_close(&crock->mailbox);
r = mailbox_open_irl(cdata->dav.mailbox, &crock->mailbox);
if (r) goto done;
}
/* Load record. */
r = mailbox_find_index_record(crock->mailbox, cdata->dav.imap_uid, &record);
if (r) goto done;
/* Load VEVENT from record. */
ical = record_to_ical(crock->mailbox, &record);
if (!ical) {
syslog(LOG_ERR, "record_to_ical failed for record %u:%s",
cdata->dav.imap_uid, crock->mailbox->name);
r = IMAP_INTERNAL;
goto done;
}
/* Match the event against the filter and update statistics. */
if (crock->filter && !calevent_filter_matches(crock->filter, cdata, ical)) {
goto done;
}
crock->total++;
if (crock->position > crock->total) {
goto done;
}
if (crock->limit && crock->limit >= json_array_size(crock->events)) {
goto done;
}
/* All done. Add the event identifier. */
json_array_append_new(crock->events, json_string(cdata->ical_uid));
done:
if (ical) icalcomponent_free(ical);
return r;
}
static int getCalendarEventList(struct jmap_req *req)
{
int r = 0, pe;
json_t *invalid;
int dofetch = 0;
json_t *filter;
struct caleventlist_rock rock;
struct caldav_db *db;
memset(&rock, 0, sizeof(struct caleventlist_rock));
db = caldav_open_userid(req->userid);
if (!db) {
syslog(LOG_ERR, "caldav_open_mailbox failed for user %s", req->userid);
r = IMAP_INTERNAL;
goto done;
}
/* Parse and validate arguments. */
invalid = json_pack("[]");
/* filter */
filter = json_object_get(req->args, "filter");
if (filter && filter != json_null()) {
rock.filter = calevent_filter_parse(filter, "filter", invalid);
}
/* position */
json_int_t pos = 0;
if (json_object_get(req->args, "position") != json_null()) {
pe = jmap_readprop(req->args, "position", 0 /*mandatory*/, invalid, "i", &pos);
if (pe > 0 && pos < 0) {
json_array_append_new(invalid, json_string("position"));
}
}
rock.position = pos;
/* limit */
json_int_t limit = 0;
if (json_object_get(req->args, "limit") != json_null()) {
pe = jmap_readprop(req->args, "limit", 0 /*mandatory*/, invalid, "i", &limit);
if (pe > 0 && limit < 0) {
json_array_append_new(invalid, json_string("limit"));
}
}
rock.limit = limit;
/* fetchCalendarEvents */
if (json_object_get(req->args, "fetchCalendarEvents") != json_null()) {
jmap_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);
/* Inspect every entry in this accounts mailbox. */
rock.events = json_pack("[]");
r = caldav_foreach(db, NULL, getcalendareventlist_cb, &rock);
if (rock.mailbox) mailbox_close(&rock.mailbox);
if (r) goto done;
/* Prepare response. */
json_t *eventList = json_pack("{}");
json_object_set_new(eventList, "accountId", json_string(req->userid));
json_object_set_new(eventList, "state", json_string(req->state));
json_object_set_new(eventList, "position", json_integer(rock.position));
json_object_set_new(eventList, "total", json_integer(rock.total));
json_object_set(eventList, "calendarEventIds", rock.events);
json_t *item = json_pack("[]");
json_array_append_new(item, json_string("calendarEventList"));
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 && json_array_size(rock.events)) {
struct jmap_req subreq = *req;
subreq.args = json_pack("{}");
json_object_set(subreq.args, "ids", rock.events);
r = getCalendarEvents(&subreq);
json_decref(subreq.args);
}
done:
if (rock.filter) calevent_filter_free(&rock.filter);
if (rock.events) json_decref(rock.events);
if (db) caldav_close(db);
return r;
}
static int getCalendarPreferences(struct jmap_req *req)
{
/* XXX Just a dummy implementation to make the JMAP web client happy while
* testing. */
json_t *item = json_pack("[]");
json_array_append_new(item, json_string("calendarPreferences"));
json_array_append_new(item, json_pack("{}"));
json_array_append_new(item, json_string(req->tag));
json_array_append_new(req->response, item);
return 0;
}

File Metadata

Mime Type
text/x-c
Expires
Mon, Apr 6, 2:24 AM (1 w, 4 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18832028
Default Alt Text
http_jmap.c (250 KB)

Event Timeline