Page MenuHomePhorge

No OneTemporary

diff --git a/imap/jmap_ical.c b/imap/jmap_ical.c
index 148d01fc0..2a46bf9b2 100644
--- a/imap/jmap_ical.c
+++ b/imap/jmap_ical.c
@@ -1,5039 +1,5014 @@
/* jmap_ical.c --Routines to convert calendar events between JMAP and iCalendar
*
* Copyright (c) 1994-2016 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 "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_carddav.h"
#include "http_caldav_sched.h"
#include "http_dav.h"
#include "http_jmap.h"
#include "http_proxy.h"
#include "ical_support.h"
#include "icu_wrap.h"
#include "json_support.h"
#include "mailbox.h"
#include "mboxlist.h"
#include "mboxname.h"
#include "parseaddr.h"
#include "seen.h"
#include "statuscache.h"
#include "stristr.h"
#include "times.h"
#include "util.h"
#include "vcard_support.h"
#include "version.h"
#include "xmalloc.h"
#include "xsha1.h"
#include "xstrlcat.h"
#include "xstrlcpy.h"
#include "zoneinfo_db.h"
/* for sasl_encode64 */
#include <sasl/sasl.h>
#include <sasl/saslutil.h>
/* generated headers are not necessarily in current directory */
#include "imap/http_err.h"
#include "imap/imap_err.h"
#include "jmap_ical.h"
static int is_valid_jmapid(const char *s)
{
if (!s) return 0;
size_t i;
for (i = 0; s[i] && i < 256; i++) {
char c = s[i];
if (!((('0' <= c) && (c <= '9')) ||
(('a' <= c) && (c <= 'z')) ||
(('A' <= c) && (c <= 'Z')) ||
((c == '-' || c == '_')))) {
return 0;
}
}
return i > 0 && s[i] == '\0';
}
/* Forward declarations */
static json_t *calendarevent_from_ical(icalcomponent *comp, hash_table *props, icalcomponent *master);
static void calendarevent_to_ical(icalcomponent *comp, struct jmap_parser *parser, json_t *jsevent);
static char *sha1key(const char *val)
{
unsigned char dest[SHA1_DIGEST_LENGTH];
char idbuf[2*SHA1_DIGEST_LENGTH+1];
int r;
xsha1((const unsigned char *) val, strlen(val), dest);
r = bin_to_hex(dest, SHA1_DIGEST_LENGTH, idbuf, BH_LOWER);
assert(r == 2*SHA1_DIGEST_LENGTH);
idbuf[2*SHA1_DIGEST_LENGTH] = '\0';
return xstrdup(idbuf);
}
static char *mailaddr_from_uri(const char *uri)
{
if (!uri || strncasecmp(uri, "mailto:", 7)) {
return NULL;
}
uri += 7;
const char *p = strchr(uri, '?');
if (!p) return address_canonicalise(uri);
char *tmp = xstrndup(uri, p - uri);
char *ret = address_canonicalise(uri);
free(tmp);
return ret;
}
static char *normalized_uri(const char *uri)
{
const char *col = strchr(uri, ':');
if (!col) return xstrdupnull(uri);
struct buf buf = BUF_INITIALIZER;
buf_setmap(&buf, uri, col - uri);
buf_lcase(&buf);
buf_appendcstr(&buf, col);
return buf_release(&buf);
}
static char *mailaddr_to_uri(const char *addr)
{
struct buf buf = BUF_INITIALIZER;
buf_setcstr(&buf, "mailto:");
buf_appendcstr(&buf, addr);
return buf_release(&buf);
}
static void remove_icalxparam(icalproperty *prop, const char *name)
{
icalparameter *param, *next;
for (param = icalproperty_get_first_parameter(prop, ICAL_ANY_PARAMETER);
param;
param = next) {
next = icalproperty_get_next_parameter(prop, ICAL_ANY_PARAMETER);
if (strcasecmpsafe(icalparameter_get_xname(param), name)) {
continue;
}
icalproperty_remove_parameter_by_ref(prop, param);
}
}
static const char*
get_icalxparam_value(icalproperty *prop, const char *name)
{
icalparameter *param;
for (param = icalproperty_get_first_parameter(prop, ICAL_ANY_PARAMETER);
param;
param = icalproperty_get_next_parameter(prop, ICAL_ANY_PARAMETER)) {
if (strcasecmpsafe(icalparameter_get_xname(param), name)) {
continue;
}
return icalparameter_get_xvalue(param);
}
return NULL;
}
static void
set_icalxparam(icalproperty *prop, const char *name, const char *val, int purge)
{
icalparameter *param;
if (purge) remove_icalxparam(prop, name);
param = icalparameter_new(ICAL_X_PARAMETER);
icalparameter_set_xname(param, name);
icalparameter_set_xvalue(param, val);
icalproperty_add_parameter(prop, param);
}
/* Compare the value of the first occurences of property kind in components
* a and b. Return 0 if they match or if both do not contain kind. Note that
* this function does not define an order on property values, so it can't be
* used for sorting. */
int compare_icalprop(icalcomponent *a, icalcomponent *b,
icalproperty_kind kind) {
icalproperty *pa, *pb;
icalvalue *va, *vb;
pa = icalcomponent_get_first_property(a, kind);
pb = icalcomponent_get_first_property(b, kind);
if (!pa && !pb) {
return 0;
}
va = icalproperty_get_value(pa);
vb = icalproperty_get_value(pb);
enum icalparameter_xliccomparetype cmp = icalvalue_compare(va, vb);
return cmp != ICAL_XLICCOMPARETYPE_EQUAL;
}
static const char*
get_icalxprop_value(icalcomponent *comp, const char *name)
{
icalproperty *prop;
for (prop = icalcomponent_get_first_property(comp, ICAL_X_PROPERTY);
prop;
prop = icalcomponent_get_next_property(comp, ICAL_X_PROPERTY)) {
if (strcasecmp(icalproperty_get_x_name(prop), name)) {
continue;
}
return icalproperty_get_value_as_string(prop);
}
return NULL;
}
/* Remove and deallocate any x-properties with name in comp. */
static void remove_icalxprop(icalcomponent *comp, const char *name)
{
icalproperty *prop, *next;
icalproperty_kind kind = ICAL_X_PROPERTY;
for (prop = icalcomponent_get_first_property(comp, kind);
prop;
prop = next) {
next = icalcomponent_get_next_property(comp, kind);
if (strcasecmp(icalproperty_get_x_name(prop), name))
continue;
icalcomponent_remove_property(comp, prop);
icalproperty_free(prop);
}
}
static char *xjmapid_from_ical(icalproperty *prop)
{
const char *id = (char *) get_icalxparam_value(prop, JMAPICAL_XPARAM_ID);
if (id) return xstrdup(id);
return sha1key(icalproperty_as_ical_string(prop));
}
static void xjmapid_to_ical(icalproperty *prop, const char *id)
{
struct buf buf = BUF_INITIALIZER;
icalparameter *param;
buf_setcstr(&buf, JMAPICAL_XPARAM_ID);
buf_appendcstr(&buf, "=");
buf_appendcstr(&buf, id);
param = icalparameter_new_from_string(buf_cstring(&buf));
icalproperty_add_parameter(prop, param);
buf_free(&buf);
}
-struct datetime {
- int year;
- int month; // Jan=1
- int day;
- int hour;
- int minute;
- int second;
- bit64 nano;
-};
-
-#define JMAP_DATETIME_INITIALIZER { 0, 0, 0, 0, 0, 0, 0 };
-
-static int datetime_has_zero_time(const struct datetime *dt)
+HIDDEN int jmapical_datetime_has_zero_time(const struct jmapical_datetime *dt)
{
return dt->hour == 0 && dt->minute == 0 && dt->second == 0 && dt->nano == 0;
}
-static struct icaltimetype datetime_to_icaldate(const struct datetime *dt)
+HIDDEN struct icaltimetype jmapical_datetime_to_icaldate(const struct jmapical_datetime *dt)
{
struct icaltimetype icaldt = icaltime_null_time();
icaldt.year = dt->year;
icaldt.month = dt->month;
icaldt.day = dt->day;
icaldt.hour = dt->hour;
icaldt.minute = dt->minute;
icaldt.second = dt->second;
icaldt.is_date = 1;
return icaldt;
}
-static icaltimetype datetime_to_icaltime(const struct datetime *dt,
- const icaltimezone* zone)
+HIDDEN icaltimetype jmapical_datetime_to_icaltime(const struct jmapical_datetime *dt,
+ const icaltimezone* zone)
{
struct icaltimetype icaldt = icaltime_null_time();
icaldt.year = dt->year;
icaldt.month = dt->month;
icaldt.day = dt->day;
icaldt.hour = dt->hour;
icaldt.minute = dt->minute;
icaldt.second = dt->second;
icaldt.is_date = 0;
icaldt.zone = zone;
return icaldt;
}
-static void datetime_from_icaltime(icaltimetype icaldt, struct datetime *dt)
+HIDDEN void jmapical_datetime_from_icaltime(icaltimetype icaldt, struct jmapical_datetime *dt)
{
- memset(dt, 0, sizeof(struct datetime));
+ memset(dt, 0, sizeof(struct jmapical_datetime));
dt->year = icaldt.year;
dt->month = icaldt.month;
dt->day = icaldt.day;
dt->hour = icaldt.hour;
dt->minute = icaldt.minute;
dt->second = icaldt.second;
}
-static int compare_datetime(const struct datetime *a,
- const struct datetime *b)
+HIDDEN int jmapical_datetime_compare(const struct jmapical_datetime *a,
+ const struct jmapical_datetime *b)
{
if (a->year != b->year)
return a->year > b->year ? 1 : -1;
if (a->month != b->month)
return a->month > b->month ? 1 : -1;
if (a->day != b->day)
return a->day > b->day ? 1 : -1;
if (a->hour != b->hour)
return a->hour > b->hour ? 1 : -1;
if (a->minute != b->minute)
return a->minute > b->minute ? 1 : -1;
if (a->second != b->second)
return a->second > b->second ? 1 : -1;
if (a->nano != b->nano)
return a->nano > b->nano ? 1 : -1;
return 0;
}
-static void format_datetime(const struct datetime *dt, struct buf *dst)
+static void format_datetime(const struct jmapical_datetime *dt, struct buf *dst)
{
buf_reset(dst);
buf_printf(dst, "%04d-%02d-%02dT%02d:%02d:%02d",
dt->year, dt->month, dt->day, dt->hour, dt->minute, dt->second);
if (dt->nano) {
buf_printf(dst, ".%.9llu", dt->nano);
int n = buf_len(dst);
const char *b = buf_base(dst);
while (b[n-1] == '0') n--;
buf_truncate(dst, n);
}
buf_cstring(dst);
}
-static void format_localdatetime(const struct datetime *dt, struct buf *dst)
+HIDDEN void jmapical_localdatetime_as_string(const struct jmapical_datetime *dt, struct buf *dst)
{
format_datetime(dt, dst);
buf_cstring(dst);
}
-static void format_utcdatetime(const struct datetime *dt, struct buf *dst)
+HIDDEN void jmapical_utcdatetime_as_string(const struct jmapical_datetime *dt, struct buf *dst)
{
format_datetime(dt, dst);
buf_putc(dst, 'Z');
buf_cstring(dst);
}
static const char *parse_fracsec(const char *val, bit64 *nanoptr)
{
const char *end = NULL;
bit64 nano = 0;
if (parsenum(val, &end, 9, &nano) >= 0) {
/* Normalize to nanoseconds */
ssize_t i, n = end - val;
for (i = 0; i < 9 - n; i++) {
nano *= 10;
}
/* Skip remaining fractional seconds */
while (isdigit(*end)) end++;
/* No trailing zeros allowed */
if (end[-1] == '0') {
return NULL;
}
*nanoptr = nano;
return end;
}
else return NULL;
}
-static const char *parse_datetime(const char *val, struct datetime *dt)
+static const char *parse_datetime(const char *val, struct jmapical_datetime *dt)
{
struct tm tm;
memset(&tm, 0, sizeof(struct tm));
tm.tm_isdst = -1;
const char *p = strptime(val, "%Y-%m-%dT%H:%M:%S", &tm);
if (!p) return NULL;
- memset(dt, 0, sizeof(struct datetime));
+ memset(dt, 0, sizeof(struct jmapical_datetime));
dt->year = tm.tm_year + 1900;
dt->month = tm.tm_mon + 1;
dt->day = tm.tm_mday;
dt->hour = tm.tm_hour;
dt->minute = tm.tm_min;
dt->second = tm.tm_sec;
if (*p == '.') p = parse_fracsec(p+1, &dt->nano);
return p;
}
-static int parse_localdatetime(const char *val, struct datetime *dt)
+HIDDEN int jmapical_localdatetime_from_string(const char *val, struct jmapical_datetime *dt)
{
const char *p = parse_datetime(val, dt);
return (!p || p[0] != '\0') ? -1 : 0;
}
-static int parse_utcdatetime(const char *val, struct datetime *dt)
+HIDDEN int jmapical_utcdatetime_from_string(const char *val, struct jmapical_datetime *dt)
{
const char *p = parse_datetime(val, dt);
return (!p || p[0] != 'Z' || p[1] != '\0') ? -1 : 0;
}
static void subseconds_from_icalprop(icalproperty *prop, bit64 *nanoptr)
{
*nanoptr = 0;
const char *subsecs = get_icalxparam_value(prop, "SUBSECOND");
if (subsecs && *subsecs == '0') {
/* Parse subseconds, ignoring invalid values */
while (*subsecs == '0') subsecs++;
if (*subsecs == '.') {
const char *end = parse_fracsec(subsecs+1, nanoptr);
if (!end || end[0] != '\0') *nanoptr = 0;
}
}
}
-static int datetime_from_icalprop(icalproperty *prop, struct datetime *dt)
+HIDDEN int jmapical_datetime_from_icalprop(icalproperty *prop, struct jmapical_datetime *dt)
{
icaltimetype icaldt = icalvalue_get_datetimedate(icalproperty_get_value(prop));
if (!icaltime_is_valid_time(icaldt)) return -1;
- datetime_from_icaltime(icaldt, dt);
+ jmapical_datetime_from_icaltime(icaldt, dt);
subseconds_from_icalprop(prop, &dt->nano);
return 0;
}
-struct duration {
- int is_neg;
- unsigned int days;
- unsigned int weeks;
- unsigned int hours;
- unsigned int minutes;
- unsigned int seconds;
- bit64 nanos;
-};
-
-#define JMAP_DURATION_INITIALIZER { 0, 0, 0, 0, 0, 0, 0 }
-
-static int duration_has_zero_time(const struct duration *dur)
+HIDDEN int jmapical_duration_has_zero_time(const struct jmapical_duration *dur)
{
return dur->hours == 0 && dur->minutes == 0 &&
dur->seconds == 0 && dur->nanos == 0;
}
-
-static struct icaldurationtype duration_to_icalduration(const struct duration *dur)
+static struct icaldurationtype duration_to_icalduration(const struct jmapical_duration *dur)
{
struct icaldurationtype icaldur = icaldurationtype_null_duration();
icaldur.is_neg = dur->is_neg;
icaldur.days = dur->days;
icaldur.weeks = dur->weeks;
icaldur.hours = dur->hours;
icaldur.minutes = dur->minutes;
icaldur.seconds = dur->seconds;
return icaldur;
}
-static void duration_from_icalduration(struct icaldurationtype icaldur, struct duration *dur)
+HIDDEN void jmapical_duration_from_icalduration(struct icaldurationtype icaldur,
+ struct jmapical_duration *dur)
{
- memset(dur, 0, sizeof(struct duration));
+ memset(dur, 0, sizeof(struct jmapical_duration));
dur->is_neg = icaldur.is_neg;
dur->days = icaldur.days;
dur->weeks = icaldur.weeks;
dur->hours = icaldur.hours;
dur->minutes = icaldur.minutes;
dur->seconds = icaldur.seconds;
}
-static void duration_between(time_t t1, bit64 t1nanos, time_t t2, bit64 t2nanos, struct duration *dur)
+HIDDEN void jmapical_duration_between(time_t t1, bit64 t1nanos,
+ time_t t2, bit64 t2nanos,
+ struct jmapical_duration *dur)
{
const icaltimezone *utc = icaltimezone_get_utc_timezone();
int is_neg = t1 > t2 || (t1 == t2 && t1nanos > t2nanos);
bit64 nanos = 0;
time_t tx = is_neg ? t2 : t1;
bit64 txnanos = is_neg ? t2nanos : t1nanos;
time_t ty = is_neg ? t1 : t2;
bit64 tynanos = is_neg ? t1nanos : t2nanos;
if (txnanos < tynanos) {
nanos = tynanos - txnanos;
}
else if (txnanos > tynanos) {
nanos = (1000000000 - txnanos) + tynanos;
if (tx != ty) ty -= 1;
}
icaltimetype icaltx = icaltime_from_timet_with_zone(tx, 0, utc);
icaltimetype icalty = icaltime_from_timet_with_zone(ty, 0, utc);
struct icaldurationtype icaldur = icaltime_subtract(icalty, icaltx);
icaldur.is_neg = is_neg;
- duration_from_icalduration(icaldur, dur);
+ jmapical_duration_from_icalduration(icaldur, dur);
dur->nanos = nanos;
}
-static int parse_duration(const char *val, struct duration *dur)
+HIDDEN int jmapical_duration_from_string(const char *val, struct jmapical_duration *dur)
{
bit64 nanos = 0;
char *myval = NULL;
const char *fracsec = strchr(val, '.');
if (fracsec) {
// Parse fractional seconds.
const char *p = parse_fracsec(fracsec + 1, &nanos);
if (!p || p[0] != 'S' || p[1] != '\0') return -1;
// Truncate to iCalendar duration.
myval = xstrdup(val);
myval[fracsec-val] = 'S';
myval[fracsec-val+1] = '\0';
val = myval;
}
// Parse iCalendar duration.
struct icaldurationtype icaldur = icaldurationtype_from_string(val);
free(myval);
if (icaldurationtype_is_bad_duration(icaldur)) return -1;
- duration_from_icalduration(icaldur, dur);
+ jmapical_duration_from_icalduration(icaldur, dur);
dur->nanos = nanos;
return 0;
}
-static void format_duration(const struct duration *dur, struct buf *buf)
+HIDDEN void jmapical_duration_as_string(const struct jmapical_duration *dur, struct buf *buf)
{
struct icaldurationtype icaldur = duration_to_icalduration(dur);
char *tmp = icaldurationtype_as_ical_string_r(icaldur);
buf_setcstr(buf, tmp);
if (dur->nanos) {
const char *b = buf_base(buf);
int n = buf_len(buf);
/* Append fracsec part */
if (b[n-1] == 'S') {
buf_truncate(buf, n-1);
}
else {
buf_putc(buf, '0');
}
buf_printf(buf, ".%.9llu", dur->nanos);
/* Truncate trailing zeros */
b = buf_base(buf);
n = buf_len(buf);
while (b[n-1] == '0') n--;
buf_truncate(buf, n);
buf_putc(buf, 'S');
}
free(tmp);
buf_cstring(buf);
}
static icaltimezone *tz_from_tzid(const char *tzid)
{
icaltimezone *tz = NULL;
if (!tzid)
return NULL;
/* libical doesn't return the UTC singleton for Etc/UTC */
if (!strcmp(tzid, "Etc/UTC") || !strcmp(tzid, "UTC"))
return icaltimezone_get_utc_timezone();
tz = icaltimezone_get_builtin_timezone(tzid);
if (!tz) {
/* see if its a MS Windows TZID */
char *my_tzid = icu_getIDForWindowsID(tzid);
if (!my_tzid) return NULL;
tz = icaltimezone_get_builtin_timezone(my_tzid);
free(my_tzid);
}
return tz;
}
/* Determine the Olson TZID, if any, of the ical property prop. */
static const char *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 = tz_from_tzid(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 && tz_from_tzid(tzid) ? tzid : NULL;
} else if (tz) return icaltimezone_get_tzid(tz);
} else {
icalvalue *val = icalproperty_get_value(prop);
icaltimetype dt = icalvalue_get_datetime(val);
if (icaltime_is_valid_time(dt) && icaltime_is_utc(dt)) {
tzid = "Etc/UTC";
}
}
return tzid;
}
/* Determine the Olson TZID, if any, of the first ical property of
* kind in component comp. */
static const char *tzid_from_ical(icalcomponent *comp,
icalproperty_kind kind) {
icalproperty *prop = icalcomponent_get_first_property(comp, kind);
if (!prop) {
return NULL;
}
return tzid_from_icalprop(prop, 1/*guess*/);
}
static struct icaltimetype dtstart_from_ical(icalcomponent *comp)
{
struct icaltimetype dt;
const char *tzid;
dt = icalcomponent_get_dtstart(comp);
if (dt.zone) return dt;
if ((tzid = tzid_from_ical(comp, ICAL_DTSTART_PROPERTY))) {
dt.zone = tz_from_tzid(tzid);
}
else if ((tzid = tzid_from_ical(comp, ICAL_DTEND_PROPERTY))) {
/* Seen in the wild: a floating DTSTART and a DTEND with TZID */
dt.zone = tz_from_tzid(tzid);
}
return dt;
}
static struct icaltimetype dtend_from_ical(icalcomponent *comp)
{
struct icaltimetype dtend;
icalproperty *prop;
struct icaltimetype dtstart = dtstart_from_ical(comp);
if ((prop = icalcomponent_get_first_property(comp, ICAL_DTEND_PROPERTY))) {
dtend = icalproperty_get_dtend(prop);
if (!dtend.zone) {
const char *tzid = tzid_from_icalprop(prop, 1);
dtend.zone = tz_from_tzid(tzid);
}
}
else dtend = icalcomponent_get_dtend(comp);
/* Normalize floating DTEND to DTSTART time zone, if any */
if (!dtend.zone) dtend.zone = dtstart.zone;
return dtend;
}
/* Compare int in ascending order. */
static int compare_int(const void *aa, const void *bb)
{
const int *a = aa, *b = bb;
return (*a < *b) ? -1 : (*a > *b);
}
/* Return the identity of i. This is a helper for recur_byX. */
static int identity_int(int i) {
return i;
}
/*
* Conversion from iCalendar to JMAP
*/
static json_t* relatedto_from_ical(icalcomponent*);
/* 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* recurrence_byX_fromical(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), compare_int);
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 recurrenceRule */
static json_t*
recurrence_from_ical(icalcomponent *comp)
{
char *s = NULL;
size_t i;
struct buf buf = BUF_INITIALIZER;
icalproperty *prop;
struct icalrecurrencetype rrule;
prop = icalcomponent_get_first_property(comp, ICAL_RRULE_PROPERTY);
if (!prop) {
return json_null();
}
rrule = icalproperty_get_rrule(prop);
if (rrule.freq == ICAL_NO_RECURRENCE) {
return json_null();
}
json_t *recur = json_pack("{s:s}", "@type", "RecurrenceRule");
/* frequency */
s = lcase(xstrdup(icalrecur_freq_to_string(rrule.freq)));
json_object_set_new(recur, "frequency", json_string(s));
free(s);
json_object_set_new(recur, "interval", json_pack("i", rrule.interval));
#ifdef HAVE_RSCALE
/* rscale */
if (rrule.rscale) {
s = xstrdup(rrule.rscale);
s = lcase(s);
json_object_set_new(recur, "rscale", json_string(s));
free(s);
} else json_object_set_new(recur, "rscale", json_string("gregorian"));
/* skip */
const char *skip = NULL;
switch (rrule.skip) {
case ICAL_SKIP_BACKWARD:
skip = "backward";
break;
case ICAL_SKIP_FORWARD:
skip = "forward";
break;
case ICAL_SKIP_OMIT:
/* fall through */
default:
skip = "omit";
}
json_object_set_new(recur, "skip", json_string(skip));
#endif
/* firstDayOfWeek */
s = xstrdup(icalrecur_weekday_to_string(rrule.week_start));
s = lcase(s);
json_object_set_new(recur, "firstDayOfWeek", json_string(s));
free(s);
/* byDay */
json_t *jbd = json_pack("[]");
for (i = 0; i < ICAL_BY_DAY_SIZE; i++) {
json_t *jday;
icalrecurrencetype_weekday weekday;
int pos;
if (rrule.by_day[i] == ICAL_RECURRENCE_ARRAY_MAX) {
break;
}
jday = json_pack("{}");
weekday = icalrecurrencetype_day_day_of_week(rrule.by_day[i]);
s = xstrdup(icalrecur_weekday_to_string(weekday));
s = lcase(s);
json_object_set_new(jday, "day", json_string(s));
free(s);
pos = icalrecurrencetype_day_position(rrule.by_day[i]);
if (pos) {
json_object_set_new(jday, "nthOfPeriod", json_integer(pos));
}
if (json_object_size(jday)) {
json_array_append_new(jbd, jday);
} else {
json_decref(jday);
}
}
if (json_array_size(jbd)) {
json_object_set_new(recur, "byDay", jbd);
} else {
json_decref(jbd);
}
/* byMonth */
json_t *jbm = json_pack("[]");
for (i = 0; i < ICAL_BY_MONTH_SIZE; i++) {
short bymonth;
if (rrule.by_month[i] == ICAL_RECURRENCE_ARRAY_MAX) {
break;
}
bymonth = rrule.by_month[i];
buf_printf(&buf, "%d", icalrecurrencetype_month_month(bymonth));
if (icalrecurrencetype_month_is_leap(bymonth)) {
buf_appendcstr(&buf, "L");
}
json_array_append_new(jbm, json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
if (json_array_size(jbm)) {
json_object_set_new(recur, "byMonth", jbm);
} else {
json_decref(jbm);
}
if (rrule.by_month_day[0] != ICAL_RECURRENCE_ARRAY_MAX) {
json_object_set_new(recur, "byMonthDay",
recurrence_byX_fromical(rrule.by_month_day,
ICAL_BY_MONTHDAY_SIZE, &identity_int));
}
if (rrule.by_year_day[0] != ICAL_RECURRENCE_ARRAY_MAX) {
json_object_set_new(recur, "byYearDay",
recurrence_byX_fromical(rrule.by_year_day,
ICAL_BY_YEARDAY_SIZE, &identity_int));
}
if (rrule.by_week_no[0] != ICAL_RECURRENCE_ARRAY_MAX) {
json_object_set_new(recur, "byWeekNo",
recurrence_byX_fromical(rrule.by_week_no,
ICAL_BY_WEEKNO_SIZE, &identity_int));
}
if (rrule.by_hour[0] != ICAL_RECURRENCE_ARRAY_MAX) {
json_object_set_new(recur, "byHour",
recurrence_byX_fromical(rrule.by_hour,
ICAL_BY_HOUR_SIZE, &identity_int));
}
if (rrule.by_minute[0] != ICAL_RECURRENCE_ARRAY_MAX) {
json_object_set_new(recur, "byMinute",
recurrence_byX_fromical(rrule.by_minute,
ICAL_BY_MINUTE_SIZE, &identity_int));
}
if (rrule.by_second[0] != ICAL_RECURRENCE_ARRAY_MAX) {
json_object_set_new(recur, "bySecond",
recurrence_byX_fromical(rrule.by_second,
ICAL_BY_SECOND_SIZE, &identity_int));
}
if (rrule.by_set_pos[0] != ICAL_RECURRENCE_ARRAY_MAX) {
json_object_set_new(recur, "bySetPosition",
recurrence_byX_fromical(rrule.by_set_pos,
ICAL_BY_SETPOS_SIZE, &identity_int));
}
if (rrule.count != 0) {
/* Recur count takes precedence over until. */
json_object_set_new(recur, "count", json_integer(rrule.count));
} else if (!icaltime_is_null_time(rrule.until)) {
/* Convert iCalendar UNTIL to start timezone */
const char *tzid = NULL;
icalproperty *dtstart_prop =
icalcomponent_get_first_property(comp, ICAL_DTSTART_PROPERTY);
if (dtstart_prop) {
icalparameter *tzid_param =
icalproperty_get_first_parameter(dtstart_prop, ICAL_TZID_PARAMETER);
if (tzid_param) tzid = icalparameter_get_tzid(tzid_param);
}
icaltimezone *tz = tz_from_tzid(tzid);
icaltimetype dtuntil;
if (rrule.until.is_date) {
dtuntil = rrule.until;
dtuntil.hour = 23;
dtuntil.minute = 59;
dtuntil.second = 59;
dtuntil.is_date = 0;
}
else {
dtuntil = icaltime_convert_to_zone(rrule.until, tz);
}
- struct datetime until = JMAP_DATETIME_INITIALIZER;
- datetime_from_icaltime(dtuntil, &until);
+ struct jmapical_datetime until = JMAPICAL_DATETIME_INITIALIZER;
+ jmapical_datetime_from_icaltime(dtuntil, &until);
struct buf buf = BUF_INITIALIZER;
- format_localdatetime(&until, &buf);
+ jmapical_localdatetime_as_string(&until, &buf);
json_object_set_new(recur, "until", json_string(buf_cstring(&buf)));
buf_free(&buf);
}
if (!json_object_size(recur)) {
json_decref(recur);
recur = json_null();
}
buf_free(&buf);
return recur;
}
static json_t*
override_rdate_from_ical(icalproperty *prop)
{
/* returns a JSON object with a single key value pair */
json_t *override = json_pack("{}");
json_t *o = json_pack("{}");
struct icaldatetimeperiodtype rdate = icalproperty_get_rdate(prop);
struct buf buf = BUF_INITIALIZER;
- struct datetime rdatedt = JMAP_DATETIME_INITIALIZER;
+ struct jmapical_datetime rdatedt = JMAPICAL_DATETIME_INITIALIZER;
if (!icaltime_is_null_time(rdate.time)) {
- datetime_from_icaltime(rdate.time, &rdatedt);
+ jmapical_datetime_from_icaltime(rdate.time, &rdatedt);
} else {
/* PERIOD */
- datetime_from_icaltime(rdate.period.start, &rdatedt);
+ jmapical_datetime_from_icaltime(rdate.period.start, &rdatedt);
/* Determine duration */
struct icaldurationtype icaldur;
if (!icaltime_is_null_time(rdate.period.end)) {
icaldur = icaltime_subtract(rdate.period.end, rdate.period.start);
} else {
icaldur = rdate.period.duration;
}
- struct duration dur = JMAP_DURATION_INITIALIZER;
- duration_from_icalduration(icaldur, &dur);
- format_duration(&dur, &buf);
+ struct jmapical_duration dur = JMAPICAL_DURATION_INITIALIZER;
+ jmapical_duration_from_icalduration(icaldur, &dur);
+ jmapical_duration_as_string(&dur, &buf);
json_object_set_new(o, "duration", json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
if (!icaltime_is_null_time(rdate.time) ||
!icaltime_is_null_time(rdate.period.start)) {
subseconds_from_icalprop(prop, &rdatedt.nano);
- format_localdatetime(&rdatedt, &buf);
+ jmapical_localdatetime_as_string(&rdatedt, &buf);
json_object_set_new(override, buf_cstring(&buf), o);
buf_reset(&buf);
}
if (!json_object_size(override)) {
json_decref(override);
json_decref(o);
override = NULL;
}
buf_free(&buf);
return override;
}
static json_t*
override_exdate_from_ical(icalproperty *prop, const char *tzid_start)
{
json_t *override = json_pack("{}");
icaltimetype exdate = icalproperty_get_exdate(prop);
const char *tzid_xdate = tzid_from_icalprop(prop, 1);
if (tzid_start && tzid_xdate && strcmp(tzid_start, tzid_xdate)) {
icaltimezone *tz_xdate = tz_from_tzid(tzid_xdate);
icaltimezone *tz_start = tz_from_tzid(tzid_start);
if (tz_xdate && tz_start) {
if (exdate.zone) exdate.zone = tz_xdate;
exdate = icaltime_convert_to_zone(exdate, tz_start);
}
}
if (!icaltime_is_null_time(exdate)) {
- struct datetime exdatedt = JMAP_DATETIME_INITIALIZER;
- datetime_from_icaltime(exdate, &exdatedt);
+ struct jmapical_datetime exdatedt = JMAPICAL_DATETIME_INITIALIZER;
+ jmapical_datetime_from_icaltime(exdate, &exdatedt);
subseconds_from_icalprop(prop, &exdatedt.nano);
struct buf buf = BUF_INITIALIZER;
- format_localdatetime(&exdatedt, &buf);
+ jmapical_localdatetime_as_string(&exdatedt, &buf);
json_object_set_new(override, buf_cstring(&buf), json_pack("{s:b}", "excluded", 1));
buf_free(&buf);
}
if (!json_object_size(override)) {
json_decref(override);
override = NULL;
}
return override;
}
static json_t*
overrides_from_ical(icalcomponent *comp, json_t *event, const char *tzid_start)
{
icalproperty *prop;
json_t *overrides = json_pack("{}");
const char *uid = icalcomponent_get_uid(comp);
/* RDATE */
for (prop = icalcomponent_get_first_property(comp, ICAL_RDATE_PROPERTY);
prop;
prop = icalcomponent_get_next_property(comp, ICAL_RDATE_PROPERTY)) {
json_t *override = override_rdate_from_ical(prop);
if (override) {
json_object_update(overrides, override);
json_decref(override);
}
}
/* EXDATE */
for (prop = icalcomponent_get_first_property(comp, ICAL_EXDATE_PROPERTY);
prop;
prop = icalcomponent_get_next_property(comp, ICAL_EXDATE_PROPERTY)) {
json_t *override = override_exdate_from_ical(prop, tzid_start);
if (override) {
json_object_update(overrides, override);
json_decref(override);
}
}
/* VEVENT exceptions */
json_t *exceptions = json_pack("{}");
icalcomponent *excomp, *ical;
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)) {
if (excomp == comp) continue; /* skip toplevel promoted object */
/* Skip unrelated VEVENTs */
const char *exuid = icalcomponent_get_uid(excomp);
if (strcmpsafe(exuid, uid)) continue;
/* Convert VEVENT exception to JMAP */
json_t *ex = calendarevent_from_ical(excomp, NULL, comp);
if (!ex) continue;
/* Recurrence-id */
/* Convert the recurrence-id into the timezone of the main event.
* Some clients generate the recurrence id as UTC date time,
* even if the main VEVENT has a DTSTART with a TZID */
icaltimetype icalrecurid = icalcomponent_get_recurrenceid(excomp);
if ((prop = icalcomponent_get_first_property(comp, ICAL_DTSTART_PROPERTY))) {
icalrecurid.is_date = icalproperty_get_dtstart(prop).is_date;
if (!icalrecurid.is_date) {
icalparameter *tzid_param =
icalproperty_get_first_parameter(prop, ICAL_TZID_PARAMETER);
if (tzid_param) {
const char *start_tzid = icalparameter_get_tzid(tzid_param);
if (start_tzid) {
icaltimezone *start_tz = tz_from_tzid(start_tzid);
if (start_tz) {
icalrecurid = icaltime_convert_to_zone(icalrecurid, start_tz);
}
}
}
}
}
/* Format recurrence id */
- struct datetime exrecurdt = JMAP_DATETIME_INITIALIZER;
- datetime_from_icaltime(icalrecurid, &exrecurdt);
+ struct jmapical_datetime exrecurdt = JMAPICAL_DATETIME_INITIALIZER;
+ jmapical_datetime_from_icaltime(icalrecurid, &exrecurdt);
// Set subseconds
prop = icalcomponent_get_first_property(excomp, ICAL_RECURRENCEID_PROPERTY);
if (prop) subseconds_from_icalprop(prop, &exrecurdt.nano);
if (!exrecurdt.nano) {
prop = icalcomponent_get_first_property(comp, ICAL_DTSTART_PROPERTY);
if (prop) subseconds_from_icalprop(prop, &exrecurdt.nano);
}
struct buf buf = BUF_INITIALIZER;
- format_localdatetime(&exrecurdt, &buf);
+ jmapical_localdatetime_as_string(&exrecurdt, &buf);
char *recurid = buf_release(&buf);
/* start */
const char *exstart = json_string_value(json_object_get(ex, "start"));
if (exstart && !strcmp(exstart, recurid)) {
json_object_del(ex, "start");
}
/* Create override patch */
json_t *diff = jmap_patchobject_create(event, ex);
json_object_del(diff, "@type");
json_object_del(diff, "uid");
json_object_del(diff, "relatedTo");
json_object_del(diff, "prodId");
json_object_del(diff, "method");
json_object_del(diff, "recurrenceId");
json_object_del(diff, "recurrenceRule");
json_object_del(diff, "recurrenceOverrides");
json_object_del(diff, "replyTo");
if (json_is_null(json_object_get(diff, "start"))) {
json_object_del(diff, "start");
}
if (json_is_null(json_object_get(diff, "showWithoutTime"))) {
json_object_del(diff, "showWithoutTime");
}
/* Set override at recurrence id */
json_object_set_new(exceptions, recurid, diff);
json_decref(ex);
free(recurid);
}
json_object_update(overrides, exceptions);
json_decref(exceptions);
if (!json_object_size(overrides)) {
json_decref(overrides);
overrides = json_null();
}
return overrides;
}
static int match_uri(const char *uri1, const char *uri2)
{
const char *col1 = strchr(uri1, ':');
const char *col2 = strchr(uri2, ':');
if (col1 == NULL && col2 == NULL) {
return !strcmp(uri1, uri2);
}
else if (col1 && col2 && (col1-uri1) == (col2-uri2)) {
size_t schemelen = col1-uri1;
return !strncasecmp(uri1, uri2, schemelen) &&
!strcmp(uri1+schemelen, uri2+schemelen);
}
else return 0;
}
static json_t*
rsvpto_from_ical(icalproperty *prop)
{
json_t *rsvpTo = json_object();
struct buf buf = BUF_INITIALIZER;
/* Read RVSP methods defined in RSVP-URI x-parameters. A RSVP-URI
* x-parameter value is of the form method:uri. If no method is defined,
* it's interpreted as the "web" method for legacy reasons. */
icalparameter *param, *next;
for (param = icalproperty_get_first_parameter(prop, ICAL_X_PARAMETER);
param;
param = next) {
next = icalproperty_get_next_parameter(prop, ICAL_X_PARAMETER);
if (strcasecmp(icalparameter_get_xname(param), JMAPICAL_XPARAM_RSVP_URI)) {
continue;
}
const char *val = icalparameter_get_xvalue(param);
const char *col1 = strchr(val, ':');
const char *col2 = col1 ? strchr(col1 + 1, ':') : NULL;
if (!col2) {
json_object_set_new(rsvpTo, "web", json_string(val));
} else {
buf_setmap(&buf, val, col1 - val);
json_object_set_new(rsvpTo, buf_cstring(&buf), json_string(col1 + 1));
}
}
/* Read URI from property value and check if this URI already is defined.
* If it isn't, this could be because an iCalendar client updated the
* property value, but kept the RSVP x-params. */
const char *caladdress = icalproperty_get_value_as_string(prop);
int caladdress_is_defined = 0;
json_t *jval;
const char *key;
json_object_foreach(rsvpTo, key, jval) {
if (match_uri(caladdress, json_string_value(jval))) {
caladdress_is_defined = 1;
break;
}
}
if (!caladdress_is_defined) {
if (!strncasecmp(caladdress, "mailto:", 7))
json_object_set_new(rsvpTo, "imip", json_string(caladdress));
else
json_object_set_new(rsvpTo, "other", json_string(caladdress));
}
if (!json_object_size(rsvpTo)) {
json_decref(rsvpTo);
rsvpTo = json_null();
}
buf_free(&buf);
return rsvpTo;
}
static json_t *participant_from_ical(icalproperty *prop,
hash_table *attendee_by_uri,
hash_table *id_by_uri,
icalproperty *orga)
{
json_t *p = json_pack("{s:s}", "@type", "Participant");
icalparameter *param;
struct buf buf = BUF_INITIALIZER;
/* FIXME invitedBy */
/* sendTo */
json_t *sendTo = rsvpto_from_ical(prop);
json_object_set_new(p, "sendTo", sendTo ? sendTo : json_null());
/* email */
char *email = NULL;
param = icalproperty_get_first_parameter(prop, ICAL_EMAIL_PARAMETER);
if (param) {
email = xstrdupnull(icalparameter_get_value_as_string(param));
}
else if (json_object_get(sendTo, "imip")) {
const char *uri = json_string_value(json_object_get(sendTo, "imip"));
email = mailaddr_from_uri(uri);
}
json_object_set_new(p, "email", email ? json_string(email) : json_null());
free(email);
/* name */
const char *name = NULL;
param = icalproperty_get_first_parameter(prop, ICAL_CN_PARAMETER);
if (param) {
name = icalparameter_get_cn(param);
}
json_object_set_new(p, "name", json_string(name ? name : ""));
/* kind */
const char *kind = NULL;
param = icalproperty_get_first_parameter(prop, ICAL_CUTYPE_PARAMETER);
if (param) {
icalparameter_cutype cutype = icalparameter_get_cutype(param);
switch (cutype) {
case ICAL_CUTYPE_INDIVIDUAL:
kind = "individual";
break;
case ICAL_CUTYPE_GROUP:
kind = "group";
break;
case ICAL_CUTYPE_RESOURCE:
kind = "resource";
break;
case ICAL_CUTYPE_ROOM:
kind = "location";
break;
default:
kind = "unknown";
}
}
if (kind) {
json_object_set_new(p, "kind", json_string(kind));
}
/* attendance */
const char *attendance = NULL;
icalparameter_role ical_role = ICAL_ROLE_REQPARTICIPANT;
param = icalproperty_get_first_parameter(prop, ICAL_ROLE_PARAMETER);
if (param) {
ical_role = icalparameter_get_role(param);
switch (ical_role) {
case ICAL_ROLE_REQPARTICIPANT:
attendance = "required";
break;
case ICAL_ROLE_OPTPARTICIPANT:
attendance = "optional";
break;
case ICAL_ROLE_NONPARTICIPANT:
attendance = "none";
break;
case ICAL_ROLE_CHAIR:
/* fall through */
default:
attendance = "required";
}
}
if (!attendance) attendance = "required";
json_object_set_new(p, "attendance", json_string(attendance));
/* roles */
json_t *roles = json_object();
for (param = icalproperty_get_first_parameter(prop, ICAL_X_PARAMETER);
param;
param = icalproperty_get_next_parameter(prop, ICAL_X_PARAMETER)) {
if (strcmp(icalparameter_get_xname(param), JMAPICAL_XPARAM_ROLE))
continue;
buf_setcstr(&buf, icalparameter_get_xvalue(param));
json_object_set_new(roles, buf_lcase(&buf), json_true());
}
if (!json_object_get(roles, "owner")) {
const char *o = icalproperty_get_organizer(orga);
const char *a = icalproperty_get_attendee(prop);
if (!strcasecmpsafe(o, a)) {
json_object_set_new(roles, "owner", json_true());
json_object_set_new(roles, "attendee", json_true());
}
}
if (ical_role == ICAL_ROLE_CHAIR) {
json_object_set_new(roles, "chair", json_true());
}
if (!json_object_size(roles)) {
json_object_set_new(roles, "attendee", json_true());
}
json_object_set_new(p, "roles", roles);
/* locationId */
const char *locid;
if ((locid = get_icalxparam_value(prop, JMAPICAL_XPARAM_LOCATIONID))) {
json_object_set_new(p, "locationId", json_string(locid));
}
/* participationStatus */
const char *partstat = NULL;
short depth = 0;
icalproperty *partstat_prop = prop;
while (!partstat) {
param = icalproperty_get_first_parameter(partstat_prop, ICAL_PARTSTAT_PARAMETER);
if (!param) break;
icalparameter_partstat pst = icalparameter_get_partstat(param);
switch (pst) {
case ICAL_PARTSTAT_ACCEPTED:
partstat = "accepted";
break;
case ICAL_PARTSTAT_DECLINED:
partstat = "declined";
break;
case ICAL_PARTSTAT_TENTATIVE:
partstat = "tentative";
break;
case ICAL_PARTSTAT_NEEDSACTION:
partstat = "needs-action";
break;
case ICAL_PARTSTAT_DELEGATED:
/* Follow the delegate chain */
param = icalproperty_get_first_parameter(prop, ICAL_DELEGATEDTO_PARAMETER);
if (param) {
const char *to = icalparameter_get_delegatedto(param);
if (!to) continue;
char *uri = normalized_uri(to);
partstat_prop = hash_lookup(uri, attendee_by_uri);
free(uri);
if (partstat_prop) {
/* Determine PARTSTAT from delegate. */
if (++depth > 64) {
/* This is a pathological case: libical does
* not check for infinite DELEGATE chains, so we
* make sure not to fall in an endless loop. */
partstat = "none";
}
continue;
}
}
/* fallthrough */
default:
partstat = "none";
}
}
if (!partstat || !strcmp(partstat, "none"))
partstat = "needs-action";
json_object_set_new(p, "participationStatus", json_string(partstat));
/* expectReply */
int expect_reply = 0;
param = icalproperty_get_first_parameter(prop, ICAL_RSVP_PARAMETER);
if (param) {
icalparameter_rsvp val = icalparameter_get_rsvp(param);
expect_reply = val == ICAL_RSVP_TRUE;
}
json_object_set_new(p, "expectReply", json_boolean(expect_reply));
/* delegatedTo */
json_t *delegatedTo = json_object();
for (param = icalproperty_get_first_parameter(prop, ICAL_DELEGATEDTO_PARAMETER);
param;
param = icalproperty_get_next_parameter(prop, ICAL_DELEGATEDTO_PARAMETER)) {
char *uri = normalized_uri(icalparameter_get_delegatedto(param));
const char *to_id = hash_lookup(uri, id_by_uri);
free(uri);
if (to_id) json_object_set_new(delegatedTo, to_id, json_true());
}
if (json_object_size(delegatedTo)) {
json_object_set_new(p, "delegatedTo", delegatedTo);
}
else {
json_decref(delegatedTo);
}
/* delegatedFrom */
json_t *delegatedFrom = json_object();
for (param = icalproperty_get_first_parameter(prop, ICAL_DELEGATEDFROM_PARAMETER);
param;
param = icalproperty_get_next_parameter(prop, ICAL_DELEGATEDFROM_PARAMETER)) {
char *uri = normalized_uri(icalparameter_get_delegatedfrom(param));
const char *from_id = hash_lookup(uri, id_by_uri);
free(uri);
if (from_id) json_object_set_new(delegatedFrom, from_id, json_true());
}
if (json_object_size(delegatedFrom)) {
json_object_set_new(p, "delegatedFrom", delegatedFrom);
}
else {
json_decref(delegatedFrom);
}
/* memberof */
json_t *memberOf = json_object();
for (param = icalproperty_get_first_parameter(prop, ICAL_MEMBER_PARAMETER);
param;
param = icalproperty_get_next_parameter(prop, ICAL_MEMBER_PARAMETER)) {
char *uri = normalized_uri(icalparameter_get_member(param));
char *id = xstrdupnull(hash_lookup(uri, id_by_uri));
if (!id) id = sha1key(uri);
json_object_set_new(memberOf, id, json_true());
free(id);
free(uri);
}
if (json_object_size(memberOf)) {
json_object_set_new(p, "memberOf", memberOf);
} else {
json_decref(memberOf);
}
/* linkIds */
json_t *linkIds = json_object();
for (param = icalproperty_get_first_parameter(prop, ICAL_X_PARAMETER);
param;
param = icalproperty_get_next_parameter(prop, ICAL_X_PARAMETER)) {
if (strcmp(icalparameter_get_xname(param), JMAPICAL_XPARAM_LINKID))
continue;
buf_setcstr(&buf, icalparameter_get_xvalue(param));
json_object_set_new(linkIds, buf_lcase(&buf), json_true());
}
if (json_object_size(linkIds)) {
json_object_set_new(p, "linkIds", linkIds);
}
else {
json_decref(linkIds);
}
/* scheduleSequence */
long schedule_sequence = 0;
const char *xval = get_icalxparam_value(prop, JMAPICAL_XPARAM_SEQUENCE);
if (xval) {
bit64 res;
if (parsenum(xval, &xval, strlen(xval), &res) == 0) {
schedule_sequence = res;
}
}
json_object_set_new(p, "scheduleSequence", json_integer(schedule_sequence));
/* scheduleUpdated */
if ((xval = get_icalxparam_value(prop, JMAPICAL_XPARAM_DTSTAMP))) {
icaltimetype icaltstamp = icaltime_from_string(xval);
if (!icaltime_is_null_time(icaltstamp) && !icaltstamp.is_date &&
icaltstamp.zone == icaltimezone_get_utc_timezone()) {
- struct datetime tstamp = JMAP_DATETIME_INITIALIZER;
- datetime_from_icaltime(icaltstamp, &tstamp); // XXX subseconds
- format_utcdatetime(&tstamp, &buf);
+ struct jmapical_datetime tstamp = JMAPICAL_DATETIME_INITIALIZER;
+ jmapical_datetime_from_icaltime(icaltstamp, &tstamp); // XXX subseconds
+ jmapical_utcdatetime_as_string(&tstamp, &buf);
json_object_set_new(p, "scheduleUpdated", json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
}
/* participationComment */
const char *comment = get_icalxparam_value(prop, JMAPICAL_XPARAM_COMMENT);
if (comment) {
json_object_set_new(p, "participationComment", json_string(comment));
}
buf_free(&buf);
return p;
}
static json_t*
participant_from_icalorganizer(icalproperty *orga)
{
json_t *jorga = json_pack("{s:s}", "@type", "Participant");
/* name */
icalparameter *param;
const char *name = NULL;
if ((param = icalproperty_get_first_parameter(orga, ICAL_CN_PARAMETER))) {
name = icalparameter_get_cn(param);
}
json_object_set_new(jorga, "name", json_string(name ? name : ""));
/* roles */
json_object_set_new(jorga, "roles", json_pack("{s:b}", "owner", 1));
/* sendTo */
/* email */
const char *caladdress = icalproperty_get_value_as_string(orga);
if (!strncasecmp(caladdress, "mailto:", 7)) {
json_object_set_new(jorga, "sendTo", json_pack("{s:s}", "imip", caladdress));
char *email = mailaddr_from_uri(caladdress);
json_object_set_new(jorga, "email", json_string(email));
free(email);
}
else {
json_object_set_new(jorga, "sendTo", json_pack("{s:s}", "other", caladdress));
json_object_set_new(jorga, "email", json_null());
}
/* Set default values */
json_object_set_new(jorga, "attendance", json_string("required"));
json_object_set_new(jorga, "participationStatus", json_string("needs-action"));
json_object_set_new(jorga, "scheduleSequence", json_integer(0));
json_object_set_new(jorga, "expectReply", json_false());
return jorga;
}
/* Convert the ical ORGANIZER/ATTENDEEs in comp to CalendarEvent participants */
static json_t*
participants_from_ical(icalcomponent *comp)
{
struct hash_table attendee_by_uri = HASH_TABLE_INITIALIZER;
struct hash_table id_by_uri = HASH_TABLE_INITIALIZER;
icalproperty *prop;
json_t *participants = json_object();
/* Collect all attendees in a map to lookup delegates and their ids. */
construct_hash_table(&attendee_by_uri, 32, 0);
construct_hash_table(&id_by_uri, 32, 0);
for (prop = icalcomponent_get_first_property(comp, ICAL_ATTENDEE_PROPERTY);
prop;
prop = icalcomponent_get_next_property(comp, ICAL_ATTENDEE_PROPERTY)) {
/* Map normalized URI to ATTENDEE */
char *uri = normalized_uri(icalproperty_get_value_as_string(prop));
hash_insert(uri, prop, &attendee_by_uri);
/* Map mailto:URI to ID */
char *id = xstrdupnull(get_icalxparam_value(prop, JMAPICAL_XPARAM_ID));
if (!id) id = sha1key(uri);
hash_insert(uri, id, &id_by_uri);
free(uri);
}
/* Map ATTENDEE to JSCalendar */
icalproperty *orga = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
for (prop = icalcomponent_get_first_property(comp, ICAL_ATTENDEE_PROPERTY);
prop;
prop = icalcomponent_get_next_property(comp, ICAL_ATTENDEE_PROPERTY)) {
char *uri = normalized_uri(icalproperty_get_value_as_string(prop));
const char *id = hash_lookup(uri, &id_by_uri);
json_t *p = participant_from_ical(prop, &attendee_by_uri, &id_by_uri, orga);
json_object_set_new(participants, id, p);
free(uri);
}
if (orga) {
const char *caladdress = icalproperty_get_value_as_string(orga);
char *uri = normalized_uri(caladdress);
if (!hash_lookup(uri, &attendee_by_uri)) {
/* Add a default participant for the organizer. */
char *id = xstrdupnull(get_icalxparam_value(orga, JMAPICAL_XPARAM_ID));
if (!id) id = sha1key(uri);
json_t *jorga = participant_from_icalorganizer(orga);
json_object_set_new(participants, id, jorga);
free(id);
}
free(uri);
}
if (!json_object_size(participants)) {
json_decref(participants);
participants = json_null();
}
free_hash_table(&attendee_by_uri, NULL);
free_hash_table(&id_by_uri, free);
return participants;
}
static json_t*
link_from_ical(icalproperty *prop)
{
/* href */
const char *href = NULL;
if (icalproperty_isa(prop) == ICAL_ATTACH_PROPERTY) {
icalattach *attach = icalproperty_get_attach(prop);
/* Ignore ATTACH properties with value BINARY. */
if (!attach || !icalattach_get_is_url(attach)) {
return NULL;
}
href = icalattach_get_url(attach);
}
else if (icalproperty_isa(prop) == ICAL_URL_PROPERTY) {
href = icalproperty_get_value_as_string(prop);
}
if (!href || *href == '\0') return NULL;
json_t *link = json_pack("{s:s: s:s}", "@type", "Link", "href", href);
icalparameter *param = NULL;
const char *s;
/* cid */
if ((s = get_icalxparam_value(prop, JMAPICAL_XPARAM_CID))) {
json_object_set_new(link, "cid", json_string(s));
}
/* contentType */
param = icalproperty_get_first_parameter(prop, ICAL_FMTTYPE_PARAMETER);
if (param && ((s = icalparameter_get_fmttype(param)))) {
json_object_set_new(link, "contentType", json_string(s));
}
/* title - reuse the same x-param as Apple does for their locations */
if ((s = get_icalxparam_value(prop, JMAPICAL_XPARAM_TITLE))) {
json_object_set_new(link, "title", json_string(s));
}
/* size */
json_int_t size = -1;
param = icalproperty_get_size_parameter(prop);
if (param) {
if ((s = icalparameter_get_size(param))) {
char *ptr;
size = strtol(s, &ptr, 10);
json_object_set_new(link, "size",
ptr && *ptr == '\0' ? json_integer(size) : json_null());
}
}
/* rel */
const char *rel = get_icalxparam_value(prop, JMAPICAL_XPARAM_REL);
if (!rel)
rel = icalproperty_isa(prop) == ICAL_URL_PROPERTY ? "describedby" :
"enclosure";
json_object_set_new(link, "rel", json_string(rel));
/* display */
if ((s = get_icalxparam_value(prop, JMAPICAL_XPARAM_DISPLAY))) {
json_object_set_new(link, "display", json_string(s));
}
return link;
}
static json_t*
links_from_ical(icalcomponent *comp)
{
icalproperty* prop;
json_t *ret = json_pack("{}");
/* Read iCalendar ATTACH properties */
for (prop = icalcomponent_get_first_property(comp, ICAL_ATTACH_PROPERTY);
prop;
prop = icalcomponent_get_next_property(comp, ICAL_ATTACH_PROPERTY)) {
char *id = xstrdupnull(get_icalxparam_value(prop, JMAPICAL_XPARAM_ID));
if (!id) id = sha1key(icalproperty_get_value_as_string(prop));
json_t *link = link_from_ical(prop);
if (link) json_object_set_new(ret, id, link);
free(id);
}
/* Read iCalendar URL property. Should only be one. */
for (prop = icalcomponent_get_first_property(comp, ICAL_URL_PROPERTY);
prop;
prop = icalcomponent_get_next_property(comp, ICAL_URL_PROPERTY)) {
char *id = xstrdupnull(get_icalxparam_value(prop, JMAPICAL_XPARAM_ID));
if (!id) id = sha1key(icalproperty_get_value_as_string(prop));
json_t *link = link_from_ical(prop);
if (link) json_object_set_new(ret, id, link);
free(id);
}
if (!json_object_size(ret)) {
json_decref(ret);
ret = json_null();
}
return ret;
}
/* Convert the VALARMS in the VEVENT comp to CalendarEvent alerts.
* Adds any ATTACH properties found in VALARM components to the
* event 'links' property. */
static json_t*
alerts_from_ical(icalcomponent *comp)
{
json_t* alerts = json_pack("{}");
icalcomponent* alarm;
struct buf buf = BUF_INITIALIZER;
for (alarm = icalcomponent_get_first_component(comp, ICAL_VALARM_COMPONENT);
alarm;
alarm = icalcomponent_get_next_component(comp, ICAL_VALARM_COMPONENT)) {
icalproperty* prop;
json_t *alert = json_pack("{s:s}", "@type", "Alert");
/* alert id */
char *id = (char *) icalcomponent_get_uid(alarm);
if (!id) {
id = sha1key(icalcomponent_as_ical_string(alarm));
} else {
id = xstrdup(id);
}
/* Determine TRIGGER and RELATED parameter */
struct icaltriggertype trigger = {
icaltime_null_time(), icaldurationtype_null_duration()
};
icalparameter_related related = ICAL_RELATED_START;
icalproperty *triggerprop = icalcomponent_get_first_property(alarm, ICAL_TRIGGER_PROPERTY);
if (triggerprop) {
trigger = icalproperty_get_trigger(triggerprop);
icalparameter *param = icalproperty_get_first_parameter(triggerprop, ICAL_RELATED_PARAMETER);
if (param) {
related = icalparameter_get_related(param);
if (related != ICAL_RELATED_START && related != ICAL_RELATED_END) {
free(id);
continue;
}
}
}
/* trigger */
json_t *jtrigger = json_object();
if (!icaldurationtype_is_null_duration(trigger.duration) ||
icaltime_is_null_time(trigger.time)) {
/* Convert to offset trigger */
json_object_set_new(jtrigger, "@type", json_string("OffsetTrigger"));
- struct duration duration = JMAP_DURATION_INITIALIZER;
- duration_from_icalduration(trigger.duration, &duration);
+ struct jmapical_duration duration = JMAPICAL_DURATION_INITIALIZER;
+ jmapical_duration_from_icalduration(trigger.duration, &duration);
subseconds_from_icalprop(triggerprop, &duration.nanos);
/* relativeTo */
const char *relative_to = related == ICAL_RELATED_START ? "start" : "end";
json_object_set_new(jtrigger, "relativeTo", json_string(relative_to));
/* offset*/
- format_duration(&duration, &buf);
+ jmapical_duration_as_string(&duration, &buf);
json_object_set_new(jtrigger, "offset", json_string(buf_cstring(&buf)));
buf_reset(&buf);
} else {
/* Convert to absolute trigger */
json_object_set_new(jtrigger, "@type", json_string("AbsoluteTrigger"));
- struct datetime when = JMAP_DATETIME_INITIALIZER;
- datetime_from_icalprop(triggerprop, &when);
- format_utcdatetime(&when, &buf);
+ struct jmapical_datetime when = JMAPICAL_DATETIME_INITIALIZER;
+ jmapical_datetime_from_icalprop(triggerprop, &when);
+ jmapical_utcdatetime_as_string(&when, &buf);
/* when */
json_object_set_new(jtrigger, "when", json_string(buf_cstring(&buf)));
}
json_object_set_new(alert, "trigger", jtrigger);
/* action */
const char *action = "display";
prop = icalcomponent_get_first_property(alarm, ICAL_ACTION_PROPERTY);
if (prop && icalproperty_get_action(prop) == ICAL_ACTION_EMAIL) {
action = "email";
}
json_object_set_new(alert, "action", json_string(action));
/* acknowledged */
if ((prop = icalcomponent_get_acknowledged_property(alarm))) {
- struct datetime tstamp = JMAP_DATETIME_INITIALIZER;
- datetime_from_icalprop(prop, &tstamp);
- format_utcdatetime(&tstamp, &buf);
+ struct jmapical_datetime tstamp = JMAPICAL_DATETIME_INITIALIZER;
+ jmapical_datetime_from_icalprop(prop, &tstamp);
+ jmapical_utcdatetime_as_string(&tstamp, &buf);
json_t *jval = json_string(buf_cstring(&buf));
buf_reset(&buf);
json_object_set_new(alert, "acknowledged", jval);
}
/* relatedTo */
json_t *jrelatedto = relatedto_from_ical(alarm);
if (JNOTNULL(jrelatedto)) {
json_object_set_new(alert, "relatedTo", jrelatedto);
}
json_object_set_new(alerts, id, alert);
free(id);
}
if (!json_object_size(alerts)) {
json_decref(alerts);
alerts = json_null();
}
buf_free(&buf);
return alerts;
}
/* Convert a VEVENT ical component to CalendarEvent keywords */
static json_t*
keywords_from_ical(icalcomponent *comp)
{
icalproperty* prop;
json_t *ret = json_object();
for (prop = icalcomponent_get_first_property(comp, ICAL_CATEGORIES_PROPERTY);
prop;
prop = icalcomponent_get_next_property(comp, ICAL_CATEGORIES_PROPERTY)) {
json_object_set_new(ret, icalproperty_get_categories(prop), json_true());
}
if (!json_object_size(ret)) {
json_decref(ret);
ret = json_null();
}
return ret;
}
/* Convert a VEVENT ical component to CalendarEvent relatedTo */
static json_t*
relatedto_from_ical(icalcomponent *comp)
{
icalproperty* prop;
json_t *ret = json_pack("{}");
for (prop = icalcomponent_get_first_property(comp, ICAL_RELATEDTO_PROPERTY);
prop;
prop = icalcomponent_get_next_property(comp, ICAL_RELATEDTO_PROPERTY)) {
const char *uid = icalproperty_get_value_as_string(prop);
if (!uid || !strlen(uid)) continue;
icalparameter *param = NULL;
json_t *relation = json_object();
for (param = icalproperty_get_first_parameter(prop, ICAL_RELTYPE_PARAMETER);
param;
param = icalproperty_get_next_parameter(prop, ICAL_RELTYPE_PARAMETER)) {
const char *reltype = icalparameter_get_xvalue(param);
if (reltype && *reltype) {
char *s = lcase(xstrdup(reltype));
json_object_set_new(relation, s, json_true());
free(s);
}
else json_object_set_new(relation, "parent", json_true());
}
json_object_set_new(ret, uid, json_pack("{s:s s:o}",
"@type", "Relation", "relation", relation));
}
if (!json_object_size(ret)) {
json_decref(ret);
ret = json_null();
}
return ret;
}
static json_t* location_from_ical(icalproperty *prop, json_t *links)
{
icalparameter *param;
json_t *loc = json_pack("{s:s}", "@type", "Location");
/* name */
const char *name = icalvalue_get_text(icalproperty_get_value(prop));
json_object_set_new(loc, "name", json_string(name ? name : ""));
/* rel */
const char *rel = get_icalxparam_value(prop, JMAPICAL_XPARAM_REL);
if (rel) json_object_set_new(loc, "relativeTo", json_string(rel));
/* description */
const char *desc = get_icalxparam_value(prop, JMAPICAL_XPARAM_DESCRIPTION);
if (desc) json_object_set_new(loc, "description", json_string(desc));
/* timeZone */
const char *tzid = get_icalxparam_value(prop, JMAPICAL_XPARAM_TZID);
if (tzid) json_object_set_new(loc, "timeZone", json_string(tzid));
/* coordinates */
const char *coord = get_icalxparam_value(prop, JMAPICAL_XPARAM_GEO);
if (coord) json_object_set_new(loc, "coordinates", json_string(coord));
/* linkIds (including altrep) */
json_t *linkids = json_object();
for (param = icalproperty_get_first_parameter(prop, ICAL_X_PARAMETER);
param;
param = icalproperty_get_next_parameter(prop, ICAL_X_PARAMETER)) {
if (strcasecmp(icalparameter_get_xname(param), JMAPICAL_XPARAM_LINKID)) {
continue;
}
const char *s = icalparameter_get_xvalue(param);
if (!s) continue;
json_object_set_new(linkids, s, json_true());
}
const char *altrep = NULL;
param = icalproperty_get_first_parameter(prop, ICAL_ALTREP_PARAMETER);
if (param) altrep = icalparameter_get_altrep(param);
if (altrep) {
char *tmp = sha1key(altrep);
json_object_set_new(links, tmp, json_pack("{s:s}", "href", altrep));
json_object_set_new(linkids, tmp, json_true());
free(tmp);
}
if (!json_object_size(linkids)) {
json_decref(linkids);
linkids = NULL;
}
else json_object_set_new(loc, "linkIds", linkids);
return loc;
}
static json_t *coordinates_from_ical(icalproperty *prop)
{
/* Use verbatim coordinate string, rather than the parsed ical value */
const char *p, *val = icalproperty_get_value_as_string(prop);
struct buf buf = BUF_INITIALIZER;
json_t *c;
p = strchr(val, ';');
if (!p) return NULL;
buf_setcstr(&buf, "geo:");
buf_appendmap(&buf, val, p-val);
buf_appendcstr(&buf, ",");
val = p + 1;
buf_appendcstr(&buf, val);
c = json_string(buf_cstring(&buf));
buf_free(&buf);
return c;
}
static json_t*
locations_from_ical(icalcomponent *comp, json_t *links)
{
icalproperty* prop;
json_t *loc, *locations = json_pack("{}");
char *id;
/* Handle end locations */
const char *tzidstart = tzid_from_ical(comp, ICAL_DTSTART_PROPERTY);
const char *tzidend = tzid_from_ical(comp, ICAL_DTEND_PROPERTY);
if (tzidstart && tzidend && strcmp(tzidstart, tzidend)) {
prop = icalcomponent_get_first_property(comp, ICAL_DTEND_PROPERTY);
id = xjmapid_from_ical(prop);
loc = json_pack("{s:s s:s}", "timeZone", tzidend, "relativeTo", "end");
json_object_set_new(locations, id, loc);
free(id);
}
/* LOCATION */
if ((prop = icalcomponent_get_first_property(comp, ICAL_LOCATION_PROPERTY))) {
id = xjmapid_from_ical(prop);
if ((loc = location_from_ical(prop, links))) {
json_object_set_new(locations, id, loc);
}
free(id);
}
/* GEO */
if ((prop = icalcomponent_get_first_property(comp, ICAL_GEO_PROPERTY))) {
json_t *coord = coordinates_from_ical(prop);
if (coord) {
loc = json_pack("{s:o}", "coordinates", coord);
id = xjmapid_from_ical(prop);
json_object_set_new(locations, id, loc);
free(id);
}
}
/* Lookup X-property locations */
for (prop = icalcomponent_get_first_property(comp, ICAL_X_PROPERTY);
prop;
prop = icalcomponent_get_next_property(comp, ICAL_X_PROPERTY)) {
const char *name = icalproperty_get_property_name(prop);
/* X-APPLE-STRUCTURED-LOCATION */
/* FIXME Most probably,
* a X-APPLE-STRUCTURED-LOCATION may occur only once and
* always comes with a LOCATION. But who knows for sure? */
if (!strcmp(name, "X-APPLE-STRUCTURED-LOCATION")) {
const char *title, *uri;
icalvalue *val;
val = icalproperty_get_value(prop);
if (icalvalue_isa(val) != ICAL_URI_VALUE) continue;
uri = icalvalue_as_ical_string(val);
if (strncmp(uri, "geo:", 4)) continue;
loc = json_pack("{s:s}", "coordinates", uri);
if ((title = get_icalxparam_value(prop, JMAPICAL_XPARAM_TITLE))) {
json_object_set_new(loc, "name", json_string(title));
}
id = xjmapid_from_ical(prop);
json_object_set_new(locations, id, loc);
free(id);
continue;
}
if (strcmp(name, JMAPICAL_XPROP_LOCATION)) {
continue;
}
/* X-JMAP-LOCATION */
id = xjmapid_from_ical(prop);
loc = location_from_ical(prop, links);
if (loc) json_object_set_new(locations, id, loc);
free(id);
}
if (!json_object_size(locations)) {
json_decref(locations);
locations = json_null();
}
return locations;
}
static json_t*
virtuallocations_from_ical(icalcomponent *comp)
{
icalproperty* prop;
json_t *locations = json_pack("{}");
for (prop = icalcomponent_get_first_property(comp, ICAL_CONFERENCE_PROPERTY);
prop;
prop = icalcomponent_get_next_property(comp, ICAL_CONFERENCE_PROPERTY)) {
char *id = xjmapid_from_ical(prop);
json_t *loc = json_pack("{s:s}", "@type", "VirtualLocation");
const char *uri = icalproperty_get_value_as_string(prop);
if (uri) json_object_set_new(loc, "uri", json_string(uri));
const char *name = "";
icalparameter *param = icalproperty_get_first_parameter(prop, ICAL_LABEL_PARAMETER);
if (param) name = icalparameter_get_label(param);
if (!name) name = "";
json_object_set_new(loc, "name", json_string(name));
const char *desc = get_icalxparam_value(prop, JMAPICAL_XPARAM_DESCRIPTION);
if (desc) json_object_set_new(loc, "description", json_string(desc));
if (uri) json_object_set_new(locations, id, loc);
free(id);
}
if (!json_object_size(locations)) {
json_decref(locations);
locations = json_null();
}
return locations;
}
-static void duration_from_vevent(icalcomponent *comp, struct duration *dur)
+static void duration_from_vevent(icalcomponent *comp, struct jmapical_duration *dur)
{
struct icaldurationtype icaldur = icaldurationtype_null_duration();
struct icaltimetype dtstart = dtstart_from_ical(comp);
struct icaltimetype dtend = dtend_from_ical(comp);
bit64 nanos = 0;
if (!icaltime_is_null_time(dtend)) {
time_t tstart = icaltime_as_timet_with_zone(dtstart, dtstart.zone);
time_t tend = icaltime_as_timet_with_zone(dtend, dtend.zone);
icalproperty *prop;
if ((prop = icalcomponent_get_first_property(comp, ICAL_DTEND_PROPERTY))) {
bit64 startnanos = 0;
bit64 endnanos = 0;
subseconds_from_icalprop(prop, &endnanos);
if ((prop = icalcomponent_get_first_property(comp, ICAL_DTSTART_PROPERTY))) {
subseconds_from_icalprop(prop, &startnanos);
}
- struct duration utcduration = JMAP_DURATION_INITIALIZER;
- duration_between(tstart, startnanos, tend, endnanos, &utcduration);
+ struct jmapical_duration utcduration = JMAPICAL_DURATION_INITIALIZER;
+ jmapical_duration_between(tstart, startnanos, tend, endnanos, &utcduration);
icaldur = duration_to_icalduration(&utcduration);
nanos = utcduration.nanos;
}
else if ((prop = icalcomponent_get_first_property(comp, ICAL_DURATION_PROPERTY))) {
subseconds_from_icalprop(prop, &nanos);
icaldur = icaldurationtype_from_int((int)(tend - tstart));
if (icaldurationtype_is_bad_duration(icaldur) || icaldur.is_neg) {
icaldur = icaldurationtype_null_duration();
nanos = 0;
}
}
}
- duration_from_icalduration(icaldur, dur);
+ jmapical_duration_from_icalduration(icaldur, dur);
dur->nanos = nanos;
}
static json_t*
locale_from_ical(icalcomponent *comp)
{
icalproperty *sum, *dsc;
icalparameter *param = NULL;
const char *lang = NULL;
sum = icalcomponent_get_first_property(comp, ICAL_SUMMARY_PROPERTY);
dsc = icalcomponent_get_first_property(comp, ICAL_DESCRIPTION_PROPERTY);
if (sum) {
param = icalproperty_get_first_parameter(sum, ICAL_LANGUAGE_PARAMETER);
}
if (!param && dsc) {
param = icalproperty_get_first_parameter(dsc, ICAL_LANGUAGE_PARAMETER);
}
if (param) {
lang = icalparameter_get_language(param);
}
return lang ? json_string(lang) : json_null();
}
/* Convert the libical VEVENT comp to a CalendarEvent
*
* master: if not NULL, treat comp as a VEVENT exception
* props: if not NULL, only convert properties named as keys
*/
static json_t*
calendarevent_from_ical(icalcomponent *comp, hash_table *props, icalcomponent *master)
{
icalproperty* prop = NULL;
int is_exception = master != NULL;
hash_table *wantprops = NULL;
json_t *event = json_pack("{s:s}", "@type", "jsevent");
struct buf buf = BUF_INITIALIZER;
if (jmap_wantprop(props, "recurrenceOverrides") && !is_exception) {
/* Fetch all properties if recurrenceOverrides are requested,
* otherwise we might return incomplete override patches */
wantprops = props;
props = NULL;
}
/* Handle bogus mix of floating and time zoned types */
const char *tzid_start = tzid_from_ical(comp, ICAL_DTSTART_PROPERTY);
if (!tzid_start) tzid_start = tzid_from_ical(comp, ICAL_DTEND_PROPERTY);
/* start */
if (jmap_wantprop(props, "start")) {
- struct datetime start = JMAP_DATETIME_INITIALIZER;
+ struct jmapical_datetime start = JMAPICAL_DATETIME_INITIALIZER;
if ((prop = icalcomponent_get_first_property(comp, ICAL_DTSTART_PROPERTY))) {
- datetime_from_icalprop(prop, &start);
+ jmapical_datetime_from_icalprop(prop, &start);
}
- format_localdatetime(&start, &buf);
+ jmapical_localdatetime_as_string(&start, &buf);
json_object_set_new(event, "start", json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
/* timeZone */
if (jmap_wantprop(props, "timeZone")) {
json_object_set_new(event, "timeZone", tzid_start ?
json_string(tzid_start) : json_null());
}
/* duration */
if (jmap_wantprop(props, "duration")) {
- struct duration dur = JMAP_DURATION_INITIALIZER;
+ struct jmapical_duration dur = JMAPICAL_DURATION_INITIALIZER;
duration_from_vevent(comp, &dur);
- format_duration(&dur, &buf);
+ jmapical_duration_as_string(&dur, &buf);
json_object_set_new(event, "duration", json_string(buf_cstring(&buf)));
buf_reset(&buf);
}
/* showWithoutTime */
if (jmap_wantprop(props, "showWithoutTime")) {
int show_without_time = 0;
const char *strval = get_icalxprop_value(comp, "SHOW-WITHOUT-TIME");
if (strval) {
show_without_time = !strcasecmp(strval, "TRUE");
}
else {
show_without_time = icaltime_is_date(icalcomponent_get_dtstart(comp));
}
json_object_set_new(event, "showWithoutTime", json_boolean(show_without_time));
}
/* uid */
const char *uid = icalcomponent_get_uid(comp);
if (uid && !is_exception) {
json_object_set_new(event, "uid", json_string(uid));
}
/* relatedTo */
if (jmap_wantprop(props, "relatedTo") && !is_exception) {
json_object_set_new(event, "relatedTo", relatedto_from_ical(comp));
}
/* prodId */
if (jmap_wantprop(props, "prodId") && !is_exception) {
icalcomponent *ical = icalcomponent_get_parent(comp);
const char *prodid = NULL;
prop = icalcomponent_get_first_property(ical, ICAL_PRODID_PROPERTY);
if (prop) prodid = icalproperty_get_prodid(prop);
json_object_set_new(event, "prodId",
prodid ? json_string(prodid) : json_null());
}
/* created */
if (jmap_wantprop(props, "created")) {
- struct datetime created = JMAP_DATETIME_INITIALIZER;
+ struct jmapical_datetime created = JMAPICAL_DATETIME_INITIALIZER;
if ((prop = icalcomponent_get_first_property(comp, ICAL_CREATED_PROPERTY))) {
- datetime_from_icalprop(prop, &created);
- format_utcdatetime(&created, &buf);
+ jmapical_datetime_from_icalprop(prop, &created);
+ jmapical_utcdatetime_as_string(&created, &buf);
json_t *jval = json_string(buf_cstring(&buf));
buf_reset(&buf);
json_object_set_new(event, "created", jval);
}
}
/* updated */
if (jmap_wantprop(props, "updated")) {
- struct datetime updated = JMAP_DATETIME_INITIALIZER;
+ struct jmapical_datetime updated = JMAPICAL_DATETIME_INITIALIZER;
if ((prop = icalcomponent_get_first_property(comp, ICAL_DTSTAMP_PROPERTY))) {
- datetime_from_icalprop(prop, &updated);
- format_utcdatetime(&updated, &buf);
+ jmapical_datetime_from_icalprop(prop, &updated);
+ jmapical_utcdatetime_as_string(&updated, &buf);
json_t* jval = json_string(buf_cstring(&buf));
buf_reset(&buf);
json_object_set_new(event, "updated", jval);
}
}
/* sequence */
if (jmap_wantprop(props, "sequence")) {
json_object_set_new(event, "sequence",
json_integer(icalcomponent_get_sequence(comp)));
}
/* priority */
if (jmap_wantprop(props, "priority")) {
int priority = 0;
prop = icalcomponent_get_first_property(comp, ICAL_PRIORITY_PROPERTY);
if (prop) priority = icalproperty_get_priority(prop);
json_object_set_new(event, "priority", json_integer(priority));
}
/* title */
if (jmap_wantprop(props, "title")) {
const char *title= "";
prop = icalcomponent_get_first_property(comp, ICAL_SUMMARY_PROPERTY);
if (prop) {
title = icalproperty_get_summary(prop);
if (!title) title = "";
}
json_object_set_new(event, "title", json_string(title));
}
/* description */
if (jmap_wantprop(props, "description") || jmap_wantprop(props, "descriptionContentType")) {
const char *desc = "";
prop = icalcomponent_get_first_property(comp, ICAL_DESCRIPTION_PROPERTY);
if (prop) {
desc = icalproperty_get_description(prop);
if (!desc) desc = "";
}
if (jmap_wantprop(props, "description")) {
json_object_set_new(event, "description", json_string(desc));
}
if (jmap_wantprop(props, "descriptionContentType")) {
json_object_set_new(event, "descriptionContentType", json_string("text/plain"));
}
}
/* method */
if (jmap_wantprop(props, "method")) {
icalcomponent *ical = icalcomponent_get_parent(comp);
if (ical) {
icalproperty_method icalmethod = icalcomponent_get_method(ical);
if (icalmethod != ICAL_METHOD_NONE) {
char *method = xstrdupsafe(icalenum_method_to_string(icalmethod));
lcase(method);
json_object_set_new(event, "method", json_string(method));
free(method);
}
}
}
/* color */
if (jmap_wantprop(props, "color")) {
prop = icalcomponent_get_first_property(comp, ICAL_COLOR_PROPERTY);
if (prop) {
json_object_set_new(event, "color",
json_string(icalproperty_get_color(prop)));
}
}
/* keywords */
if (jmap_wantprop(props, "keywords")) {
json_object_set_new(event, "keywords", keywords_from_ical(comp));
}
/* links */
if (jmap_wantprop(props, "links")) {
json_object_set_new(event, "links", links_from_ical(comp));
}
/* locale */
if (jmap_wantprop(props, "locale")) {
json_object_set_new(event, "locale", locale_from_ical(comp));
}
/* locations */
if (jmap_wantprop(props, "locations")) {
json_t *links = json_object();
json_object_set_new(event, "locations", locations_from_ical(comp, links));
if (json_object_size(links)) {
if (JNOTNULL(json_object_get(event, "links"))) {
json_object_update(json_object_get(event, "links"), links);
} else {
json_object_set(event, "links", links);
}
}
json_decref(links);
}
/* virtualLocations */
if (jmap_wantprop(props, "virtualLocations")) {
json_object_set_new(event, "virtualLocations", virtuallocations_from_ical(comp));
}
/* recurrenceRule */
if (jmap_wantprop(props, "recurrenceRule") && !is_exception) {
json_object_set_new(event, "recurrenceRule", recurrence_from_ical(comp));
}
/* status */
if (jmap_wantprop(props, "status")) {
const char *status = "confirmed";
switch (icalcomponent_get_status(comp)) {
case ICAL_STATUS_TENTATIVE:
status = "tentative";
break;
case ICAL_STATUS_CONFIRMED:
status = "confirmed";
break;
case ICAL_STATUS_CANCELLED:
status = "cancelled";
break;
default:
status = "confirmed";
}
json_object_set_new(event, "status", json_string(status));
}
/* freeBusyStatus */
if (jmap_wantprop(props, "freeBusyStatus")) {
const char *fbs = "busy";
if ((prop = icalcomponent_get_first_property(comp,
ICAL_TRANSP_PROPERTY))) {
if (icalproperty_get_transp(prop) == ICAL_TRANSP_TRANSPARENT) {
fbs = "free";
}
}
json_object_set_new(event, "freeBusyStatus", json_string(fbs));
}
/* privacy */
if (jmap_wantprop(props, "privacy")) {
const char *prv = "public";
if ((prop = icalcomponent_get_first_property(comp, ICAL_CLASS_PROPERTY))) {
switch (icalproperty_get_class(prop)) {
case ICAL_CLASS_CONFIDENTIAL:
prv = "secret";
break;
case ICAL_CLASS_PRIVATE:
prv = "private";
break;
default:
prv = "public";
}
}
json_object_set_new(event, "privacy", json_string(prv));
}
/* replyTo */
if (jmap_wantprop(props, "replyTo") && !is_exception) {
if ((prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY))) {
json_object_set_new(event, "replyTo",rsvpto_from_ical(prop));
}
}
/* participants */
if (jmap_wantprop(props, "participants")) {
json_object_set_new(event, "participants", participants_from_ical(comp));
}
/* useDefaultAlerts */
if (jmap_wantprop(props, "useDefaultAlerts")) {
const char *v = get_icalxprop_value(comp, JMAPICAL_XPROP_USEDEFALERTS);
json_object_set_new(event, "useDefaultAlerts",
json_boolean(v && !strcasecmp(v, "true")));
}
/* alerts */
if (jmap_wantprop(props, "alerts")) {
json_object_set_new(event, "alerts", alerts_from_ical(comp));
}
/* recurrenceOverrides - must be last to generate patches */
if (jmap_wantprop(props, "recurrenceOverrides") && !is_exception) {
json_object_set_new(event, "recurrenceOverrides",
overrides_from_ical(comp, event, tzid_start));
}
if (wantprops) {
jmap_filterprops(event, wantprops);
}
buf_free(&buf);
return event;
}
json_t*
jmapical_tojmap_all(icalcomponent *ical, hash_table *props)
{
icalcomponent* comp;
/* Locate all main VEVENTs. */
ptrarray_t todo = PTRARRAY_INITIALIZER;
icalcomponent *firstcomp =
icalcomponent_get_first_component(ical, ICAL_VEVENT_COMPONENT);
for (comp = firstcomp;
comp;
comp = icalcomponent_get_next_component(ical, ICAL_VEVENT_COMPONENT)) {
icalproperty *recurid = icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
if (recurid) continue;
if (icalcomponent_get_uid(comp) == NULL) continue;
ptrarray_append(&todo, comp);
}
/* magic promote to toplevel for the first item */
if (firstcomp && !ptrarray_size(&todo)) {
ptrarray_append(&todo, firstcomp);
}
else if (!ptrarray_size(&todo)) {
return json_array();
}
/* Convert the VEVENTs to JMAP. */
json_t *events = json_array();
while ((comp = ptrarray_pop(&todo))) {
json_t *jsevent = calendarevent_from_ical(comp, props, NULL);
if (jsevent) json_array_append_new(events, jsevent);
}
ptrarray_fini(&todo);
return events;
}
json_t*
jmapical_tojmap(icalcomponent *ical, hash_table *props)
{
json_t *jsevents = jmapical_tojmap_all(ical, props);
json_t *ret = NULL;
if (json_array_size(jsevents)) {
ret = json_incref(json_array_get(jsevents, 0));
}
json_decref(jsevents);
return ret;
}
/*
* Convert to iCalendar from JMAP
*/
static int validate_type(struct jmap_parser *parser, json_t *jobj, const char *wanttype)
{
json_t *jtype = json_object_get(jobj, "@type");
if (jtype && jtype != json_null()) {
if (!json_is_string(jtype) || strcasecmp(json_string_value(jtype), wanttype)) {
jmap_parser_invalid(parser, "@type");
return 0;
}
}
return 1;
}
static void relatedto_to_ical(icalcomponent *, struct jmap_parser *, json_t *);
/* defined in http_tzdist */
extern void icalcomponent_add_required_timezones(icalcomponent *ical);
/* Remove and deallocate any properties of kind in comp. */
static void remove_icalprop(icalcomponent *comp, icalproperty_kind kind)
{
icalproperty *prop, *next;
for (prop = icalcomponent_get_first_property(comp, kind);
prop;
prop = next) {
next = icalcomponent_get_next_property(comp, kind);
icalcomponent_remove_property(comp, prop);
icalproperty_free(prop);
}
}
static void subseconds_to_icalprop(icalproperty *prop, bit64 nano)
{
if (!nano) return;
struct buf buf = BUF_INITIALIZER;
buf_printf(&buf, "0.%llu", nano);
/* Truncate trailing zeros */
int n = buf_len(&buf);
const char *b = buf_base(&buf);
while (b[n-1] == '0') n--;
buf_truncate(&buf, n);
set_icalxparam(prop, "SUBSECOND", buf_cstring(&buf), 1);
buf_free(&buf);
}
/* Add or overwrite the datetime property kind in comp. If tz is not NULL, set
* the TZID parameter on the property. Also take care to purge conflicting
* datetime properties such as DTEND and DURATION. */
static icalproperty *insert_icaltimeprop(icalcomponent *comp,
icaltimetype dt,
bit64 nano,
int remove_existing,
enum icalproperty_kind kind) {
icalproperty *prop;
/* Purge existing property. */
if (remove_existing) {
remove_icalprop(comp, kind);
}
/* Resolve DTEND/DURATION conflicts. */
if (kind == ICAL_DTEND_PROPERTY) {
remove_icalprop(comp, ICAL_DURATION_PROPERTY);
} else if (kind == ICAL_DURATION_PROPERTY) {
remove_icalprop(comp, ICAL_DTEND_PROPERTY);
}
/* backwards compatible way to set date or datetime */
icalvalue *val =
dt.is_date ? icalvalue_new_date(dt) : icalvalue_new_datetime(dt);
if (!val) {
syslog(LOG_ERR, "insert_icaltimeprop: invalid time value");
return NULL;
}
/* Set the new property. */
prop = icalproperty_new(kind);
icalproperty_set_value(prop, val);
if (dt.zone && !icaltime_is_utc(dt)) {
icalparameter *param =
icalproperty_get_first_parameter(prop, ICAL_TZID_PARAMETER);
/* XXX libical uses non-const icaltimezone pointer for read-only */
const char *tzid = icaltimezone_get_location((icaltimezone*)dt.zone);
if (param) {
icalparameter_set_tzid(param, tzid);
} else {
icalproperty_add_parameter(prop,icalparameter_new_tzid(tzid));
}
}
if (!dt.is_date) subseconds_to_icalprop(prop, nano);
icalcomponent_add_property(comp, prop);
return prop;
}
static int location_is_endtimezone(json_t *loc)
{
const char *rel = json_string_value(json_object_get(loc, "relativeTo"));
if (!rel) return 0;
return json_object_get(loc, "timeZone") && !strcmp(rel, "end");
}
/* Update the start and end properties of VEVENT comp, as defined by
* the JMAP calendarevent event. */
static void
startend_to_ical(icalcomponent *comp, struct jmap_parser *parser, json_t *event)
{
json_t *jprop;
/* timeZone */
icaltimezone *tzstart = NULL;
jprop = json_object_get(event, "timeZone");
if (json_is_string(jprop)) {
const char *val = json_string_value(jprop);
tzstart = tz_from_tzid(val);
if (!tzstart) {
jmap_parser_invalid(parser, "timeZone");
}
} else if (JNOTNULL(jprop)) {
jmap_parser_invalid(parser, "timeZone");
}
/* Read end timezone */
icaltimezone *tzend = tzstart;
const char *endzone_location_id = NULL;
json_t *locations = json_object_get(event, "locations");
if (json_is_object(locations)) {
json_t *jval;
const char *id;
jmap_parser_push(parser, "locations");
json_object_foreach(locations, id, jval) {
if (!location_is_endtimezone(jval)) {
continue;
}
/* Pick the first location with timeZone and rel=end */
jmap_parser_push(parser, id);
endzone_location_id = id;
json_t *timeZone = json_object_get(jval, "timeZone");
if (json_is_string(timeZone)) {
tzend = tz_from_tzid(json_string_value(timeZone));
if (!tzend || !tzstart) {
jmap_parser_invalid(parser, "timeZone");
}
}
else if (JNOTNULL(jprop)) {
jmap_parser_invalid(parser, "timeZone");
}
jmap_parser_pop(parser);
break;
}
jmap_parser_pop(parser);
} else if (JNOTNULL(locations)) {
jmap_parser_invalid(parser, "locations");
}
/* Read duration */
- struct duration dur = JMAP_DURATION_INITIALIZER;
+ struct jmapical_duration dur = JMAPICAL_DURATION_INITIALIZER;
jprop = json_object_get(event, "duration");
if (json_is_string(jprop)) {
- if (parse_duration(json_string_value(jprop), &dur) < 0) {
+ if (jmapical_duration_from_string(json_string_value(jprop), &dur) < 0) {
jmap_parser_invalid(parser, "duration");
}
} else if (JNOTNULL(jprop)) {
jmap_parser_invalid(parser, "duration");
}
/* Read start */
- struct datetime start = JMAP_DATETIME_INITIALIZER;
+ struct jmapical_datetime start = JMAPICAL_DATETIME_INITIALIZER;
jprop = json_object_get(event, "start");
if (json_is_string(jprop)) {
- if (parse_localdatetime(json_string_value(jprop), &start) < 0) {
+ if (jmapical_localdatetime_from_string(json_string_value(jprop), &start) < 0) {
jmap_parser_invalid(parser, "start");
}
} else {
jmap_parser_invalid(parser, "start");
}
/* Bail out for property errors */
if (json_array_size(parser->invalid))
return;
/* Purge and rebuild start and end */
remove_icalprop(comp, ICAL_DTSTART_PROPERTY);
remove_icalprop(comp, ICAL_DTEND_PROPERTY);
remove_icalprop(comp, ICAL_DURATION_PROPERTY);
/* Add DTSTART */
int is_date = 0;
- if (!tzstart && !tzend && datetime_has_zero_time(&start) && duration_has_zero_time(&dur)) {
+ if (!tzstart && !tzend && jmapical_datetime_has_zero_time(&start) &&
+ jmapical_duration_has_zero_time(&dur)) {
/* Determine if to store DTSTART as DATE type */
is_date = 1;
/* Check recurrence frequency */
json_t *jrrule = json_object_get(event, "recurrenceRule");
if (json_is_object(jrrule)) {
const char *freq = json_string_value(json_object_get(jrrule, "frequency"));
if (!strcmpsafe(freq, "hourly") ||
!strcmpsafe(freq, "minutely") ||
!strcmpsafe(freq, "secondly")) {
is_date = 0;
}
else {
/* Check that all overrides have zero time */
json_t *joverrides = json_object_get(event, "recurrenceOverrides");
const char *recuridval;
json_t *jval;
json_object_foreach(joverrides, recuridval, jval) {
- struct datetime recurid = JMAP_DATETIME_INITIALIZER;
- if ((parse_localdatetime(recuridval, &recurid) >= 0) &&
- !datetime_has_zero_time(&recurid)) {
+ struct jmapical_datetime recurid = JMAPICAL_DATETIME_INITIALIZER;
+ if ((jmapical_localdatetime_from_string(recuridval, &recurid) >= 0) &&
+ !jmapical_datetime_has_zero_time(&recurid)) {
is_date = 0;
break;
}
}
}
}
if (json_object_get(event, "showWithoutTime") == json_false()) {
/* Explicitly set to false. Keep start as floating time. */
is_date = 0;
}
}
struct icaltimetype dtstart = is_date ?
- datetime_to_icaldate(&start) :
- datetime_to_icaltime(&start, tzstart);
+ jmapical_datetime_to_icaldate(&start) :
+ jmapical_datetime_to_icaltime(&start, tzstart);
insert_icaltimeprop(comp, dtstart, start.nano, 1, ICAL_DTSTART_PROPERTY);
if (tzstart != tzend) {
/* Add DTEND */
struct icaldurationtype icaldur = duration_to_icalduration(&dur);
bit64 endnanos = start.nano + dur.nanos;
icaldur.seconds += endnanos / 1000000000;
endnanos %= 1000000000;
icaltimetype dtend = icaltime_add(dtstart, icaldur);
dtend = icaltime_convert_to_zone(dtend, tzend);
icalproperty *prop = insert_icaltimeprop(comp, dtend, endnanos, 1, ICAL_DTEND_PROPERTY);
if (prop) xjmapid_to_ical(prop, endzone_location_id);
} else {
/* Add DURATION */
struct icaldurationtype icaldur = duration_to_icalduration(&dur);
icalproperty *prop = icalproperty_new_duration(icaldur);
subseconds_to_icalprop(prop, dur.nanos);
icalcomponent_add_property(comp, prop);
}
json_t *jshowWithoutTime = json_object_get(event, "showWithoutTime");
if (json_is_boolean(jshowWithoutTime)) {
int show_without_time = json_boolean_value(jshowWithoutTime);
/* Only set in iCalendar if it isn't implied by DTSTART value type */
if ((is_date == 0) != (show_without_time == 0)) {
icalproperty *prop = icalproperty_new(ICAL_X_PROPERTY);
icalproperty_set_x_name(prop, "SHOW-WITHOUT-TIME");
icalvalue *icalval = icalvalue_new_boolean(show_without_time);
icalproperty_set_value(prop, icalval);
icalcomponent_add_property(comp, prop);
}
}
else if (JNOTNULL(jshowWithoutTime)) {
jmap_parser_invalid(parser, "showWithoutTime");
}
}
static void
participant_roles_to_ical(icalproperty *prop,
struct jmap_parser *parser,
json_t *roles,
icalparameter_role ical_role,
int is_replyto)
{
if (!json_object_size(roles)) {
jmap_parser_invalid(parser, "roles");
return;
}
const char *key;
json_t *jval;
jmap_parser_push(parser, "roles");
json_object_foreach(roles, key, jval) {
if (jval != json_true()) {
jmap_parser_invalid(parser, key);
}
}
jmap_parser_pop(parser);
int has_owner = json_object_get(roles, "owner") == json_true();
int has_chair = json_object_get(roles, "chair") == json_true();
int has_attendee = json_object_get(roles, "attendee") == json_true();
size_t xroles_count = json_object_size(roles);
/* Try to map roles to iCalendar without falling back to X-ROLE */
if (has_chair && ical_role == ICAL_ROLE_REQPARTICIPANT) {
/* Can use iCalendar ROLE=CHAIR parameter */
xroles_count--;
}
if (has_owner && is_replyto) {
/* This is the ORGANIZER or its ATTENDEE, which is implicit "owner" */
xroles_count--;
}
if (has_attendee) {
/* Default role for ATTENDEE without X-ROLE is "attendee" */
xroles_count--;
}
if (xroles_count == 0) {
/* No need to set X-ROLE parameters on this ATTENDEE */
if (has_chair) {
icalparameter *param = icalparameter_new_role(ICAL_ROLE_CHAIR);
icalproperty_add_parameter(prop, param);
}
}
else {
/* Map roles to X-ROLE */
json_object_foreach(roles, key, jval) {
/* Try to use standard CHAIR role */
if (!strcasecmp(key, "CHAIR") && ical_role == ICAL_ROLE_REQPARTICIPANT) {
icalparameter *param = icalparameter_new_role(ICAL_ROLE_CHAIR);
icalproperty_add_parameter(prop, param);
} else {
set_icalxparam(prop, JMAPICAL_XPARAM_ROLE, key, 0);
}
}
}
}
static int is_valid_rsvpmethod(const char *s)
{
if (!s) return 0;
size_t i;
for (i = 0; s[i]; i++) {
if (!isascii(s[i]) || !isalpha(s[i])) {
return 0;
}
}
return i > 0;
}
static int
participant_equals(json_t *jpart1, json_t *jpart2)
{
/* Special-case sendTo URI values */
json_t *jsendTo1 = json_object_get(jpart1, "sendTo");
json_t *jsendTo2 = json_object_get(jpart2, "sendTo");
if (jsendTo1 == NULL || jsendTo1 == json_null()) {
json_t *jemail = json_object_get(jpart1, "email");
if (json_is_string(jemail)) {
char *tmp = strconcat("mailto:", json_string_value(jemail), NULL);
json_object_set_new(jpart1, "sendTo", json_pack("{s:s}", "imip", tmp));
free(tmp);
}
jsendTo1 = json_object_get(jpart1, "sendTo");
}
if (jsendTo2 == NULL || jsendTo2 == json_null()) {
json_t *jemail = json_object_get(jpart2, "email");
if (json_is_string(jemail)) {
char *tmp = strconcat("mailto:", json_string_value(jemail), NULL);
json_object_set_new(jpart2, "sendTo", json_pack("{s:s}", "imip", tmp));
free(tmp);
}
jsendTo2 = json_object_get(jpart2, "sendTo");
}
if (json_object_size(jsendTo1) != json_object_size(jsendTo2)) return 0;
if (JNOTNULL(jsendTo1)) {
json_t *juri1;
const char *method;
json_object_foreach(jsendTo1, method, juri1) {
json_t *juri2 = json_object_get(jsendTo2, method);
if (!juri2) return 0;
const char *uri1 = json_string_value(juri1);
const char *uri2 = json_string_value(juri2);
if (!uri1 || !uri2 || !match_uri(uri1, uri2)) return 0;
}
}
json_t *jval1 = json_copy(jpart1);
json_t *jval2 = json_copy(jpart2);
json_object_del(jval1, "sendTo");
json_object_del(jval2, "sendTo");
/* Remove default values */
if (!strcmpsafe(json_string_value(json_object_get(jval1, "name")), ""))
json_object_del(jval1, "name");
if (!strcmpsafe(json_string_value(json_object_get(jval2, "name")), ""))
json_object_del(jval2, "name");
if (!strcmpsafe(json_string_value(json_object_get(jval1, "participationStatus")), "needs-action"))
json_object_del(jval1, "participationStatus");
if (!strcmpsafe(json_string_value(json_object_get(jval2, "participationStatus")), "needs-action"))
json_object_del(jval2, "participationStatus");
if (!strcmpsafe(json_string_value(json_object_get(jval1, "attendance")), "required"))
json_object_del(jval1, "attendance");
if (!strcmpsafe(json_string_value(json_object_get(jval2, "attendance")), "required"))
json_object_del(jval2, "attendance");
if (!json_boolean_value(json_object_get(jval1, "expectReply")))
json_object_del(jval1, "expectReply");
if (!json_boolean_value(json_object_get(jval2, "expectReply")))
json_object_del(jval2, "expectReply");
if (json_integer_value(json_object_get(jval1, "scheduleSequence")) == 0)
json_object_del(jval1, "scheduleSequence");
if (json_integer_value(json_object_get(jval2, "scheduleSequence")) == 0)
json_object_del(jval2, "scheduleSequence");
/* Unify JSON null to NULL */
json_t *jprop;
const char *key;
void *tmp;
json_object_foreach_safe(jval1, tmp, key, jprop) {
if (json_is_null(jprop)) json_object_del(jval1, key);
}
json_object_foreach_safe(jval2, tmp, key, jprop) {
if (json_is_null(jprop)) json_object_del(jval2, key);
}
int is_equal = json_equal(jval1, jval2);
json_decref(jval1);
json_decref(jval2);
return is_equal;
}
static void
participant_to_ical(icalcomponent *comp,
struct jmap_parser *parser,
const char *id,
json_t *jpart,
json_t *participants,
json_t *links,
const char *orga_uri,
hash_table *caladdress_by_participant_id)
{
const char *caladdress = hash_lookup(id, caladdress_by_participant_id);
icalproperty *prop = icalproperty_new_attendee(caladdress);
set_icalxparam(prop, JMAPICAL_XPARAM_ID, id, 1);
icaltimezone *utc = icaltimezone_get_utc_timezone();
icalproperty *orga = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
int is_orga = match_uri(caladdress, orga_uri);
if (is_orga) set_icalxparam(orga, JMAPICAL_XPARAM_ID, id, 1);
/* name */
json_t *jname = json_object_get(jpart, "name");
if (json_is_string(jname)) {
const char *name = json_string_value(jname);
icalproperty_add_parameter(prop, icalparameter_new_cn(name));
if (is_orga) {
icalproperty_add_parameter(orga, icalparameter_new_cn(name));
}
}
else if (JNOTNULL(jname)) {
jmap_parser_invalid(parser, "name");
}
/* sendTo */
json_t *sendTo = json_object_get(jpart, "sendTo");
if (json_object_size(sendTo)) {
jmap_parser_push(parser, "sendTo");
struct buf buf = BUF_INITIALIZER;
/* Only set RSVP URI x-params if not trivial */
int set_rsvp_uris = 0;
if (json_object_size(sendTo) > 1) {
set_rsvp_uris = 1;
}
else {
const char *method = json_object_iter_key(json_object_iter(sendTo));
set_rsvp_uris = strcmp(method, "imip") && strcmp(method, "other");
}
const char *key;
json_t *jval;
/* Process RSVP URIs */
json_object_foreach(sendTo, key, jval) {
if (!is_valid_rsvpmethod(key) || !json_is_string(jval)) {
jmap_parser_invalid(parser, key);
continue;
}
if (!set_rsvp_uris) continue;
buf_setcstr(&buf, key);
buf_putc(&buf, ':');
buf_appendcstr(&buf, json_string_value(jval));
set_icalxparam(prop, JMAPICAL_XPARAM_RSVP_URI, buf_cstring(&buf), 0);
}
buf_free(&buf);
jmap_parser_pop(parser);
}
else if (JNOTNULL(sendTo)) {
jmap_parser_invalid(parser, "sendTo");
}
/* email */
json_t *jemail = json_object_get(jpart, "email");
if (json_is_string(jemail)) {
const char *uri = icalproperty_get_value_as_string(prop);
const char *email = json_string_value(jemail);
if (!match_uri(uri, email)) {
icalproperty_add_parameter(prop, icalparameter_new_email(email));
if (is_orga) {
icalproperty_add_parameter(orga, icalparameter_new_email(email));
}
}
}
else if (JNOTNULL(jemail)) {
jmap_parser_invalid(parser, "email");
}
/* kind */
json_t *kind = json_object_get(jpart, "kind");
if (json_is_string(kind)) {
icalparameter *param = NULL;
char *tmp = ucase(xstrdup(json_string_value(kind)));
icalparameter_cutype cu;
if (!strcmp(tmp, "LOCATION"))
cu = ICAL_CUTYPE_ROOM;
else
cu = icalparameter_string_to_enum(tmp);
switch (cu) {
case ICAL_CUTYPE_INDIVIDUAL:
case ICAL_CUTYPE_GROUP:
case ICAL_CUTYPE_RESOURCE:
case ICAL_CUTYPE_ROOM:
param = icalparameter_new_cutype(cu);
icalproperty_add_parameter(prop, param);
break;
default:
/* ignore */ ;
}
free(tmp);
}
else if (JNOTNULL(kind)) {
jmap_parser_invalid(parser, "kind");
}
/* attendance */
icalparameter_role ical_role = ICAL_ROLE_REQPARTICIPANT;
json_t *attendance = json_object_get(jpart, "attendance");
if (json_is_string(attendance)) {
const char *s = json_string_value(attendance);
if (!strcasecmp(s, "required")) {
ical_role = ICAL_ROLE_REQPARTICIPANT;
}
else if (!strcasecmp(s, "optional")) {
ical_role = ICAL_ROLE_OPTPARTICIPANT;
}
else if (!strcasecmp(s, "none")) {
ical_role = ICAL_ROLE_NONPARTICIPANT;
}
if (ical_role != ICAL_ROLE_REQPARTICIPANT) {
icalproperty_add_parameter(prop, icalparameter_new_role(ical_role));
}
}
else if (JNOTNULL(attendance)) {
jmap_parser_invalid(parser, "attendance");
}
/* roles */
json_t *roles = json_object_get(jpart, "roles");
if (json_object_size(roles)) {
participant_roles_to_ical(prop, parser, roles, ical_role, is_orga);
}
else if (roles) {
jmap_parser_invalid(parser, "roles");
}
/* locationId */
json_t *locationId = json_object_get(jpart, "locationId");
if (json_is_string(locationId)) {
const char *s = json_string_value(locationId);
set_icalxparam(prop, JMAPICAL_XPARAM_LOCATIONID, s, 1);
}
else if (JNOTNULL(locationId)) {
jmap_parser_invalid(parser, "locationId");
}
/* participationStatus */
icalparameter_partstat ps = ICAL_PARTSTAT_NONE;
json_t *participationStatus = json_object_get(jpart, "participationStatus");
if (json_is_string(participationStatus)) {
char *tmp = ucase(xstrdup(json_string_value(participationStatus)));
ps = icalparameter_string_to_enum(tmp);
switch (ps) {
case ICAL_PARTSTAT_NEEDSACTION:
case ICAL_PARTSTAT_ACCEPTED:
case ICAL_PARTSTAT_DECLINED:
case ICAL_PARTSTAT_TENTATIVE:
break;
default:
jmap_parser_invalid(parser, "participationStatus");
ps = ICAL_PARTSTAT_NONE;
}
free(tmp);
}
else if (JNOTNULL(participationStatus)) {
jmap_parser_invalid(parser, "participationStatus");
}
if (ps != ICAL_PARTSTAT_NONE) {
icalproperty_add_parameter(prop, icalparameter_new_partstat(ps));
}
/* expectReply */
json_t *expectReply = json_object_get(jpart, "expectReply");
if (json_is_boolean(expectReply)) {
icalparameter *param = NULL;
if (expectReply == json_true()) {
param = icalparameter_new_rsvp(ICAL_RSVP_TRUE);
if (ps == ICAL_PARTSTAT_NONE) {
icalproperty_add_parameter(prop,
icalparameter_new_partstat(ICAL_PARTSTAT_NEEDSACTION));
}
}
else {
param = icalparameter_new_rsvp(ICAL_RSVP_FALSE);
}
icalproperty_add_parameter(prop, param);
}
else if (JNOTNULL(expectReply)) {
jmap_parser_invalid(parser, "expectReply");
}
/* delegatedTo */
json_t *delegatedTo = json_object_get(jpart, "delegatedTo");
if (json_object_size(delegatedTo)) {
const char *id;
json_t *jval;
json_object_foreach(delegatedTo, id, jval) {
json_t *delegatee = json_object_get(participants, id);
if (is_valid_jmapid(id) && delegatee && jval == json_true()) {
const char *uri = hash_lookup(id, caladdress_by_participant_id);
if (uri) {
icalproperty_add_parameter(prop, icalparameter_new_delegatedto(uri));
}
}
else {
jmap_parser_push(parser, "delegatedTo");
jmap_parser_invalid(parser, id);
jmap_parser_pop(parser);
}
}
}
else if (JNOTNULL(delegatedTo)) {
jmap_parser_invalid(parser, "delegatedTo");
}
/* delegatedFrom */
json_t *delegatedFrom = json_object_get(jpart, "delegatedFrom");
if (json_object_size(delegatedFrom)) {
const char *id;
json_t *jval;
json_object_foreach(delegatedFrom, id, jval) {
json_t *delegator = json_object_get(participants, id);
if (is_valid_jmapid(id) && delegator && jval == json_true()) {
const char *uri = hash_lookup(id, caladdress_by_participant_id);
if (uri) {
icalproperty_add_parameter(prop, icalparameter_new_delegatedfrom(uri));
}
}
else {
jmap_parser_push(parser, "delegatedFrom");
jmap_parser_invalid(parser, id);
jmap_parser_pop(parser);
}
}
}
else if (JNOTNULL(delegatedFrom)) {
jmap_parser_invalid(parser, "delegatedFrom");
}
/* memberOf */
json_t *memberOf = json_object_get(jpart, "memberOf");
if (json_object_size(memberOf)) {
const char *id;
json_t *jval;
json_object_foreach(memberOf, id, jval) {
json_t *group = json_object_get(participants, id);
if (is_valid_jmapid(id) && group && jval == json_true()) {
const char *uri = hash_lookup(id, caladdress_by_participant_id);
if (uri) {
icalproperty_add_parameter(prop, icalparameter_new_member(uri));
}
}
else {
jmap_parser_push(parser, "memberOf");
jmap_parser_invalid(parser, id);
jmap_parser_pop(parser);
}
}
}
else if (JNOTNULL(memberOf)) {
jmap_parser_invalid(parser, "memberOf");
}
/* linkIds */
json_t *linkIds = json_object_get(jpart, "linkIds");
if (json_object_size(linkIds)) {
const char *id;
json_t *jval;
json_object_foreach(linkIds, id, jval) {
if (!is_valid_jmapid(id) || !json_object_get(links, id) || jval != json_true()) {
jmap_parser_push(parser, "linkIds");
jmap_parser_invalid(parser, id);
jmap_parser_pop(parser);
continue;
}
set_icalxparam(prop, JMAPICAL_XPARAM_LINKID, id, 0);
}
}
else if (JNOTNULL(linkIds)) {
jmap_parser_invalid(parser, "linkIds");
}
/* scheduleSequence */
json_t *scheduleSequence = json_object_get(jpart, "scheduleSequence");
if (json_is_integer(scheduleSequence) && json_integer_value(scheduleSequence) >= 0) {
struct buf buf = BUF_INITIALIZER;
buf_printf(&buf, "%lld", json_integer_value(scheduleSequence));
set_icalxparam(prop, JMAPICAL_XPARAM_SEQUENCE, buf_cstring(&buf), 0);
buf_free(&buf);
}
else if (JNOTNULL(scheduleSequence)) {
jmap_parser_invalid(parser, "scheduleSequence");
}
/* scheduleUpdated */
json_t *scheduleUpdated = json_object_get(jpart, "scheduleUpdated");
if (json_is_string(scheduleUpdated)) {
- struct datetime tstamp = JMAP_DATETIME_INITIALIZER;
- if (parse_utcdatetime(json_string_value(scheduleUpdated), &tstamp) >= 0) {
- icaltimetype icaltstamp = datetime_to_icaltime(&tstamp, utc);
+ struct jmapical_datetime tstamp = JMAPICAL_DATETIME_INITIALIZER;
+ if (jmapical_utcdatetime_from_string(json_string_value(scheduleUpdated), &tstamp) >= 0) {
+ icaltimetype icaltstamp = jmapical_datetime_to_icaltime(&tstamp, utc);
char *tmp = icaltime_as_ical_string_r(icaltstamp);
set_icalxparam(prop, JMAPICAL_XPARAM_DTSTAMP, tmp, 0);
free(tmp);
}
else {
jmap_parser_invalid(parser, "scheduleSequence");
}
}
else if (JNOTNULL(scheduleUpdated)) {
jmap_parser_invalid(parser, "scheduleSequence");
}
/* participationComment */
json_t *jcomment = json_object_get(jpart, "participationComment");
if (json_is_string(jcomment)) {
const char *comment = json_string_value(jcomment);
if (*comment) {
set_icalxparam(prop, JMAPICAL_XPARAM_COMMENT, comment, 1);
}
}
else if (JNOTNULL(jcomment)) {
jmap_parser_invalid(parser, "participationComment");
}
if (is_orga) {
/* We might get away by not creating an ATTENDEE, if the
* participant is owner of the event and all its JSCalendar
* properties can be mapped to the ORGANIZER property. */
json_t *jorga = participant_from_icalorganizer(orga);
if (participant_equals(jorga, jpart)) {
icalproperty_free(prop);
prop = NULL;
}
json_decref(jorga);
if (!prop) return;
}
icalcomponent_add_property(comp, prop);
}
/* Create or update the ORGANIZER and ATTENDEEs in the VEVENT component comp as
* defined by the participants and replyTo property. */
static void
participants_to_ical(icalcomponent *comp, struct jmap_parser *parser, json_t *event)
{
/* Purge existing ATTENDEEs and ORGANIZER */
remove_icalprop(comp, ICAL_ATTENDEE_PROPERTY);
remove_icalprop(comp, ICAL_ORGANIZER_PROPERTY);
json_t *jval = NULL;
const char *key = NULL;
/* If participants are set, replyTo must be set */
json_t *replyTo = json_object_get(event, "replyTo");
if (JNOTNULL(replyTo) && !json_object_size(replyTo)) {
jmap_parser_invalid(parser, "replyTo");
}
json_t *participants = json_object_get(event, "participants");
if (JNOTNULL(participants) && !json_object_size(participants)) {
jmap_parser_invalid(parser, "participants");
}
if (JNOTNULL(replyTo) != JNOTNULL(participants)) {
jmap_parser_invalid(parser, "replyTo");
jmap_parser_invalid(parser, "participants");
return;
}
else if (!JNOTNULL(replyTo)) return;
/* OK, there's both replyTo and participants set. */
/* Parse replyTo */
jmap_parser_push(parser, "replyTo");
json_object_foreach(replyTo, key, jval) {
if (!is_valid_rsvpmethod(key) || !json_is_string(jval)) {
jmap_parser_invalid(parser, key);
continue;
}
}
jmap_parser_pop(parser);
/* Map participant ids to their iCalendar CALADDRESS */
hash_table caladdress_by_participant_id = HASH_TABLE_INITIALIZER;
construct_hash_table(&caladdress_by_participant_id, json_object_size(participants)+1, 0);
json_object_foreach(participants, key, jval) {
if (!is_valid_jmapid(key)) continue;
char *caladdress = NULL;
json_t *sendTo = json_object_get(jval, "sendTo");
if (json_object_get(sendTo, "imip")) {
caladdress = xstrdup(json_string_value(json_object_get(sendTo, "imip")));
}
else if (json_object_get(sendTo, "other")) {
caladdress = xstrdup(json_string_value(json_object_get(sendTo, "other")));
}
else if (json_object_size(sendTo)) {
const char *anymethod = json_object_iter_key(json_object_iter(sendTo));
caladdress = xstrdup(json_string_value(json_object_get(sendTo, anymethod)));
}
else if (json_object_get(jval, "email")) {
caladdress = mailaddr_to_uri(json_string_value(json_object_get(jval, "email")));
}
if (!caladdress) continue; /* reported later as error */
hash_insert(key, caladdress, &caladdress_by_participant_id);
}
/* Pick the ORGANIZER URI */
const char *orga_method = NULL;
if (json_object_get(replyTo, "imip")) {
orga_method = "imip";
}
else if (json_object_get(replyTo, "other")) {
orga_method = "other";
}
else {
orga_method = json_object_iter_key(json_object_iter(replyTo));
}
const char *orga_uri = json_string_value(json_object_get(replyTo, orga_method));
/* Create the ORGANIZER property */
icalproperty *orga = icalproperty_new_organizer(orga_uri);
/* Keep track of the RSVP URIs and their method */
if (json_object_size(replyTo) > 1 || (strcmp(orga_method, "imip") && strcmp(orga_method, "other"))) {
struct buf buf = BUF_INITIALIZER;
json_object_foreach(replyTo, key, jval) {
buf_setcstr(&buf, key);
buf_putc(&buf, ':');
buf_appendcstr(&buf, json_string_value(jval));
set_icalxparam(orga, JMAPICAL_XPARAM_RSVP_URI, buf_cstring(&buf), 0);
}
buf_free(&buf);
}
icalcomponent_add_property(comp, orga);
/* Process participants */
jmap_parser_push(parser, "participants");
json_t *links = json_object_get(event, "links");
json_object_foreach(participants, key, jval) {
jmap_parser_push(parser, key);
if (!is_valid_jmapid(key)) {
jmap_parser_invalid(parser, NULL);
jmap_parser_pop(parser);
continue;
}
validate_type(parser, jval, "Participant");
const char *caladdress = hash_lookup(key, &caladdress_by_participant_id);
if (!caladdress) {
jmap_parser_invalid(parser, "sendTo");
jmap_parser_invalid(parser, "email");
jmap_parser_pop(parser);
continue;
}
/* Map participant to iCalendar */
participant_to_ical(comp, parser, key, jval, participants, links,
orga_uri, &caladdress_by_participant_id);
jmap_parser_pop(parser);
}
jmap_parser_pop(parser);
free_hash_table(&caladdress_by_participant_id, free);
}
static int is_valid_regrel(const char *rel)
{
// RFC 8288, section 3.3, reg-rel-type:
const char *p = rel;
while ((('a' <= *p) && (*p <= 'z')) ||
(('0' <= *p) && (*p <= '9')) ||
((*p == '.') && p > rel) ||
((*p == '-') && p > rel)) {
p++;
}
return *p == '\0' && p > rel;
}
static void
links_to_ical(icalcomponent *comp, struct jmap_parser *parser, json_t *links)
{
icalproperty *prop;
struct buf buf = BUF_INITIALIZER;
/* Purge existing attachments */
remove_icalprop(comp, ICAL_ATTACH_PROPERTY);
remove_icalprop(comp, ICAL_URL_PROPERTY);
jmap_parser_push(parser, "links");
const char *id;
json_t *link;
json_object_foreach(links, id, link) {
const char *href = NULL;
const char *contenttype = NULL;
const char *title = NULL;
const char *rel = NULL;
const char *cid = NULL;
const char *display = NULL;
json_int_t size = -1;
json_t *jprop = NULL;
jmap_parser_push(parser, id);
if (!is_valid_jmapid(id)) {
jmap_parser_invalid(parser, id);
jmap_parser_pop(parser);
continue;
}
validate_type(parser, link, "Link");
/* href */
href = json_string_value(json_object_get(link, "href"));
if (!href || !strlen(href)) {
jmap_parser_invalid(parser, "href");
href = NULL;
}
/* contentType */
jprop = json_object_get(link, "contentType");
if (json_is_string(jprop)) {
contenttype = json_string_value(jprop);
}
else if (JNOTNULL(jprop)) {
jmap_parser_invalid(parser, "type");
}
/* title */
jprop = json_object_get(link, "title");
if (json_is_string(jprop)) {
title = json_string_value(jprop);
}
else if (JNOTNULL(jprop)) {
jmap_parser_invalid(parser, "title");
}
/* cid */
jprop = json_object_get(link, "cid");
if (json_is_string(jprop)) {
cid = json_string_value(jprop);
}
else if (JNOTNULL(jprop)) {
jmap_parser_invalid(parser, "cid");
}
/* display */
jprop = json_object_get(link, "display");
if (json_is_string(jprop)) {
display = json_string_value(jprop);
}
else if (JNOTNULL(jprop)) {
jmap_parser_invalid(parser, "display");
}
/* rel */
jprop = json_object_get(link, "rel");
if (json_is_string(jprop)) {
rel = json_string_value(jprop);
if (!is_valid_regrel(rel)) {
jmap_parser_invalid(parser, "rel");
}
}
else if (JNOTNULL(jprop)) {
jmap_parser_invalid(parser, "rel");
}
/* size */
jprop = json_object_get(link, "size");
if (json_is_integer(jprop)) {
size = json_integer_value(jprop);
if (size < 0) {
jmap_parser_invalid(parser, "size");
}
} else if (JNOTNULL(jprop)) {
jmap_parser_invalid(parser, "size");
}
jmap_parser_pop(parser);
if (href && !json_array_size(parser->invalid)) {
/* Build iCalendar property */
if (!strcmpsafe(rel, "describedby") &&
!icalcomponent_get_first_property(comp, ICAL_URL_PROPERTY) &&
json_object_size(link) == 2) {
prop = icalproperty_new(ICAL_URL_PROPERTY);
icalproperty_set_value(prop, icalvalue_new_uri(href));
}
else {
icalattach *icalatt = icalattach_new_from_url(href);
prop = icalproperty_new_attach(icalatt);
icalattach_unref(icalatt);
}
/* contentType */
if (contenttype) {
icalproperty_add_parameter(prop,
icalparameter_new_fmttype(contenttype));
}
/* title */
if (title) {
set_icalxparam(prop, JMAPICAL_XPARAM_TITLE, title, 1);
}
/* cid */
if (cid) set_icalxparam(prop, JMAPICAL_XPARAM_CID, cid, 1);
/* size */
if (size >= 0) {
buf_printf(&buf, "%"JSON_INTEGER_FORMAT, size);
icalproperty_add_parameter(prop,
icalparameter_new_size(buf_cstring(&buf)));
buf_reset(&buf);
}
/* rel */
if (rel && strcmp(rel, "enclosure"))
set_icalxparam(prop, JMAPICAL_XPARAM_REL, rel, 1);
/* Set custom id */
set_icalxparam(prop, JMAPICAL_XPARAM_ID, id, 1);
/* display */
if (display) set_icalxparam(prop, JMAPICAL_XPARAM_DISPLAY, display, 1);
/* Add ATTACH property. */
icalcomponent_add_property(comp, prop);
}
buf_free(&buf);
}
jmap_parser_pop(parser);
}
static void
description_to_ical(icalcomponent *comp, struct jmap_parser *parser, json_t *jsevent)
{
remove_icalprop(comp, ICAL_DESCRIPTION_PROPERTY);
const char *desc = NULL;
json_t *jprop = json_object_get(jsevent, "description");
if (json_is_string(jprop)) {
desc = json_string_value(jprop);
}
else if JNOTNULL(jprop) {
jmap_parser_invalid(parser, "description");
}
jprop = json_object_get(jsevent, "descriptionContentType");
if (json_is_string(jprop)) {
const char *content_type = json_string_value(jprop);
/* FIXME
* We'd like to support HTML descriptions, but with iCalendar being
* our storage format there really isn't a good way to deal with
* that. We can't rely on iCalendar clients correctly handling the
* ALTREP parameters on DESCRIPTION, and we don't want to make the
* CalDAV PUT code deal with comparing old vs new descriptions to
* try figuring out what the client did.
* This should become more sane to handle if we start using
* JSCalendar for storage.
*/
if (content_type && strcasecmp(content_type, "TEXT/PLAIN")) {
jmap_parser_invalid(parser, "descriptionContentType");
}
}
else if JNOTNULL(jprop) {
jmap_parser_invalid(parser, "descriptionContentType");
}
if (desc && strlen(desc)) icalcomponent_set_description(comp, desc);
}
/* Create or update the VALARMs in the VEVENT component comp as defined by the
* JMAP alerts. */
static void
alerts_to_ical(icalcomponent *comp, struct jmap_parser *parser, json_t *alerts)
{
icalcomponent *alarm, *next;
icaltimezone *utc = icaltimezone_get_utc_timezone();
/* Purge all VALARMs. */
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 (!JNOTNULL(alerts)) {
return;
}
const char *id;
json_t *alert;
jmap_parser_push(parser, "alerts");
json_object_foreach(alerts, id, alert) {
icalproperty *prop;
icalparameter *param;
if (!is_valid_jmapid(id)) {
jmap_parser_invalid(parser, id);
continue;
}
jmap_parser_push(parser, id);
validate_type(parser, alert, "Alert");
alarm = icalcomponent_new_valarm();
icalcomponent_set_uid(alarm, id);
/* trigger */
struct icaltriggertype trigger = {
icaltime_null_time(), icaldurationtype_null_duration()
};
json_t *jtrigger = json_object_get(alert, "trigger");
if (json_is_object(jtrigger)) {
const char *triggertype = json_string_value(json_object_get(jtrigger, "@type"));
if (!strcmpsafe(triggertype, "OffsetTrigger")) {
jmap_parser_push(parser, "trigger");
/* offset */
- struct duration offset = JMAP_DURATION_INITIALIZER;
+ struct jmapical_duration offset = JMAPICAL_DURATION_INITIALIZER;
json_t *joffset = json_object_get(jtrigger, "offset");
if (json_is_string(joffset)) {
- if (parse_duration(json_string_value(joffset), &offset) < 0) {
+ if (jmapical_duration_from_string(json_string_value(joffset), &offset) < 0) {
jmap_parser_invalid(parser, "offset");
}
} else {
jmap_parser_invalid(parser, "offset");
}
/* relativeTo */
icalparameter_related rel = ICAL_RELATED_START;
json_t *jrelativeTo = json_object_get(jtrigger, "relativeTo");
if (json_is_string(jrelativeTo)) {
const char *val = json_string_value(jrelativeTo);
if (!strcmp(val, "start")) {
rel = ICAL_RELATED_START;
} else if (!strcmp(val, "end")) {
rel = ICAL_RELATED_END;
} else {
jmap_parser_invalid(parser, "relativeTo");
}
} else if (JNOTNULL(jrelativeTo)) {
jmap_parser_invalid(parser, "relativeTo");
}
jmap_parser_pop(parser);
/* Add offset trigger */
trigger.duration = duration_to_icalduration(&offset);
prop = icalproperty_new_trigger(trigger);
subseconds_to_icalprop(prop, offset.nanos);
param = icalparameter_new_related(rel);
icalproperty_add_parameter(prop, param);
icalcomponent_add_property(alarm, prop);
}
else if (!strcmpsafe(triggertype, "AbsoluteTrigger")) {
jmap_parser_push(parser, "trigger");
json_t *jwhen = json_object_get(jtrigger, "when");
- struct datetime when = JMAP_DATETIME_INITIALIZER;
+ struct jmapical_datetime when = JMAPICAL_DATETIME_INITIALIZER;
if (json_is_string(jwhen) &&
- parse_utcdatetime(json_string_value(jwhen), &when) >= 0) {
+ jmapical_utcdatetime_from_string(json_string_value(jwhen), &when) >= 0) {
/* Add absolute trigger */
- trigger.time = datetime_to_icaltime(&when, utc);
+ trigger.time = jmapical_datetime_to_icaltime(&when, utc);
prop = icalproperty_new_trigger(trigger);
subseconds_to_icalprop(prop, when.nano);
icalcomponent_add_property(alarm, prop);
}
else jmap_parser_invalid(parser, "when");
jmap_parser_pop(parser);
}
else {
/* XXX should preserve unknown triggers */
}
}
else jmap_parser_invalid(parser, "trigger");
/* acknowledged */
json_t *jacknowledged = json_object_get(alert, "acknowledged");
if (json_is_string(jacknowledged)) {
- struct datetime acktime = JMAP_DATETIME_INITIALIZER;
- if (parse_utcdatetime(json_string_value(jacknowledged), &acktime) >= 0) {
- prop = icalproperty_new_acknowledged(datetime_to_icaltime(&acktime, utc));
+ struct jmapical_datetime acktime = JMAPICAL_DATETIME_INITIALIZER;
+ if (jmapical_utcdatetime_from_string(json_string_value(jacknowledged), &acktime) >= 0) {
+ prop = icalproperty_new_acknowledged(jmapical_datetime_to_icaltime(&acktime, utc));
subseconds_to_icalprop(prop, acktime.nano);
icalcomponent_add_property(alarm, prop);
} else {
jmap_parser_invalid(parser, "acknowledged");
}
} else if (JNOTNULL(jacknowledged)) {
jmap_parser_invalid(parser, "acknowledged");
}
/* action */
icalproperty_action action = ICAL_ACTION_DISPLAY;
json_t *jaction = json_object_get(alert, "action");
if (json_is_string(jaction)) {
const char *val = json_string_value(jaction);
if (!strcmp(val, "email")) {
action = ICAL_ACTION_EMAIL;
} else if (!strcmp(val, "display")) {
action = ICAL_ACTION_DISPLAY;
} else {
jmap_parser_invalid(parser, "action");
}
} else if (JNOTNULL(jaction)) {
jmap_parser_invalid(parser, "action");
}
prop = icalproperty_new_action(action);
icalcomponent_add_property(alarm, prop);
/* relatedTo */
json_t *jrelatedto = json_object_get(alert, "relatedTo");
if (json_is_object(jrelatedto)) {
relatedto_to_ical(alarm, parser, jrelatedto);
}
else if (JNOTNULL(jrelatedto)) {
jmap_parser_invalid(parser, "relatedTo");
}
if (action == ICAL_ACTION_EMAIL) {
/* ATTENDEE */
const char *annotname = DAV_ANNOT_NS "<" XML_NS_CALDAV ">calendar-user-address-set";
char *mailboxname = caldav_mboxname(httpd_userid, NULL);
struct buf buf = BUF_INITIALIZER;
int r = annotatemore_lookupmask(mailboxname, annotname, httpd_userid, &buf);
char *recipient = NULL;
if (!r && buf.len > 7 && !strncasecmp(buf_cstring(&buf), "mailto:", 7)) {
recipient = buf_release(&buf);
} else {
recipient = strconcat("mailto:", httpd_userid, NULL);
}
icalcomponent_add_property(alarm, icalproperty_new_attendee(recipient));
free(recipient);
buf_free(&buf);
free(mailboxname);
/* SUMMARY */
const char *summary = icalcomponent_get_summary(comp);
if (!summary) summary = "Your event alert";
icalcomponent_add_property(alarm, icalproperty_new_summary(summary));
}
/* DESCRIPTION is required for both email and display */
const char *description = icalcomponent_get_description(comp);
if (!description) description = "";
icalcomponent_add_property(alarm, icalproperty_new_description(description));
icalcomponent_add_component(comp, alarm);
jmap_parser_pop(parser);
}
jmap_parser_pop(parser);
}
/* 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 recurrence_byX_to_ical(json_t *rrule,
struct jmap_parser *parser,
const char *fieldName,
struct buf *icalbuf,
const char *tag,
int lower,
int upper,
int allow_zero)
{
json_t *byX = json_object_get(rrule, fieldName);
if (!json_array_size(byX)) {
if (JNOTNULL(byX) && !json_is_array(byX)) {
jmap_parser_invalid(parser, fieldName);
}
return;
}
/* Convert the array. */
buf_printf(icalbuf, ";%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 && !allow_zero && !val) {
err = 1;
}
if (!err && ((lower != INT_MIN && val < lower) || (upper != INT_MAX && val > upper))) {
err = 2;
}
if (err) {
jmap_parser_push_index(parser, fieldName, i, NULL);
jmap_parser_invalid(parser, NULL);
jmap_parser_pop(parser);
continue;
}
/* Convert the byX value to ical. */
if (i) buf_printf(icalbuf, "%c", ',');
buf_printf(icalbuf, "%d", val);
}
}
/* Create or overwrite the RRULE in the VEVENT component comp as defined by the
* JMAP recurrence. */
static void
recurrence_to_ical(icalcomponent *comp, struct jmap_parser *parser, json_t *rrule)
{
struct buf buf = BUF_INITIALIZER;
/* Purge existing RRULE. */
icalproperty *prop, *next;
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 (!JNOTNULL(rrule)) {
return;
}
jmap_parser_push(parser, "recurrenceRule");
validate_type(parser, rrule, "RecurrenceRule");
/* frequency */
const char *freq = NULL;
json_t *jprop = json_object_get(rrule, "frequency");
if (json_is_string(jprop)) {
const char *val = json_string_value(jprop);
if (!strcasecmp(val, "yearly") ||
!strcasecmp(val, "monthly") ||
!strcasecmp(val, "weekly") ||
!strcasecmp(val, "daily") ||
!strcasecmp(val, "hourly") ||
!strcasecmp(val, "minutely") ||
!strcasecmp(val, "secondly")) {
freq = val;
}
}
if (freq) {
buf_printf(&buf, "FREQ=%s", freq);
} else {
jmap_parser_invalid(parser, "frequency");
}
/* interval */
int interval = 1;
jprop = json_object_get(rrule, "interval");
if (json_is_integer(jprop)) {
interval = json_integer_value(jprop);
if (interval > 1) {
buf_printf(&buf, ";INTERVAL=%d", interval);
} else if (interval < 1) {
jmap_parser_invalid(parser, "interval");
}
}
/* skip */
char *skip = NULL;
jprop = json_object_get(rrule, "skip");
if (json_is_string(jprop)) {
skip = xstrdup(json_string_value(jprop));
ucase(skip);
} else if (JNOTNULL(jprop)) {
jmap_parser_invalid(parser, "skip");
}
/* rscale */
jprop = json_object_get(rrule, "rscale");
if (json_is_string(jprop)) {
char *rscale = xstrdup(json_string_value(jprop));
ucase(rscale);
/* Only include RSCALE/SKIP when required to not break legacy clients */
if (strcmp(rscale, "GREGORIAN") || (skip && strcmp(skip, "OMIT"))) {
buf_printf(&buf, ";RSCALE=%s", rscale);
if (skip) buf_printf(&buf, ";SKIP=%s", skip);
}
free(rscale);
} else if (JNOTNULL(jprop)) {
jmap_parser_invalid(parser, "rscale");
}
free(skip);
/* firstDayOfWeek */
jprop = json_object_get(rrule, "firstDayOfWeek");
if (json_is_string(jprop)) {
char *day = xstrdup(json_string_value(jprop));
ucase(day);
if (icalrecur_string_to_weekday(day) != ICAL_NO_WEEKDAY) {
buf_printf(&buf, ";WKST=%s", day);
} else {
jmap_parser_invalid(parser, "firstDayOfWeek");
}
free(day);
} else if (JNOTNULL(jprop)) {
jmap_parser_invalid(parser, "firstDayOfWeek");
}
/* byDay */
json_t *byday = json_object_get(rrule, "byDay");
if (json_array_size(byday) > 0) {
size_t i;
json_t *bd;
jmap_parser_push(parser, "byDay");
buf_appendcstr(&buf, ";BYDAY=");
json_array_foreach(byday, i, bd) {
char *day = NULL;
json_int_t nth = 0;
jmap_parser_push_index(parser, "byDay", i, NULL);
/* day */
day = xstrdupnull(json_string_value(json_object_get(bd, "day")));
if (day) {
ucase(day);
if (icalrecur_string_to_weekday(day) == ICAL_NO_WEEKDAY) {
free(day);
day = NULL;
}
}
if (!day) jmap_parser_invalid(parser, "day");
/* nthOfPeriod */
json_t *jnth = json_object_get(bd, "nthOfPeriod");
if (json_is_integer(jnth)) {
nth = json_integer_value(jnth);
}
else if (JNOTNULL(jnth)) {
jmap_parser_invalid(parser, "nthOfPeriod");
}
/* Append day */
if (!json_array_size(parser->invalid)) {
if (i > 0) buf_appendcstr(&buf, ",");
if (nth) buf_printf(&buf, "%+"JSON_INTEGER_FORMAT, nth);
buf_appendcstr(&buf, day);
}
free(day);
jmap_parser_pop(parser);
}
} else if (byday) {
jmap_parser_invalid(parser, "byDay");
}
/* byMonth */
json_t *bymonth = json_object_get(rrule, "byMonth");
if (json_is_array(bymonth)) {
size_t i;
json_t *jval;
buf_printf(&buf, ";BYMONTH=");
json_array_foreach(bymonth, i, jval) {
const char *s = json_string_value(jval);
jmap_parser_push_index(parser, "byMonth", i, NULL);
if (!s) {
jmap_parser_invalid(parser, NULL);
jmap_parser_pop(parser);
continue;
}
int val;
char leap = 0, dummy = 0;
int matched = sscanf(s, "%2d%c%c", &val, &leap, &dummy);
if (matched < 1 || matched > 2 || (leap && leap != 'L') || val < 1) {
jmap_parser_invalid(parser, NULL);
jmap_parser_pop(parser);
continue;
}
if (i) buf_putc(&buf, ',');
buf_printf(&buf, "%d", val);
if (leap) buf_putc(&buf, 'L');
jmap_parser_pop(parser);
}
}
else if (JNOTNULL(bymonth)) {
jmap_parser_invalid(parser, "byMonth");
}
/* byYearDay */
recurrence_byX_to_ical(rrule, parser, "byYearDay", &buf, "BYYEARDAY", -366, 366, 0);
/* byWeekNo */
recurrence_byX_to_ical(rrule, parser, "byWeekNo", &buf, "BYWEEKNO", -53, 53, 0);
/* byMonthDay */
recurrence_byX_to_ical(rrule, parser, "byMonthDay", &buf, "BYMONTHDAY", -31, 31, 0);
/* byHour */
recurrence_byX_to_ical(rrule, parser, "byHour", &buf, "BYHOUR", 0, 23, 1);
/* byMinute */
recurrence_byX_to_ical(rrule, parser, "byMinute", &buf, "BYMINUTE", 0, 59, 1);
/* bySecond */
recurrence_byX_to_ical(rrule, parser, "bySecond", &buf, "BYSECOND", 0, 59, 1);
/* bySetPosition */
recurrence_byX_to_ical(rrule, parser, "bySetPosition", &buf,"BYSETPOS", INT_MIN, INT_MAX, 1);
if (json_object_get(rrule, "count") && json_object_get(rrule, "until")) {
jmap_parser_invalid(parser, "count");
jmap_parser_invalid(parser, "until");
}
/* count */
jprop = json_object_get(rrule, "count");
if (json_is_integer(jprop)) {
int count = json_integer_value(jprop);
if (count > 0 && !json_object_get(rrule, "until")) {
buf_printf(&buf, ";COUNT=%d", count);
} else {
jmap_parser_invalid(parser, "count");
}
} else if (JNOTNULL(jprop)) {
jmap_parser_invalid(parser, "count");
}
/* until */
jprop = json_object_get(rrule, "until");
if (json_is_string(jprop)) {
- struct datetime until = JMAP_DATETIME_INITIALIZER;
- if (parse_localdatetime(json_string_value(jprop), &until) >= 0) {
+ struct jmapical_datetime until = JMAPICAL_DATETIME_INITIALIZER;
+ if (jmapical_localdatetime_from_string(json_string_value(jprop), &until) >= 0) {
int is_date = icalcomponent_get_dtstart(comp).is_date;
icaltimezone *tzstart = tz_from_tzid(tzid_from_ical(comp, ICAL_DTSTART_PROPERTY));
icaltimetype untilutc;
/* XXX we don't set SUBSECOND on RRULEs, because clients such
* as iOS reject the whole RRULE for unknown rrule fields */
if (is_date) {
- untilutc = datetime_to_icaldate(&until);
+ untilutc = jmapical_datetime_to_icaldate(&until);
}
else {
- icaltimetype untillocal = datetime_to_icaltime(&until, tzstart);
+ icaltimetype untillocal = jmapical_datetime_to_icaltime(&until, tzstart);
icaltimezone *utc = icaltimezone_get_utc_timezone();
untilutc = icaltime_convert_to_zone(untillocal, utc);
}
buf_printf(&buf, ";UNTIL=%s", icaltime_as_ical_string(untilutc));
} else {
jmap_parser_invalid(parser, "until");
}
} else if (JNOTNULL(jprop)) {
jmap_parser_invalid(parser, "until");
}
if (!json_array_size(parser->invalid)) {
/* Add RRULE to component */
struct icalrecurrencetype rt =
icalrecurrencetype_from_string(buf_ucase(&buf));
if (rt.freq != ICAL_NO_RECURRENCE) {
icalcomponent_add_property(comp, icalproperty_new_rrule(rt));
} else {
syslog(LOG_ERR, "jmap_ical: generated bogus RRULE: %s", buf_cstring(&buf));
jmap_parser_invalid(parser, NULL);
}
}
jmap_parser_pop(parser);
buf_free(&buf);
}
/* Create or overwrite JMAP keywords in comp */
static void
keywords_to_ical(icalcomponent *comp, struct jmap_parser *parser, json_t *keywords)
{
icalproperty *prop, *next;
/* Purge existing keywords from component */
for (prop = icalcomponent_get_first_property(comp, ICAL_CATEGORIES_PROPERTY);
prop;
prop = next) {
next = icalcomponent_get_next_property(comp, ICAL_CATEGORIES_PROPERTY);
icalcomponent_remove_property(comp, prop);
icalproperty_free(prop);
}
/* Add keywords */
json_t *jval;
const char *keyword;
json_object_foreach(keywords, keyword, jval) {
if (jval != json_true()) {
jmap_parser_push(parser, "keywords");
jmap_parser_invalid(parser, keyword);
jmap_parser_pop(parser);
continue;
}
// FIXME known bug: libical doesn't properly
// handle multi-values separated by comma,
// if a single entry contains a comma.
prop = icalproperty_new_categories(keyword);
icalcomponent_add_property(comp, prop);
}
}
/* Create or overwrite JMAP relatedTo in comp */
static void
relatedto_to_ical(icalcomponent *comp, struct jmap_parser *parser, json_t *relatedTo)
{
icalproperty *prop, *next;
icalparameter *param;
/* Purge existing relatedTo properties from component */
for (prop = icalcomponent_get_first_property(comp, ICAL_RELATEDTO_PROPERTY);
prop;
prop = next) {
next = icalcomponent_get_next_property(comp, ICAL_RELATEDTO_PROPERTY);
icalcomponent_remove_property(comp, prop);
icalproperty_free(prop);
}
if (relatedTo == NULL || relatedTo == json_null()) return;
/* Add relatedTo */
const char *uid = NULL;
json_t *relationObj = NULL;
jmap_parser_push(parser, "relatedTo");
json_object_foreach(relatedTo, uid, relationObj) {
jmap_parser_push(parser, uid);
validate_type(parser, relationObj, "Relation");
/* relation */
json_t *relation = json_object_get(relationObj, "relation");
if (json_object_size(relation)) {
prop = icalproperty_new_relatedto(uid);
json_t *jval;
const char *reltype;
jmap_parser_push(parser, "relation");
json_object_foreach(relation, reltype, jval) {
if (jval == json_true()) {
char *s = ucase(xstrdup(reltype));
param = icalparameter_new(ICAL_RELTYPE_PARAMETER);
icalparameter_set_xvalue(param, s);
icalproperty_add_parameter(prop, param);
free(s);
}
else {
jmap_parser_invalid(parser, reltype);
}
}
icalcomponent_add_property(comp, prop);
jmap_parser_pop(parser);
}
else if (json_is_object(relation) || relation == NULL || relation == json_null()) {
icalcomponent_add_property(comp, icalproperty_new_relatedto(uid));
}
else if (!json_is_object(relation)) {
jmap_parser_invalid(parser, "relation");
}
jmap_parser_pop(parser);
}
jmap_parser_pop(parser);
}
static int
validate_location(json_t *loc, struct jmap_parser *parser, json_t *links)
{
size_t invalid_cnt = json_array_size(parser->invalid);
json_t *jprop = NULL;
json_t *jtype = json_object_get(links, "@type");
validate_type(parser, loc, "Location");
/* At least one property other than rel MUST be set */
if ((json_object_size(loc) == 0) ||
(json_object_size(loc) == 1 && !JNOTNULL(jtype) &&
json_object_get(loc, "relativeTo")) ||
(json_object_size(loc) == 2 && JNOTNULL(jtype) &&
json_object_get(loc, "relativeTo"))) {
jmap_parser_invalid(parser, NULL);
return 0;
}
jprop = json_object_get(loc, "name");
if (JNOTNULL(jprop) && !json_is_string(jprop))
jmap_parser_invalid(parser, "name");
jprop = json_object_get(loc, "description");
if (JNOTNULL(jprop) && !json_is_string(jprop))
jmap_parser_invalid(parser, "description");
jprop = json_object_get(loc, "relativeTo");
if (JNOTNULL(jprop) && !json_is_string(jprop))
jmap_parser_invalid(parser, "relativeTo");
jprop = json_object_get(loc, "coordinates");
if (JNOTNULL(jprop) && !json_is_string(jprop))
jmap_parser_invalid(parser, "coordinates");
jprop = json_object_get(loc, "timeZone");
if (json_is_string(jprop)) {
if (!tz_from_tzid(json_string_value(jprop)))
jmap_parser_invalid(parser, "timeZone");
}
else if (JNOTNULL(jprop)) {
jmap_parser_invalid(parser, "timeZone");
}
/* linkIds */
const char *id;
json_t *jval;
json_t *linkids = json_object_get(loc, "linkIds");
if (JNOTNULL(linkids) && json_is_object(linkids)) {
jmap_parser_push(parser, "linkIds");
json_object_foreach(linkids, id, jval) {
if (!is_valid_jmapid(id) || !json_object_get(links, id) || jval != json_true()) {
jmap_parser_invalid(parser, id);
}
}
jmap_parser_pop(parser);
}
else if (JNOTNULL(linkids)) {
jmap_parser_invalid(parser, "linkIds");
}
/* Location is valid, if no invalid property has been added */
return json_array_size(parser->invalid) == invalid_cnt;
}
static void
location_to_ical(icalcomponent *comp, const char *id, json_t *loc)
{
const char *name = json_string_value(json_object_get(loc, "name"));
const char *rel = json_string_value(json_object_get(loc, "relativeTo"));
/* Gracefully handle bogus values */
if (rel && !strcmp(rel, "unknown")) rel = NULL;
/* Determine which property kind to use for this location.
* Always try to create at least one LOCATION, even if CONFERENCE
* would be more appropriate, to gracefully handle legacy clients. */
icalproperty *prop;
if (!icalcomponent_get_first_property(comp, ICAL_LOCATION_PROPERTY)) {
prop = icalproperty_new(ICAL_LOCATION_PROPERTY);
} else {
prop = icalproperty_new(ICAL_X_PROPERTY);
icalproperty_set_x_name(prop, JMAPICAL_XPROP_LOCATION);
}
/* Keep user-supplied location id */
xjmapid_to_ical(prop, id);
/* name, rel */
icalvalue *val = icalvalue_new_from_string(ICAL_TEXT_VALUE, name);
icalproperty_set_value(prop, val); // XXX doesn't support empty string
if (rel) set_icalxparam(prop, JMAPICAL_XPARAM_REL, rel, 0);
/* description, timeZone, coordinates */
const char *s = json_string_value(json_object_get(loc, "description"));
if (s) set_icalxparam(prop, JMAPICAL_XPARAM_DESCRIPTION, s, 0);
s = json_string_value(json_object_get(loc, "timeZone"));
if (s) set_icalxparam(prop, JMAPICAL_XPARAM_TZID, s, 0);
s = json_string_value(json_object_get(loc, "coordinates"));
if (s) set_icalxparam(prop, JMAPICAL_XPARAM_GEO, s, 0);
/* linkIds */
json_t *jval;
const char *key;
json_object_foreach(json_object_get(loc, "linkIds"), key, jval) {
set_icalxparam(prop, JMAPICAL_XPARAM_LINKID, key, 0);
}
icalcomponent_add_property(comp, prop);
}
/* Create or overwrite the JMAP locations in comp */
static void
locations_to_ical(icalcomponent *comp, struct jmap_parser *parser, json_t *locations, json_t *links)
{
json_t *loc;
const char *id;
/* Purge existing locations */
remove_icalprop(comp, ICAL_LOCATION_PROPERTY);
remove_icalprop(comp, ICAL_GEO_PROPERTY);
remove_icalxprop(comp, JMAPICAL_XPROP_LOCATION);
remove_icalxprop(comp, "X-APPLE-STRUCTURED-LOCATION");
/* Bail out if no location needs to be set */
if (!JNOTNULL(locations)) {
return;
}
/* Add locations */
jmap_parser_push(parser, "locations");
json_object_foreach(locations, id, loc) {
/* Validate the location id */
if (!is_valid_jmapid(id)) {
jmap_parser_invalid(parser, id);
continue;
}
jmap_parser_push(parser, id);
/* Validate and add location */
if (validate_location(loc, parser, links)) {
location_to_ical(comp, id, loc);
}
jmap_parser_pop(parser);
}
jmap_parser_pop(parser);
}
/* Create or overwrite the JMAP virtualLocations in comp */
static void
virtuallocations_to_ical(icalcomponent *comp, struct jmap_parser *parser, json_t *locations)
{
json_t *loc;
const char *id;
remove_icalprop(comp, ICAL_CONFERENCE_PROPERTY);
if (!JNOTNULL(locations)) {
return;
}
jmap_parser_push(parser, "virtualLocations");
json_object_foreach(locations, id, loc) {
/* Validate the location id */
if (!is_valid_jmapid(id)) {
jmap_parser_invalid(parser, id);
continue;
}
jmap_parser_push(parser, id);
validate_type(parser, loc, "VirtualLocation");
icalproperty *prop = icalproperty_new(ICAL_CONFERENCE_PROPERTY);
xjmapid_to_ical(prop, id);
/* uri */
json_t *juri = json_object_get(loc, "uri");
if (json_is_string(juri)) {
const char *uri = json_string_value(juri);
icalvalue *val = icalvalue_new_from_string(ICAL_URI_VALUE, uri);
icalproperty_set_value(prop, val);
}
else {
jmap_parser_invalid(parser, "uri");
}
/* name */
json_t *jname = json_object_get(loc, "name");
if (json_is_string(juri)) {
const char *name = json_string_value(jname);
icalproperty_add_parameter(prop, icalparameter_new_label(name));
}
else {
jmap_parser_invalid(parser, "uri");
}
/* description */
json_t *jdescription = json_object_get(loc, "description");
if (json_is_string(jdescription)) {
const char *desc = json_string_value(jdescription);
set_icalxparam(prop, JMAPICAL_XPARAM_DESCRIPTION, desc, 0);
}
else if (JNOTNULL(jdescription)) {
jmap_parser_invalid(parser, "description");
}
icalcomponent_add_property(comp, prop);
jmap_parser_pop(parser);
}
jmap_parser_pop(parser);
}
static void set_language_icalprop(icalcomponent *comp, icalproperty_kind kind,
const char *lang)
{
icalproperty *prop;
icalparameter *param;
prop = icalcomponent_get_first_property(comp, kind);
if (!prop) return;
icalproperty_remove_parameter_by_kind(prop, ICAL_LANGUAGE_PARAMETER);
if (!lang) return;
param = icalparameter_new(ICAL_LANGUAGE_PARAMETER);
icalparameter_set_language(param, lang);
icalproperty_add_parameter(prop, param);
}
static void
overrides_to_ical(icalcomponent *comp, struct jmap_parser *parser, json_t *overrides)
{
icalcomponent *excomp, *next, *ical;
/* Purge EXDATE, RDATE */
remove_icalprop(comp, ICAL_RDATE_PROPERTY);
remove_icalprop(comp, ICAL_EXDATE_PROPERTY);
/* Remove existing VEVENT exceptions */
ical = icalcomponent_get_parent(comp);
for (excomp = icalcomponent_get_first_component(ical, ICAL_VEVENT_COMPONENT);
excomp;
excomp = next) {
next = icalcomponent_get_next_component(ical, ICAL_VEVENT_COMPONENT);
if (excomp == comp) continue;
icalcomponent_remove_component(ical, excomp);
}
if (json_is_null(overrides)) return;
/* Determine value type of main event DTSTART */
int is_date = icalcomponent_get_dtstart(comp).is_date;
icaltimezone *tzstart = tz_from_tzid(tzid_from_ical(comp, ICAL_DTSTART_PROPERTY));
/* Convert current master event to JMAP */
json_t *master = calendarevent_from_ical(comp, NULL, NULL);
if (!master) return;
json_object_del(master, "recurrenceRule");
json_object_del(master, "recurrenceOverrides");
jmap_parser_push(parser, "recurrenceOverrides");
json_t *joverride;
const char *recuridval;
json_object_foreach(overrides, recuridval, joverride) {
- struct datetime recurid = JMAP_DATETIME_INITIALIZER;
+ struct jmapical_datetime recurid = JMAPICAL_DATETIME_INITIALIZER;
- if (parse_localdatetime(recuridval, &recurid) < 0) {
+ if (jmapical_localdatetime_from_string(recuridval, &recurid) < 0) {
jmap_parser_invalid(parser, recuridval);
continue;
}
- else if (is_date && !datetime_has_zero_time(&recurid)) {
+ else if (is_date && !jmapical_datetime_has_zero_time(&recurid)) {
jmap_parser_invalid(parser, recuridval);
continue;
}
json_t *excluded = json_object_get(joverride, "excluded");
if (excluded == json_true()) {
if (json_object_size(joverride) == 1) {
/* Add EXDATE */
struct icaltimetype exdate = is_date ?
- datetime_to_icaldate(&recurid) :
- datetime_to_icaltime(&recurid, tzstart);
+ jmapical_datetime_to_icaldate(&recurid) :
+ jmapical_datetime_to_icaltime(&recurid, tzstart);
insert_icaltimeprop(comp, exdate, recurid.nano, 0, ICAL_EXDATE_PROPERTY);
}
else {
/* excluded overrides MUST NOT define any other property */
jmap_parser_invalid(parser, recuridval);
}
} else if (!json_object_size(joverride)) {
/* Add RDATE */
struct icaltimetype rdate = is_date ?
- datetime_to_icaldate(&recurid) :
- datetime_to_icaltime(&recurid, tzstart);
+ jmapical_datetime_to_icaldate(&recurid) :
+ jmapical_datetime_to_icaltime(&recurid, tzstart);
insert_icaltimeprop(comp, rdate, recurid.nano, 0, ICAL_RDATE_PROPERTY);
} else {
/* Add VEVENT exception */
json_t *myoverride = json_copy(joverride); // shallow copy
/* JMAP spec: "A pointer MUST NOT start with one of the following
* prefixes; any patch with a such a key MUST be ignored" */
const char *key;
json_t *jval;
json_object_foreach(joverride, key, jval) {
if (!strcmp(key, "@type") ||
!strcmp(key, "uid") ||
!strcmp(key, "relatedTo") ||
!strcmp(key, "prodId") ||
!strcmp(key, "method") ||
!strcmp(key, "recurrenceId") ||
!strcmp(key, "recurrenceRule") ||
!strcmp(key, "recurrenceOverrides") ||
!strcmp(key, "replyTo") ||
!strcmp(key, "participantId")) {
json_object_del(myoverride, key);
}
}
if (!json_object_size(myoverride)) {
json_decref(myoverride);
continue;
}
/* If the override doesn't have a custom start date, use
* the LocalDate in the recurrenceOverrides object key */
if (!json_object_get(myoverride, "start")) {
json_object_set_new(myoverride, "start", json_string(recuridval));
}
/* Create overridden event from patch and master event */
json_t *ex;
if (!(ex = jmap_patchobject_apply(master, myoverride, NULL))) {
jmap_parser_invalid(parser, recuridval);
json_decref(myoverride);
continue;
}
/* Create a new VEVENT for this override */
excomp = icalcomponent_new_vevent();
struct icaltimetype icalrecurid = is_date ?
- datetime_to_icaldate(&recurid) :
- datetime_to_icaltime(&recurid, tzstart);
+ jmapical_datetime_to_icaldate(&recurid) :
+ jmapical_datetime_to_icaltime(&recurid, tzstart);
insert_icaltimeprop(excomp, icalrecurid, recurid.nano, 1, ICAL_RECURRENCEID_PROPERTY);
icalcomponent_set_uid(excomp, icalcomponent_get_uid(comp));
/* Convert the override event to iCalendar */
jmap_parser_push(parser, recuridval);
/* recurrenceId */
json_t *jrecurrenceId = json_object_get(myoverride, "recurrenceId");
if (json_is_string(jrecurrenceId)) {
const char *val = json_string_value(jrecurrenceId);
- struct datetime dt = JMAP_DATETIME_INITIALIZER;
- if (parse_localdatetime(val, &dt) < 0 ||
- compare_datetime(&dt, &recurid) != 0) {
+ struct jmapical_datetime dt = JMAPICAL_DATETIME_INITIALIZER;
+ if (jmapical_localdatetime_from_string(val, &dt) < 0 ||
+ jmapical_datetime_compare(&dt, &recurid) != 0) {
jmap_parser_invalid(parser, "recurrenceId");
}
}
else if (jrecurrenceId) {
jmap_parser_invalid(parser, "recurrenceId");
}
calendarevent_to_ical(excomp, parser, ex);
jmap_parser_pop(parser);
/* Add the exception */
icalcomponent_add_component(ical, excomp);
json_decref(ex);
json_decref(myoverride);
}
}
jmap_parser_pop(parser);
json_decref(master);
}
/* Create or overwrite the iCalendar properties in VEVENT comp based on the
* properties the JMAP calendar event. This writes a *complete* jsevent and
* does not implement patch object semantics.
*/
static void
calendarevent_to_ical(icalcomponent *comp, struct jmap_parser *parser, json_t *event)
{
icalproperty *prop = NULL;
int is_exc = icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY) != NULL;
icaltimezone *utc = icaltimezone_get_utc_timezone();
json_t *jprop = json_object_get(event, "excluded");
if (jprop && jprop != json_false()) {
jmap_parser_invalid(parser, "excluded");
}
/* uid */
const char *uid = json_string_value(json_object_get(event, "uid"));
if (uid) icalcomponent_set_uid(comp, uid);
else jmap_parser_invalid(parser, "uid");
jprop = json_object_get(event, "@type");
if (JNOTNULL(jprop) && json_is_string(jprop)) {
if (strcmp(json_string_value(jprop), "jsevent")) {
jmap_parser_invalid(parser, "@type");
}
} else if (JNOTNULL(jprop)) {
jmap_parser_invalid(parser, "@type");
}
/* start, duration, timeZone */
startend_to_ical(comp, parser, event);
/* excluded - validate, but ignore */
jprop = json_object_get(event, "excluded");
if (jprop && !json_is_boolean(jprop)) {
jmap_parser_invalid(parser, "excluded");
}
/* relatedTo */
jprop = json_object_get(event, "relatedTo");
if (json_is_null(jprop) || json_object_size(jprop)) {
relatedto_to_ical(comp, parser, jprop);
} else if (jprop) {
jmap_parser_invalid(parser, "relatedTo");
}
/* sequence */
jprop = json_object_get(event, "sequence");
if (json_is_integer(jprop)) {
json_int_t val = json_integer_value(jprop);
if (val >= 0 && val <= INT_MAX) {
icalcomponent_set_sequence(comp, (int)val);
}
else jmap_parser_invalid(parser, "sequence");
} else if (jprop) {
jmap_parser_invalid(parser, "sequence");
}
/* prodId */
if (!is_exc) {
struct buf buf = BUF_INITIALIZER;
const char *prod_id = NULL;
jprop = json_object_get(event, "prodId");
if (json_is_string(jprop)) {
prod_id = json_string_value(jprop);
}
else if (JNOTNULL(jprop)) {
jmap_parser_invalid(parser, "prodId");
}
if (!prod_id) {
/* Use same product id like jcal.c */
buf_setcstr(&buf, "-//CyrusIMAP.org/Cyrus ");
buf_appendcstr(&buf, CYRUS_VERSION);
buf_appendcstr(&buf, "//EN");
prod_id = buf_cstring(&buf);
}
/* Set PRODID in the VCALENDAR */
icalcomponent *ical = icalcomponent_get_parent(comp);
remove_icalprop(ical, ICAL_PRODID_PROPERTY);
prop = icalproperty_new_prodid(prod_id);
icalcomponent_add_property(ical, prop);
buf_free(&buf);
}
/* created */
jprop = json_object_get(event, "created");
if (json_is_string(jprop)) {
- struct datetime tstamp = JMAP_DATETIME_INITIALIZER;
- if (parse_utcdatetime(json_string_value(jprop), &tstamp) >= 0) {
- icaltimetype dt = datetime_to_icaltime(&tstamp, utc);
+ struct jmapical_datetime tstamp = JMAPICAL_DATETIME_INITIALIZER;
+ if (jmapical_utcdatetime_from_string(json_string_value(jprop), &tstamp) >= 0) {
+ icaltimetype dt = jmapical_datetime_to_icaltime(&tstamp, utc);
insert_icaltimeprop(comp, dt, tstamp.nano, 1, ICAL_CREATED_PROPERTY);
}
else {
jmap_parser_invalid(parser, "created");
}
} else if (JNOTNULL(jprop)) {
jmap_parser_invalid(parser, "created");
}
/* updated */
jprop = json_object_get(event, "updated");
if (json_is_string(jprop)) {
- struct datetime tstamp = JMAP_DATETIME_INITIALIZER;
- if (parse_utcdatetime(json_string_value(jprop), &tstamp) >= 0) {
- icaltimetype dt = datetime_to_icaltime(&tstamp, utc);
+ struct jmapical_datetime tstamp = JMAPICAL_DATETIME_INITIALIZER;
+ if (jmapical_utcdatetime_from_string(json_string_value(jprop), &tstamp) >= 0) {
+ icaltimetype dt = jmapical_datetime_to_icaltime(&tstamp, utc);
insert_icaltimeprop(comp, dt, tstamp.nano, 1, ICAL_DTSTAMP_PROPERTY);
}
else {
jmap_parser_invalid(parser, "updated");
}
} else if (jprop == NULL) {
icaltimetype now = \
icaltime_current_time_with_zone(icaltimezone_get_utc_timezone());
insert_icaltimeprop(comp, now, 0, 1, ICAL_DTSTAMP_PROPERTY);
} else {
jmap_parser_invalid(parser, "updated");
}
jprop = json_object_get(event, "priority");
if (json_integer_value(jprop) >= 0 || json_integer_value(jprop) <= 9) {
remove_icalprop(comp, ICAL_PRIORITY_PROPERTY);
prop = icalproperty_new_priority(json_integer_value(jprop));
icalcomponent_add_property(comp, prop);
} else if (JNOTNULL(jprop)) {
jmap_parser_invalid(parser, "priority");
}
/* title */
jprop = json_object_get(event, "title");
if (json_is_string(jprop)) {
const char *summary = json_string_value(jprop);
if (strlen(summary)) icalcomponent_set_summary(comp, summary);
}
else if (JNOTNULL(jprop)) {
jmap_parser_invalid(parser, "title");
}
/* description and descriptionContentType */
description_to_ical(comp, parser, event);
/* method */
jprop = json_object_get(event, "method");
if (json_is_string(jprop)) {
const char *method = json_string_value(jprop);
icalproperty_method icalmethod = icalenum_string_to_method(method);
if (icalmethod != ICAL_METHOD_NONE && !is_exc) {
icalcomponent *ical = icalcomponent_get_parent(comp);
icalcomponent_set_method(ical, icalmethod);
}
else {
jmap_parser_invalid(parser, "method");
}
} else if (JNOTNULL(jprop)) {
jmap_parser_invalid(parser, "method");
}
/* color */
jprop = json_object_get(event, "color");
if (json_is_string(jprop)) {
const char *val = json_string_value(jprop);
if (strlen(val)) {
prop = icalproperty_new_color(val);
icalcomponent_add_property(comp, prop);
}
} else if (JNOTNULL(jprop)) {
jmap_parser_invalid(parser, "color");
}
/* keywords */
jprop = json_object_get(event, "keywords");
if (json_is_null(jprop) || json_is_object(jprop)) {
keywords_to_ical(comp, parser, jprop);
} else if (jprop) {
jmap_parser_invalid(parser, "keywords");
}
/* links */
jprop = json_object_get(event, "links");
if (json_is_null(jprop) || json_object_size(jprop)) {
links_to_ical(comp, parser, jprop);
} else if (jprop) {
jmap_parser_invalid(parser, "links");
}
/* locale */
jprop = json_object_get(event, "locale");
if (json_is_string(jprop)) {
set_language_icalprop(comp, ICAL_SUMMARY_PROPERTY, NULL);
set_language_icalprop(comp, ICAL_DESCRIPTION_PROPERTY, NULL);
const char *val = json_string_value(jprop);
if (strlen(val)) {
set_language_icalprop(comp, ICAL_SUMMARY_PROPERTY, val);
}
} else if (json_is_null(jprop)) {
set_language_icalprop(comp, ICAL_SUMMARY_PROPERTY, NULL);
set_language_icalprop(comp, ICAL_DESCRIPTION_PROPERTY, NULL);
} else if (jprop) {
jmap_parser_invalid(parser, "locale");
}
/* locations */
jprop = json_object_get(event, "locations");
if (json_is_null(jprop) || json_object_size(jprop)) {
json_t *links = json_object_get(event, "links");
locations_to_ical(comp, parser, jprop, links);
} else if (jprop) {
jmap_parser_invalid(parser, "locations");
}
/* virtualLocations */
jprop = json_object_get(event, "virtualLocations");
if (json_is_null(jprop) || json_object_size(jprop)) {
virtuallocations_to_ical(comp, parser, jprop);
} else if (jprop) {
jmap_parser_invalid(parser, "virtualLocations");
}
/* recurrenceRule */
jprop = json_object_get(event, "recurrenceRule");
if (json_is_null(jprop) || json_is_object(jprop)) {
if (!is_exc) recurrence_to_ical(comp, parser, jprop);
} else if (jprop) {
jmap_parser_invalid(parser, "recurrenceRule");
}
/* status */
enum icalproperty_status status = ICAL_STATUS_NONE;
jprop = json_object_get(event, "status");
if (json_is_string(jprop)) {
const char *val = json_string_value(jprop);
if (!strcmp(val, "confirmed")) {
status = ICAL_STATUS_CONFIRMED;
} else if (!strcmp(val, "cancelled")) {
status = ICAL_STATUS_CANCELLED;
} else if (!strcmp(val, "tentative")) {
status = ICAL_STATUS_TENTATIVE;
} else {
jmap_parser_invalid(parser, "status");
}
} else if (json_is_null(jprop) || !jprop) {
status = ICAL_STATUS_CONFIRMED;
} else {
jmap_parser_invalid(parser, "status");
}
if (status != ICAL_STATUS_NONE) {
remove_icalprop(comp, ICAL_STATUS_PROPERTY);
icalcomponent_set_status(comp, status);
}
/* freeBusyStatus */
jprop = json_object_get(event, "freeBusyStatus");
if (json_is_string(jprop)) {
const char *val = json_string_value(jprop);
enum icalproperty_transp v = ICAL_TRANSP_NONE;
if (!strcmp(val, "free")) {
v = ICAL_TRANSP_TRANSPARENT;
} else if (!strcmp(val, "busy")) {
v = ICAL_TRANSP_OPAQUE;
} else {
jmap_parser_invalid(parser, "freeBusyStatus");
}
if (v != ICAL_TRANSP_NONE) {
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));
}
}
} else if (JNOTNULL(jprop)) {
jmap_parser_invalid(parser, "freeBusyStatus");
}
/* privacy */
jprop = json_object_get(event, "privacy");
if (json_is_string(jprop)) {
const char *val = json_string_value(jprop);
enum icalproperty_class v = ICAL_CLASS_NONE;
if (!strcmp(val, "public")) {
v = ICAL_CLASS_PUBLIC;
} else if (!strcmp(val, "private")) {
v = ICAL_CLASS_PRIVATE;
} else if (!strcmp(val, "secret")) {
v = ICAL_CLASS_CONFIDENTIAL;
} else {
jmap_parser_invalid(parser, "privacy");
}
if (v != ICAL_CLASS_NONE) {
prop = icalcomponent_get_first_property(comp, ICAL_CLASS_PROPERTY);
if (prop) {
icalproperty_set_class(prop, v);
} else {
icalcomponent_add_property(comp, icalproperty_new_class(v));
}
}
} else if (JNOTNULL(jprop)) {
jmap_parser_invalid(parser, "privacy");
}
/* replyTo and participants */
participants_to_ical(comp, parser, event);
/* participantId: readonly */
/* useDefaultAlerts */
jprop = json_object_get(event, "useDefaultAlerts");
if (json_is_boolean(jprop)) {
remove_icalxprop(comp, JMAPICAL_XPROP_USEDEFALERTS);
if (json_boolean_value(jprop)) {
icalvalue *icalval = icalvalue_new_boolean(1);
prop = icalproperty_new(ICAL_X_PROPERTY);
icalproperty_set_x_name(prop, JMAPICAL_XPROP_USEDEFALERTS);
icalproperty_set_value(prop, icalval);
icalcomponent_add_property(comp, prop);
}
} else if (JNOTNULL(jprop)) {
jmap_parser_invalid(parser, "useDefaultAlerts");
}
/* alerts */
jprop = json_object_get(event, "alerts");
if (json_is_null(jprop) || json_object_size(jprop)) {
alerts_to_ical(comp, parser, jprop);
} else if (jprop) {
jmap_parser_invalid(parser, "alerts");
}
/* recurrenceOverrides - must be last to apply patches */
jprop = json_object_get(event, "recurrenceOverrides");
if (json_is_null(jprop) || json_is_object(jprop)) {
overrides_to_ical(comp, parser, jprop);
} else if (jprop) {
jmap_parser_invalid(parser, "recurrenceOverrides");
}
}
icalcomponent*
jmapical_toical(json_t *jsevent, json_t *invalid)
{
struct jmap_parser parser = JMAP_PARSER_INITIALIZER;
icalcomponent *ical = NULL;
icalcomponent *comp = NULL;
/* Create a new VCALENDAR. */
ical = icalcomponent_new_vcalendar();
icalcomponent_add_property(ical, icalproperty_new_version("2.0"));
icalcomponent_add_property(ical, icalproperty_new_calscale("GREGORIAN"));
/* Create a new VEVENT. */
icaltimezone *utc = icaltimezone_get_utc_timezone();
struct icaltimetype now =
icaltime_from_timet_with_zone(time(NULL), 0, utc);
comp = icalcomponent_new_vevent();
icalcomponent_set_sequence(comp, 0);
icalcomponent_set_dtstamp(comp, now);
icalcomponent_add_property(comp, icalproperty_new_created(now));
icalcomponent_add_component(ical, comp);
/* Convert the JMAP calendar event to ical. */
calendarevent_to_ical(comp, &parser, jsevent);
icalcomponent_add_required_timezones(ical);
/* Report any property errors. */
if (json_array_size(parser.invalid)) {
if (invalid) json_array_extend(invalid, parser.invalid);
if (ical) icalcomponent_free(ical);
ical = NULL;
}
#if 0
if (ical &&
(!icalrestriction_check(ical) || icalcomponent_count_errors(ical))) {
syslog(LOG_ERR, "jmapical_toical: %s", get_icalcomponent_errstr(ical));
if (!ctx->err->code) ctx->err->code = JMAPICAL_ERROR_UNKNOWN;
icalcomponent_free(ical);
ical = NULL;
}
#endif
jmap_parser_fini(&parser);
return ical;
}
const char *
jmapical_strerror(int err)
{
switch (err) {
case 0:
return "jmapical: success";
case JMAPICAL_ERROR_CALLBACK:
return "jmapical: callback error";
case JMAPICAL_ERROR_MEMORY:
return "jmapical: no memory";
case JMAPICAL_ERROR_ICAL:
return "jmapical: iCalendar error";
case JMAPICAL_ERROR_PROPS:
return "jmapical: property error";
case JMAPICAL_ERROR_UID:
return "jmapical: iCalendar uid error";
default:
return "jmapical: unknown error";
}
}
/*
* Construct a jevent string for an iCalendar component.
*/
EXPORTED struct buf *icalcomponent_as_jevent_string(icalcomponent *ical)
{
struct buf *ret;
json_t *jcal;
size_t flags = JSON_PRESERVE_ORDER;
char *buf;
if (!ical) return NULL;
jcal = jmapical_tojmap(ical, NULL);
flags |= (config_httpprettytelemetry ? JSON_INDENT(2) : JSON_COMPACT);
buf = json_dumps(jcal, flags);
json_decref(jcal);
ret = buf_new();
buf_initm(ret, buf, strlen(buf));
return ret;
}
EXPORTED icalcomponent *jevent_string_as_icalcomponent(const struct buf *buf)
{
json_t *obj;
json_error_t jerr;
icalcomponent *ical;
const char *str = buf_cstring(buf);
if (!str) return NULL;
obj = json_loads(str, 0, &jerr);
if (!obj) {
syslog(LOG_WARNING, "json parse error: '%s'", jerr.text);
return NULL;
}
ical = jmapical_toical(obj, NULL);
json_decref(obj);
return ical;
}
-
-#undef JMAP_DURATION_INITIALIZER
-#undef JMAP_DATETIME_INITIALIZER
-
diff --git a/imap/jmap_ical.h b/imap/jmap_ical.h
index f7da49a52..cfd9f212c 100644
--- a/imap/jmap_ical.h
+++ b/imap/jmap_ical.h
@@ -1,112 +1,182 @@
/* jmap_ical.h --Routines to convert JMAP calendar events and iCalendar
*
* Copyright (c) 1994-2016 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.
*
*/
#ifndef JMAPICAL_H
#define JMAPICAL_H
#ifdef __cplusplus
extern "C" {
#endif
#include <jansson.h>
#include <libical/ical.h>
#define JMAPICAL_ERROR_UNKNOWN -1
#define JMAPICAL_ERROR_CALLBACK 1
#define JMAPICAL_ERROR_MEMORY 2
#define JMAPICAL_ERROR_ICAL 3
#define JMAPICAL_ERROR_PROPS 4
#define JMAPICAL_ERROR_UID 5
/* Custom iCalendar properties */
#define JMAPICAL_XPROP_LOCATION "X-JMAP-LOCATION"
/* FIXME libical doesn't parse USEDEFAULTALERTS, must use X-prefix */
#define JMAPICAL_XPROP_USEDEFALERTS "X-JMAP-USEDEFAULTALERTS"
/* Custom iCalendar parameters */
#define JMAPICAL_XPARAM_CID "X-JMAP-CID"
#define JMAPICAL_XPARAM_DESCRIPTION "X-JMAP-DESCRIPTION"
#define JMAPICAL_XPARAM_DISPLAY "X-JMAP-DISPLAY"
#define JMAPICAL_XPARAM_FEATURE "X-JMAP-FEATURE"
#define JMAPICAL_XPARAM_GEO "X-JMAP-GEO"
#define JMAPICAL_XPARAM_ID "X-JMAP-ID"
#define JMAPICAL_XPARAM_LINKID "X-JMAP-LINKID"
#define JMAPICAL_XPARAM_LOCATIONID "X-JMAP-LOCATIONID"
#define JMAPICAL_XPARAM_NAME "X-JMAP-NAME"
#define JMAPICAL_XPARAM_REL "X-JMAP-REL"
#define JMAPICAL_XPARAM_ROLE "X-JMAP-ROLE"
#define JMAPICAL_XPARAM_RSVP_URI "X-JMAP-RSVP-URI"
#define JMAPICAL_XPARAM_TZID "X-JMAP-TZID"
#define JMAPICAL_XPARAM_DTSTAMP "X-DTSTAMP" /* used for iMIP ATTENDEE replies */
#define JMAPICAL_XPARAM_SEQUENCE "X-SEQUENCE" /*used for iMIP ATTENDEE replies */
#define JMAPICAL_XPARAM_COMMENT "X-COMMENT" /*used for iMIP ATTENDEE replies */
#define JMAPICAL_XPARAM_TITLE "X-TITLE" /* Apple uses that for locations */
/* Converts the iCalendar component ical to JSCalendar.
* Returns NULL on error.
*/
json_t* jmapical_tojmap(icalcomponent *ical, hash_table *props);
/* Converts the iCalendar component ical to an array of JSCalendar objects.
* Returns NULL on error.
*/
json_t *jmapical_tojmap_all(icalcomponent *ical, hash_table *props);
/* Convert the jsevent to iCalendar.
* Returns NULL on error.
*/
icalcomponent* jmapical_toical(json_t *jsevent, json_t *invalid);
void icalcomponent_add_required_timezones(icalcomponent *ical);
/* for CalDAV content negotiation */
struct buf *icalcomponent_as_jevent_string(icalcomponent *ical);
icalcomponent *jevent_string_as_icalcomponent(const struct buf *buf);
+/* Base type for JSCalendar LocalDateTime and UTCDateTime */
+
+struct jmapical_datetime {
+ int year;
+ int month; // Jan=1
+ int day;
+ int hour;
+ int minute;
+ int second;
+ bit64 nano;
+};
+
+#define JMAPICAL_DATETIME_INITIALIZER { 0, 0, 0, 0, 0, 0, 0 };
+
+/* True if all components are zero */
+extern int jmapical_datetime_has_zero_time(const struct jmapical_datetime *dt);
+
+/* Convert DateTime to ical date, truncating time components */
+extern struct icaltimetype jmapical_datetime_to_icaldate(const struct jmapical_datetime *dt);
+
+/* Convert DateTime to ical time, truncating subseconds */
+extern icaltimetype jmapical_datetime_to_icaltime(const struct jmapical_datetime *dt,
+ const icaltimezone* zone);
+
+/* Convert ical time to DateTime with zero subseconds */
+extern void jmapical_datetime_from_icaltime(icaltimetype icaldt, struct jmapical_datetime *dt);
+
+/* Compare DateTime a and b, using semantics suitable for qsort */
+extern int jmapical_datetime_compare(const struct jmapical_datetime *a,
+ const struct jmapical_datetime *b);
+
+/* Convert icaltime value and subseconds parameter to DateTime */
+extern int jmapical_datetime_from_icalprop(icalproperty *prop, struct jmapical_datetime *dt);
+
+/* JSCalendar LocalDateTime */
+extern void jmapical_localdatetime_as_string(const struct jmapical_datetime *dt, struct buf *dst);
+extern int jmapical_localdatetime_from_string(const char *val, struct jmapical_datetime *dt);
+
+/* JSCalendar UTCDateTime */
+extern void jmapical_utcdatetime_as_string(const struct jmapical_datetime *dt, struct buf *dst);
+extern int jmapical_utcdatetime_from_string(const char *val, struct jmapical_datetime *dt);
+
+/* JSCalendar Duration */
+
+struct jmapical_duration {
+ int is_neg;
+ unsigned int days;
+ unsigned int weeks;
+ unsigned int hours;
+ unsigned int minutes;
+ unsigned int seconds;
+ bit64 nanos;
+};
+
+#define JMAPICAL_DURATION_INITIALIZER { 0, 0, 0, 0, 0, 0, 0 }
+
+/* True if all components are zero */
+extern int jmapical_duration_has_zero_time(const struct jmapical_duration *dur);
+
+/* Convert ical duration to Duration with zero subseconds */
+extern void jmapical_duration_from_icalduration(struct icaldurationtype icaldur,
+ struct jmapical_duration *dur);
+
+/* Calculate time-range between t1 and t2 into Duration dur */
+extern void jmapical_duration_between(time_t t1, bit64 t1nanos,
+ time_t t2, bit64 t2nanos,
+ struct jmapical_duration *dur);
+
+extern void jmapical_duration_as_string(const struct jmapical_duration *dur, struct buf *buf);
+extern int jmapical_duration_from_string(const char *val, struct jmapical_duration *dur);
#ifdef __cplusplus
}
#endif
#endif

File Metadata

Mime Type
text/x-diff
Expires
Fri, Nov 1, 8:36 AM (1 d, 9 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
10075134
Default Alt Text
(187 KB)

Event Timeline