Page MenuHomePhorge

http_caldav.c
No OneTemporary

Authored By
Unknown
Size
177 KB
Referenced Files
None
Subscribers
None

http_caldav.c

/* http_caldav.c -- Routines for handling CalDAV collections in httpd
*
* Copyright (c) 1994-2011 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.
*
*/
/*
* TODO:
*
* - Make proxying more robust. Currently depends on calendar collections
* residing on same server as user's INBOX. Doesn't handle global/shared
* calendars.
* - Support COPY/MOVE on collections
* - Add more required properties
* - GET/HEAD on collections (iCalendar stream of resources)
* - calendar-query REPORT (handle partial retrieval, prop-filter, timezone?)
* - free-busy-query REPORT (check ACL and transp on all calendars)
* - sync-collection REPORT - need to handle Depth infinity?
*/
#include <config.h>
#include <syslog.h>
#include <libical/ical.h>
#include <libxml/tree.h>
#include <libxml/uri.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "acl.h"
#include "append.h"
#include "caldav_db.h"
#include "exitcodes.h"
#include "global.h"
#include "hash.h"
#include "httpd.h"
#include "http_caldav_sched.h"
#include "http_dav.h"
#include "http_err.h"
#include "http_proxy.h"
#include "imap_err.h"
#include "index.h"
#include "jcal.h"
#include "xcal.h"
#include "mailbox.h"
#include "mboxlist.h"
#include "md5.h"
#include "message.h"
#include "message_guid.h"
#include "proxy.h"
#include "times.h"
#include "smtpclient.h"
#include "spool.h"
#include "strhash.h"
#include "stristr.h"
#include "tok.h"
#include "util.h"
#include "version.h"
#include "xmalloc.h"
#include "xstrlcat.h"
#include "xstrlcpy.h"
#ifdef HAVE_RSCALE
#include <unicode/ucal.h>
#endif
#define NEW_STAG (1<<8) /* Make sure we skip over PREFER bits */
#ifndef HAVE_SCHEDULING_PARAMS
/* Functions to replace those not available in libical < v1.0 */
static icalparameter_scheduleagent
icalparameter_get_scheduleagent(icalparameter *param)
{
const char *agent = NULL;
if (param) agent = icalparameter_get_iana_value(param);
if (!agent) return ICAL_SCHEDULEAGENT_NONE;
else if (!strcmp(agent, "SERVER")) return ICAL_SCHEDULEAGENT_SERVER;
else if (!strcmp(agent, "CLIENT")) return ICAL_SCHEDULEAGENT_CLIENT;
else return ICAL_SCHEDULEAGENT_X;
}
static icalparameter_scheduleforcesend
icalparameter_get_scheduleforcesend(icalparameter *param)
{
const char *force = NULL;
if (param) force = icalparameter_get_iana_value(param);
if (!force) return ICAL_SCHEDULEFORCESEND_NONE;
else if (!strcmp(force, "REQUEST")) return ICAL_SCHEDULEFORCESEND_REQUEST;
else if (!strcmp(force, "REPLY")) return ICAL_SCHEDULEFORCESEND_REPLY;
else return ICAL_SCHEDULEFORCESEND_X;
}
static icalparameter *icalparameter_new_schedulestatus(const char *stat)
{
icalparameter *param = icalparameter_new(ICAL_IANA_PARAMETER);
icalparameter_set_iana_name(param, "SCHEDULE-STATUS");
icalparameter_set_iana_value(param, stat);
return param;
}
/* Wrappers to fetch scheduling parameters by kind */
static icalparameter*
icalproperty_get_iana_parameter_by_name(icalproperty *prop, const char *name)
{
icalparameter *param;
for (param = icalproperty_get_first_parameter(prop, ICAL_IANA_PARAMETER);
param && strcmp(icalparameter_get_iana_name(param), name);
param = icalproperty_get_next_parameter(prop, ICAL_IANA_PARAMETER));
return param;
}
#define icalproperty_get_scheduleagent_parameter(prop) \
icalproperty_get_iana_parameter_by_name(prop, "SCHEDULE-AGENT")
#define icalproperty_get_scheduleforcesend_parameter(prop) \
icalproperty_get_iana_parameter_by_name(prop, "SCHEDULE-FORCE-SEND")
#define icalproperty_get_schedulestatus_parameter(prop) \
icalproperty_get_iana_parameter_by_name(prop, "SCHEDULE-STATUS")
#else
/* Wrappers to fetch scheduling parameters by kind */
#define icalproperty_get_scheduleagent_parameter(prop) \
icalproperty_get_first_parameter(prop, ICAL_SCHEDULEAGENT_PARAMETER)
#define icalproperty_get_scheduleforcesend_parameter(prop) \
icalproperty_get_first_parameter(prop, ICAL_SCHEDULEFORCESEND_PARAMETER)
#define icalproperty_get_schedulestatus_parameter(prop) \
icalproperty_get_first_parameter(prop, ICAL_SCHEDULESTATUS_PARAMETER)
#endif /* HAVE_SCHEDULING_PARAMS */
struct busytime {
struct icalperiodtype *busy;
unsigned len;
unsigned alloc;
};
struct calquery_filter {
unsigned comp;
struct icaltimetype start;
struct icaltimetype end;
unsigned check_transp;
unsigned save_busytime;
struct busytime busytime; /* array of found busytime periods */
};
static unsigned config_allowsched = IMAP_ENUM_CALDAV_ALLOWSCHEDULING_OFF;
static struct caldav_db *auth_caldavdb = NULL;
static time_t compile_time;
static struct buf ical_prodid_buf = BUF_INITIALIZER;
static const char *ical_prodid = NULL;
static struct caldav_db *my_caldav_open(struct mailbox *mailbox);
static void my_caldav_close(struct caldav_db *caldavdb);
static void my_caldav_init(struct buf *serverinfo);
static void my_caldav_auth(const char *userid);
static void my_caldav_reset(void);
static void my_caldav_shutdown(void);
static int caldav_parse_path(const char *path,
struct request_target_t *tgt, const char **errstr);
static int caldav_check_precond(struct transaction_t *txn, const void *data,
const char *etag, time_t lastmod);
static int caldav_acl(struct transaction_t *txn, xmlNodePtr priv, int *rights);
static int caldav_copy(struct transaction_t *txn,
struct mailbox *src_mbox, struct index_record *src_rec,
struct mailbox *dest_mbox, const char *dest_rsrc,
struct caldav_db *dest_davdb,
unsigned overwrite, unsigned flags);
static int caldav_delete_sched(struct transaction_t *txn,
struct mailbox *mailbox,
struct index_record *record, void *data);
static int meth_get(struct transaction_t *txn, void *params);
static int caldav_post(struct transaction_t *txn);
static int caldav_put(struct transaction_t *txn,
struct mime_type_t *mime,
struct mailbox *mailbox,
struct caldav_db *caldavdb,
unsigned flags);
static int propfind_getcontenttype(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx, xmlNodePtr resp,
struct propstat propstat[], void *rock);
static int propfind_restype(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx, xmlNodePtr resp,
struct propstat propstat[], void *rock);
static int propfind_reportset(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx, xmlNodePtr resp,
struct propstat propstat[], void *rock);
static int propfind_caldata(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx, xmlNodePtr resp,
struct propstat propstat[], void *rock);
static int propfind_calcompset(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx, xmlNodePtr resp,
struct propstat propstat[], void *rock);
static int proppatch_calcompset(xmlNodePtr prop, unsigned set,
struct proppatch_ctx *pctx,
struct propstat propstat[], void *rock);
static int propfind_suppcaldata(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx, xmlNodePtr resp,
struct propstat propstat[], void *rock);
static int propfind_schedtag(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx, xmlNodePtr resp,
struct propstat propstat[], void *rock);
static int propfind_caltransp(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx, xmlNodePtr resp,
struct propstat propstat[], void *rock);
static int proppatch_caltransp(xmlNodePtr prop, unsigned set,
struct proppatch_ctx *pctx,
struct propstat propstat[], void *rock);
static int propfind_tz_avail(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx, xmlNodePtr resp,
struct propstat propstat[], void *rock);
static int proppatch_timezone(xmlNodePtr prop, unsigned set,
struct proppatch_ctx *pctx,
struct propstat propstat[], void *rock);
static int proppatch_availability(xmlNodePtr prop, unsigned set,
struct proppatch_ctx *pctx,
struct propstat propstat[], void *rock);
static int propfind_rscaleset(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx, xmlNodePtr resp,
struct propstat propstat[], void *rock);
static int report_cal_query(struct transaction_t *txn, xmlNodePtr inroot,
struct propfind_ctx *fctx);
static int report_cal_multiget(struct transaction_t *txn, xmlNodePtr inroot,
struct propfind_ctx *fctx);
static int report_fb_query(struct transaction_t *txn, xmlNodePtr inroot,
struct propfind_ctx *fctx);
static int store_resource(struct transaction_t *txn, icalcomponent *ical,
struct mailbox *mailbox, const char *resource,
struct caldav_db *caldavdb, int overwrite,
unsigned flags);
static void sched_request(const char *organizer, struct sched_param *sparam,
icalcomponent *oldical, icalcomponent *newical,
const char *att_update);
static void sched_reply(const char *userid,
icalcomponent *oldical, icalcomponent *newical);
static const char *begin_icalendar(struct buf *buf);
static void end_icalendar(struct buf *buf);
static struct mime_type_t caldav_mime_types[] = {
/* First item MUST be the default type and storage format */
{ "text/calendar; charset=utf-8", "2.0", "ics", "ifb",
(char* (*)(void *)) &icalcomponent_as_ical_string_r,
(void * (*)(const char*)) &icalparser_parse_string,
(void (*)(void *)) &icalcomponent_free, &begin_icalendar, &end_icalendar
},
{ "application/calendar+xml; charset=utf-8", NULL, "xcs", "xfb",
(char* (*)(void *)) &icalcomponent_as_xcal_string,
(void * (*)(const char*)) &xcal_string_as_icalcomponent,
NULL, &begin_xcal, &end_xcal
},
#ifdef WITH_JSON
{ "application/calendar+json; charset=utf-8", NULL, "jcs", "jfb",
(char* (*)(void *)) &icalcomponent_as_jcal_string,
(void * (*)(const char*)) &jcal_string_as_icalcomponent,
NULL, &begin_jcal, &end_jcal
},
#endif
{ NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }
};
/* Array of known "live" properties */
static const struct prop_entry caldav_props[] = {
/* WebDAV (RFC 4918) properties */
{ "creationdate", NS_DAV,
PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
propfind_creationdate, NULL, NULL },
{ "displayname", NS_DAV,
PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
propfind_fromdb, proppatch_todb, NULL },
{ "getcontentlanguage", NS_DAV, PROP_ALLPROP | PROP_RESOURCE,
propfind_fromhdr, NULL, "Content-Language" },
{ "getcontentlength", NS_DAV,
PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
propfind_getlength, NULL, NULL },
{ "getcontenttype", NS_DAV,
PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
propfind_getcontenttype, NULL, "Content-Type" },
{ "getetag", NS_DAV, PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
propfind_getetag, NULL, NULL },
{ "getlastmodified", NS_DAV,
PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
propfind_getlastmod, NULL, NULL },
{ "lockdiscovery", NS_DAV, PROP_ALLPROP | PROP_RESOURCE,
propfind_lockdisc, NULL, NULL },
{ "resourcetype", NS_DAV,
PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
propfind_restype, proppatch_restype, "calendar" },
{ "supportedlock", NS_DAV, PROP_ALLPROP | PROP_RESOURCE,
propfind_suplock, NULL, NULL },
/* WebDAV Versioning (RFC 3253) properties */
{ "supported-report-set", NS_DAV, PROP_COLLECTION,
propfind_reportset, NULL, NULL },
/* WebDAV ACL (RFC 3744) properties */
{ "owner", NS_DAV, PROP_COLLECTION | PROP_RESOURCE,
propfind_owner, NULL, NULL },
{ "group", NS_DAV, 0, NULL, NULL, NULL },
{ "supported-privilege-set", NS_DAV, PROP_COLLECTION | PROP_RESOURCE,
propfind_supprivset, NULL, NULL },
{ "current-user-privilege-set", NS_DAV, PROP_COLLECTION | PROP_RESOURCE,
propfind_curprivset, NULL, NULL },
{ "acl", NS_DAV, PROP_COLLECTION | PROP_RESOURCE,
propfind_acl, NULL, NULL },
{ "acl-restrictions", NS_DAV, PROP_COLLECTION | PROP_RESOURCE,
propfind_aclrestrict, NULL, NULL },
{ "inherited-acl-set", NS_DAV, 0, NULL, NULL, NULL },
{ "principal-collection-set", NS_DAV, PROP_COLLECTION | PROP_RESOURCE,
propfind_princolset, NULL, NULL },
/* WebDAV Quota (RFC 4331) properties */
{ "quota-available-bytes", NS_DAV, PROP_COLLECTION,
propfind_quota, NULL, NULL },
{ "quota-used-bytes", NS_DAV, PROP_COLLECTION,
propfind_quota, NULL, NULL },
/* WebDAV Current Principal (RFC 5397) properties */
{ "current-user-principal", NS_DAV, PROP_COLLECTION | PROP_RESOURCE,
propfind_curprin, NULL, NULL },
/* WebDAV POST (RFC 5995) properties */
{ "add-member", NS_DAV, PROP_COLLECTION,
propfind_addmember, NULL, NULL },
/* WebDAV Sync (RFC 6578) properties */
{ "sync-token", NS_DAV, PROP_COLLECTION,
propfind_sync_token, NULL, NULL },
/* CalDAV (RFC 4791) properties */
{ "calendar-data", NS_CALDAV,
PROP_RESOURCE | PROP_PRESCREEN | PROP_NEEDPROP,
propfind_caldata, NULL, NULL },
{ "calendar-description", NS_CALDAV, PROP_COLLECTION,
propfind_fromdb, proppatch_todb, NULL },
{ "calendar-timezone", NS_CALDAV,
PROP_COLLECTION | PROP_PRESCREEN | PROP_NEEDPROP,
propfind_tz_avail, proppatch_timezone, NULL },
{ "supported-calendar-component-set", NS_CALDAV, PROP_COLLECTION,
propfind_calcompset, proppatch_calcompset, NULL },
{ "supported-calendar-data", NS_CALDAV, PROP_COLLECTION,
propfind_suppcaldata, NULL, NULL },
{ "max-resource-size", NS_CALDAV, 0, NULL, NULL, NULL },
{ "min-date-time", NS_CALDAV, 0, NULL, NULL, NULL },
{ "max-date-time", NS_CALDAV, 0, NULL, NULL, NULL },
{ "max-instances", NS_CALDAV, 0, NULL, NULL, NULL },
{ "max-attendees-per-instance", NS_CALDAV, 0, NULL, NULL, NULL },
/* CalDAV Scheduling (RFC 6638) properties */
{ "schedule-tag", NS_CALDAV, PROP_RESOURCE,
propfind_schedtag, NULL, NULL },
{ "schedule-default-calendar-URL", NS_CALDAV, PROP_COLLECTION,
propfind_calurl, NULL, SCHED_DEFAULT },
{ "schedule-calendar-transp", NS_CALDAV, PROP_COLLECTION,
propfind_caltransp, proppatch_caltransp, NULL },
/* Calendar Availability (draft-daboo-calendar-availability) properties */
{ "calendar-availability", NS_CALDAV,
PROP_COLLECTION | PROP_PRESCREEN | PROP_NEEDPROP,
propfind_tz_avail, proppatch_availability, NULL },
/* RSCALE (draft-daboo-icalendar-rscale) properties */
{ "supported-rscale-set", NS_CALDAV, PROP_COLLECTION,
propfind_rscaleset, NULL, NULL },
/* Apple Calendar Server properties */
{ "getctag", NS_CS, PROP_ALLPROP | PROP_COLLECTION,
propfind_sync_token, NULL, NULL },
{ NULL, 0, 0, NULL, NULL, NULL }
};
static struct meth_params caldav_params = {
caldav_mime_types,
&caldav_parse_path,
&caldav_check_precond,
{ (db_open_proc_t) &my_caldav_open,
(db_close_proc_t) &my_caldav_close,
(db_lookup_proc_t) &caldav_lookup_resource,
(db_foreach_proc_t) &caldav_foreach,
(db_write_proc_t) &caldav_write,
(db_delete_proc_t) &caldav_delete,
(db_delmbox_proc_t) &caldav_delmbox },
&caldav_acl,
(copy_proc_t) &caldav_copy,
&caldav_delete_sched,
{ MBTYPE_CALENDAR, "mkcalendar", "mkcalendar-response", NS_CALDAV },
&caldav_post,
{ CALDAV_SUPP_DATA, (put_proc_t) &caldav_put },
caldav_props,
{ { "calendar-query", "multistatus", &report_cal_query,
DACL_READ, REPORT_NEED_MBOX },
{ "calendar-multiget", "multistatus", &report_cal_multiget,
DACL_READ, REPORT_NEED_MBOX },
{ "free-busy-query", NULL, &report_fb_query,
DACL_READFB, REPORT_NEED_MBOX },
{ "sync-collection", "multistatus", &report_sync_col,
DACL_READ, REPORT_NEED_MBOX | REPORT_NEED_PROPS },
{ NULL, NULL, NULL, 0, 0 } }
};
/* Namespace for CalDAV collections */
struct namespace_t namespace_calendar = {
URL_NS_CALENDAR, 0, "/dav/calendars", "/.well-known/caldav", 1 /* auth */,
(ALLOW_READ | ALLOW_POST | ALLOW_WRITE | ALLOW_DELETE |
#ifdef HAVE_VAVAILABILITY
ALLOW_CAL_AVAIL |
#endif
ALLOW_DAV | ALLOW_WRITECOL | ALLOW_CAL ),
&my_caldav_init, &my_caldav_auth, my_caldav_reset, &my_caldav_shutdown,
{
{ &meth_acl, &caldav_params }, /* ACL */
{ &meth_copy, &caldav_params }, /* COPY */
{ &meth_delete, &caldav_params }, /* DELETE */
{ &meth_get, &caldav_params }, /* GET */
{ &meth_get, &caldav_params }, /* HEAD */
{ &meth_lock, &caldav_params }, /* LOCK */
{ &meth_mkcol, &caldav_params }, /* MKCALENDAR */
{ &meth_mkcol, &caldav_params }, /* MKCOL */
{ &meth_copy, &caldav_params }, /* MOVE */
{ &meth_options, &caldav_parse_path }, /* OPTIONS */
{ &meth_post, &caldav_params }, /* POST */
{ &meth_propfind, &caldav_params }, /* PROPFIND */
{ &meth_proppatch, &caldav_params }, /* PROPPATCH */
{ &meth_put, &caldav_params }, /* PUT */
{ &meth_report, &caldav_params }, /* REPORT */
{ &meth_trace, &caldav_parse_path }, /* TRACE */
{ &meth_unlock, &caldav_params } /* UNLOCK */
}
};
static struct caldav_db *my_caldav_open(struct mailbox *mailbox)
{
if (httpd_userid && mboxname_userownsmailbox(httpd_userid, mailbox->name)) {
return auth_caldavdb;
}
else {
return caldav_open_mailbox(mailbox, CALDAV_CREATE);
}
}
static void my_caldav_close(struct caldav_db *caldavdb)
{
if (caldavdb && (caldavdb != auth_caldavdb)) caldav_close(caldavdb);
}
static void my_caldav_init(struct buf *serverinfo)
{
namespace_calendar.enabled =
config_httpmodules & IMAP_ENUM_HTTPMODULES_CALDAV;
if (!namespace_calendar.enabled) return;
if (!config_getstring(IMAPOPT_CALENDARPREFIX)) {
fatal("Required 'calendarprefix' option is not set", EC_CONFIG);
}
caldav_init();
buf_printf(serverinfo, " libical/%s", ICAL_VERSION);
#ifdef HAVE_RSCALE
buf_printf(serverinfo, " ICU/%s", U_ICU_VERSION);
#endif
#ifdef WITH_JSON
buf_printf(serverinfo, " Jansson/%s", JANSSON_VERSION);
#endif
config_allowsched = config_getenum(IMAPOPT_CALDAV_ALLOWSCHEDULING);
if (config_allowsched) {
namespace_calendar.allow |= ALLOW_CAL_SCHED;
#ifndef HAVE_SCHEDULING_PARAMS
/* Need to set this to parse CalDAV Scheduling parameters */
ical_set_unknown_token_handling_setting(ICAL_ASSUME_IANA_TOKEN);
#endif
}
compile_time = calc_compile_time(__TIME__, __DATE__);
buf_printf(&ical_prodid_buf,
"-//CyrusIMAP.org/Cyrus %s//EN", cyrus_version());
ical_prodid = buf_cstring(&ical_prodid_buf);
}
static void my_caldav_auth(const char *userid)
{
const char *mailboxname;
int r;
/* Generate mailboxname of calendar-home-set */
mailboxname = caldav_mboxname(userid, NULL);
if (httpd_userisadmin ||
global_authisa(httpd_authstate, IMAPOPT_PROXYSERVERS)) {
/* admin or proxy from frontend - won't have DAV database */
return;
}
else if (config_mupdate_server && !config_getstring(IMAPOPT_PROXYSERVERS)) {
/* proxy-only server - won't have DAV database */
}
else {
/* Open CalDAV DB for 'userid' */
my_caldav_reset();
auth_caldavdb = caldav_open_userid(userid, CALDAV_CREATE);
if (!auth_caldavdb) fatal("Unable to open CalDAV DB", EC_IOERR);
}
/* Auto-provision calendars for 'userid' */
/* calendar-home-set */
r = mboxlist_lookup(mailboxname, NULL, NULL);
if (r == IMAP_MAILBOX_NONEXISTENT) {
if (config_mupdate_server) {
/* Find location of INBOX */
const char *inboxname = mboxname_user_mbox(userid, NULL);
mbentry_t *mbentry = NULL;
r = http_mlookup(inboxname, &mbentry, NULL);
if (!r && mbentry->server) {
proxy_findserver(mbentry->server, &http_protocol, proxy_userid,
&backend_cached, NULL, NULL, httpd_in);
mboxlist_entry_free(&mbentry);
return;
}
mboxlist_entry_free(&mbentry);
}
/* will have been overwritten */
mailboxname = caldav_mboxname(userid, NULL);
/* Create locally */
r = mboxlist_createmailbox(mailboxname, MBTYPE_CALENDAR,
NULL, 0,
userid, httpd_authstate,
0, 0, 0, 0, NULL);
}
/* Default calendar */
mailboxname = caldav_mboxname(userid, SCHED_DEFAULT);
r = mboxlist_lookup(mailboxname, NULL, NULL);
if (r == IMAP_MAILBOX_NONEXISTENT) {
r = mboxlist_createmailbox(mailboxname, MBTYPE_CALENDAR,
NULL, 0,
userid, httpd_authstate,
0, 0, 0, 0, NULL);
}
/* Scheduling Inbox */
mailboxname = caldav_mboxname(userid, SCHED_INBOX);
r = mboxlist_lookup(mailboxname, NULL, NULL);
if (r == IMAP_MAILBOX_NONEXISTENT) {
r = mboxlist_createmailbox(mailboxname, MBTYPE_CALENDAR,
NULL, 0,
userid, httpd_authstate,
0, 0, 0, 0, NULL);
}
/* Scheduling Outbox */
mailboxname = caldav_mboxname(userid, SCHED_OUTBOX);
r = mboxlist_lookup(mailboxname, NULL, NULL);
if (r == IMAP_MAILBOX_NONEXISTENT) {
r = mboxlist_createmailbox(mailboxname, MBTYPE_CALENDAR,
NULL, 0,
userid, httpd_authstate,
0, 0, 0, 0, NULL);
}
}
static void my_caldav_reset(void)
{
if (auth_caldavdb) caldav_close(auth_caldavdb);
auth_caldavdb = NULL;
}
static void my_caldav_shutdown(void)
{
buf_free(&ical_prodid_buf);
caldav_done();
}
/* Parse request-target path in CalDAV namespace */
static int caldav_parse_path(const char *path,
struct request_target_t *tgt, const char **errstr)
{
char *p;
size_t len;
struct mboxname_parts parts;
struct buf boxbuf = BUF_INITIALIZER;
/* Make a working copy of target path */
strlcpy(tgt->path, path, sizeof(tgt->path));
tgt->tail = tgt->path + strlen(tgt->path);
p = tgt->path;
/* Sanity check namespace */
len = strlen(namespace_calendar.prefix);
if (strlen(p) < len ||
strncmp(namespace_calendar.prefix, p, len) ||
(path[len] && path[len] != '/')) {
*errstr = "Namespace mismatch request target path";
return HTTP_FORBIDDEN;
}
/* Default to bare-bones Allow bits for toplevel collections */
tgt->allow &= ~(ALLOW_POST|ALLOW_WRITE|ALLOW_DELETE);
/* Skip namespace */
p += len;
if (!*p || !*++p) return 0;
/* Check if we're in user space */
len = strcspn(p, "/");
if (!strncmp(p, "user", len)) {
p += len;
if (!*p || !*++p) return 0;
/* Get user id */
len = strcspn(p, "/");
tgt->user = p;
tgt->userlen = len;
p += len;
if (!*p || !*++p) {
/* Make sure calendar-home-set is terminated with '/' */
if (p[-1] != '/') *p++ = '/';
goto done;
}
len = strcspn(p, "/");
}
/* Get collection */
tgt->collection = p;
tgt->collen = len;
p += len;
if (!*p || !*++p) {
/* Make sure collection is terminated with '/' */
if (p[-1] != '/') *p++ = '/';
goto done;
}
/* Get resource */
len = strcspn(p, "/");
tgt->resource = p;
tgt->reslen = len;
p += len;
if (*p) {
// *errstr = "Too many segments in request target path";
return HTTP_NOT_FOUND;
}
done:
/* Set proper Allow bits and flags based on path components */
if (tgt->collection) {
if (!strncmp(tgt->collection, SCHED_INBOX, strlen(SCHED_INBOX)))
tgt->flags = TGT_SCHED_INBOX;
else if (!strncmp(tgt->collection, SCHED_OUTBOX, strlen(SCHED_OUTBOX)))
tgt->flags = TGT_SCHED_OUTBOX;
if (tgt->resource) {
if (!tgt->flags) tgt->allow |= ALLOW_WRITE;
tgt->allow |= ALLOW_DELETE;
tgt->allow &= ~ALLOW_WRITECOL;
}
else if (tgt->flags != TGT_SCHED_INBOX) {
tgt->allow |= ALLOW_POST;
if (strcmp(tgt->collection, SCHED_DEFAULT))
tgt->allow |= ALLOW_DELETE;
}
}
else if (tgt->user) tgt->allow |= ALLOW_DELETE;
/* Create mailbox name from the parsed path */
mboxname_init_parts(&parts);
if (tgt->user && tgt->userlen) {
/* holy "avoid copying" batman */
char *userid = xstrndup(tgt->user, tgt->userlen);
mboxname_userid_to_parts(userid, &parts);
free(userid);
}
buf_setcstr(&boxbuf, config_getstring(IMAPOPT_CALENDARPREFIX));
if (tgt->collen) {
buf_putc(&boxbuf, '.');
buf_appendmap(&boxbuf, tgt->collection, tgt->collen);
}
parts.box = buf_release(&boxbuf);
/* XXX - hack to allow @domain parts for non-domain-split users */
if (httpd_userid && !httpd_userisadmin) {
struct mboxname_parts userparts;
mboxname_init_parts(&userparts);
mboxname_userid_to_parts(httpd_userid, &userparts);
if (!parts.domain) {
/* super XXX hack for FastMail magic */
return HTTP_NOT_FOUND;
}
if (parts.domain && !userparts.domain) {
//free(parts.domain); - XXX - fix when converting to real parts
parts.domain = NULL;
}
mboxname_free_parts(&userparts);
}
mboxname_parts_to_internal(&parts, tgt->mboxname);
mboxname_free_parts(&parts);
return 0;
}
/* Check headers for any preconditions */
static int caldav_check_precond(struct transaction_t *txn, const void *data,
const char *etag, time_t lastmod)
{
const struct caldav_data *cdata = (const struct caldav_data *) data;
const char *stag = cdata ? cdata->sched_tag : NULL;
const char **hdr;
int precond;
/* Do normal WebDAV/HTTP checks (primarily for lock-token via If header) */
precond = check_precond(txn, data, etag, lastmod);
if (!(precond == HTTP_OK || precond == HTTP_PARTIAL)) return precond;
/* Per RFC 6638, check Schedule-Tag */
if ((hdr = spool_getheader(txn->req_hdrs, "If-Schedule-Tag-Match"))) {
/* Special case for Apple 'If-Schedule-Tag-Match:' with no value
* and also no schedule tag on the record - let that match */
if (cdata && !stag && !hdr[0][0]) return precond;
if (etagcmp(hdr[0], stag)) return HTTP_PRECOND_FAILED;
}
if (txn->meth == METH_GET || txn->meth == METH_HEAD) {
/* Fill in Schedule-Tag for successful GET/HEAD */
txn->resp_body.stag = stag;
}
return precond;
}
static int caldav_acl(struct transaction_t *txn, xmlNodePtr priv, int *rights)
{
if (!xmlStrcmp(priv->ns->href, BAD_CAST XML_NS_CALDAV)) {
/* CalDAV privileges */
switch (txn->req_tgt.flags) {
case TGT_SCHED_INBOX:
if (!xmlStrcmp(priv->name, BAD_CAST "schedule-deliver"))
*rights |= DACL_SCHED;
else if (!xmlStrcmp(priv->name, BAD_CAST "schedule-deliver-invite"))
*rights |= DACL_INVITE;
else if (!xmlStrcmp(priv->name, BAD_CAST "schedule-deliver-reply"))
*rights |= DACL_REPLY;
else if (!xmlStrcmp(priv->name, BAD_CAST "schedule-query-freebusy"))
*rights |= DACL_SCHEDFB;
else {
/* DAV:not-supported-privilege */
txn->error.precond = DAV_SUPP_PRIV;
}
break;
case TGT_SCHED_OUTBOX:
if (!xmlStrcmp(priv->name, BAD_CAST "schedule-send"))
*rights |= DACL_SCHED;
else if (!xmlStrcmp(priv->name, BAD_CAST "schedule-send-invite"))
*rights |= DACL_INVITE;
else if (!xmlStrcmp(priv->name, BAD_CAST "schedule-send-reply"))
*rights |= DACL_REPLY;
else if (!xmlStrcmp(priv->name, BAD_CAST "schedule-send-freebusy"))
*rights |= DACL_SCHEDFB;
else {
/* DAV:not-supported-privilege */
txn->error.precond = DAV_SUPP_PRIV;
}
break;
default:
if (xmlStrcmp(priv->name, BAD_CAST "read-free-busy"))
*rights |= DACL_READFB;
else {
/* DAV:not-supported-privilege */
txn->error.precond = DAV_SUPP_PRIV;
}
break;
}
/* Done processing this priv */
return 1;
}
else if (!xmlStrcmp(priv->ns->href, BAD_CAST XML_NS_DAV)) {
/* WebDAV privileges */
if (!xmlStrcmp(priv->name, BAD_CAST "all")) {
switch (txn->req_tgt.flags) {
case TGT_SCHED_INBOX:
/* DAV:all aggregates CALDAV:schedule-deliver */
*rights |= DACL_SCHED;
break;
case TGT_SCHED_OUTBOX:
/* DAV:all aggregates CALDAV:schedule-send */
*rights |= DACL_SCHED;
break;
default:
/* DAV:all aggregates CALDAV:read-free-busy */
*rights |= DACL_READFB;
break;
}
}
else if (!xmlStrcmp(priv->name, BAD_CAST "read")) {
switch (txn->req_tgt.flags) {
case TGT_SCHED_INBOX:
case TGT_SCHED_OUTBOX:
break;
default:
/* DAV:read aggregates CALDAV:read-free-busy */
*rights |= DACL_READFB;
break;
}
}
}
/* Process this priv in meth_acl() */
return 0;
}
/* Perform a COPY/MOVE request
*
* preconditions:
* CALDAV:supported-calendar-data
* CALDAV:valid-calendar-data
* CALDAV:valid-calendar-object-resource
* CALDAV:supported-calendar-component
* CALDAV:no-uid-conflict (DAV:href)
* CALDAV:calendar-collection-location-ok
* CALDAV:max-resource-size
* CALDAV:min-date-time
* CALDAV:max-date-time
* CALDAV:max-instances
* CALDAV:max-attendees-per-instance
*/
static int caldav_copy(struct transaction_t *txn,
struct mailbox *src_mbox, struct index_record *src_rec,
struct mailbox *dest_mbox, const char *dest_rsrc,
struct caldav_db *dest_davdb,
unsigned overwrite, unsigned flags)
{
int r;
const char *organizer = NULL;
icalcomponent *ical, *comp;
icalproperty *prop;
const char *base;
size_t len;
/* Load message containing the resource and parse iCal data */
r = mailbox_map_message(src_mbox, src_rec->uid, &base, &len);
if (r) return r;
ical = icalparser_parse_string(base + src_rec->header_size);
mailbox_unmap_message(src_mbox, src_rec->uid, &base, &len);
if (!ical) {
txn->error.precond = CALDAV_VALID_DATA;
return HTTP_FORBIDDEN;
}
/* Finished our initial read of source mailbox */
mailbox_unlock_index(src_mbox, NULL);
if (namespace_calendar.allow & ALLOW_CAL_SCHED) {
comp = icalcomponent_get_first_real_component(ical);
prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
if (prop) organizer = icalproperty_get_organizer(prop);
if (organizer) flags |= NEW_STAG;
}
/* Store source resource at destination */
r = store_resource(txn, ical, dest_mbox, dest_rsrc, dest_davdb,
overwrite, flags);
icalcomponent_free(ical);
return r;
}
/* Perform scheduling actions for a DELETE request */
static int caldav_delete_sched(struct transaction_t *txn,
struct mailbox *mailbox,
struct index_record *record, void *data)
{
struct caldav_data *cdata = (struct caldav_data *) data;
int r = 0;
if (!(namespace_calendar.allow & ALLOW_CAL_SCHED)) return 0;
/* Only process deletes on regular calendar collections */
if (txn->req_tgt.flags) return 0;
if (!record) {
/* XXX DELETE collection - check all resources for sched objects */
}
else if (cdata->sched_tag) {
/* Scheduling object resource */
const char *userid, *organizer, **hdr;
icalcomponent *ical, *comp;
icalproperty *prop;
struct sched_param sparam;
const char *base;
size_t len;
/* Load message containing the resource and parse iCal data */
r = mailbox_map_message(mailbox, record->uid, &base, &len);
if (r) return r;
ical = icalparser_parse_string(base + record->header_size);
mailbox_unmap_message(mailbox, record->uid, &base, &len);
if (!ical) {
syslog(LOG_ERR,
"meth_delete: failed to parse iCalendar object %s:%u",
txn->req_tgt.mboxname, record->uid);
return HTTP_SERVER_ERROR;
}
/* Construct userid corresponding to mailbox */
userid = mboxname_to_userid(txn->req_tgt.mboxname);
/* Grab the organizer */
comp = icalcomponent_get_first_real_component(ical);
prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
organizer = icalproperty_get_organizer(prop);
if (caladdress_lookup(organizer, &sparam)) {
syslog(LOG_ERR,
"meth_delete: failed to process scheduling message in %s"
" (org=%s, att=%s)",
txn->req_tgt.mboxname, organizer, userid);
txn->error.desc = "Failed to lookup organizer address\r\n";
r = HTTP_SERVER_ERROR;
goto done;
}
if (!strcmp(sparam.userid, userid)) {
/* Organizer scheduling object resource */
sched_request(organizer, &sparam, ical, NULL, 0);
}
else if (!(hdr = spool_getheader(txn->req_hdrs, "Schedule-Reply")) ||
strcmp(hdr[0], "F")) {
/* Attendee scheduling object resource */
sched_reply(userid, ical, NULL);
}
done:
icalcomponent_free(ical);
}
return r;
}
static const char *begin_icalendar(struct buf *buf)
{
/* Begin iCalendar stream */
buf_setcstr(buf, "BEGIN:VCALENDAR\r\n");
buf_appendcstr(buf, ical_prodid);
buf_appendcstr(buf, "VERSION:2.0\r\n");
return "";
}
static void end_icalendar(struct buf *buf)
{
/* End iCalendar stream */
buf_setcstr(buf, "END:VCALENDAR\r\n");
}
static int dump_calendar(struct transaction_t *txn, struct meth_params *gparams)
{
int ret = 0, r, precond;
struct resp_body_t *resp_body = &txn->resp_body;
struct buf *buf = &resp_body->payload;
struct mailbox *mailbox = NULL;
static char etag[33];
uint32_t recno;
struct index_record record;
struct hash_table tzid_table;
static const char *displayname_annot =
ANNOT_NS "<" XML_NS_DAV ">displayname";
struct buf attrib = BUF_INITIALIZER;
const char **hdr, *sep;
struct mime_type_t *mime = NULL;
/* Check requested MIME type:
1st entry in caldav_mime_types array MUST be default MIME type */
if ((hdr = spool_getheader(txn->req_hdrs, "Accept")))
mime = get_accept_type(hdr, caldav_mime_types);
else mime = caldav_mime_types;
if (!mime) return HTTP_NOT_ACCEPTABLE;
/* Open mailbox for reading */
r = mailbox_open_irl(txn->req_tgt.mboxname, &mailbox);
if (r) {
syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s",
txn->req_tgt.mboxname, error_message(r));
txn->error.desc = error_message(r);
ret = HTTP_SERVER_ERROR;
goto done;
}
/* Check any preconditions */
sprintf(etag, "%u-%u-%u",
mailbox->i.uidvalidity, mailbox->i.last_uid, mailbox->i.exists);
precond = gparams->check_precond(txn, NULL, etag, mailbox->index_mtime);
switch (precond) {
case HTTP_OK:
case HTTP_NOT_MODIFIED:
/* Fill in ETag, Last-Modified, Expires, and Cache-Control */
txn->resp_body.etag = etag;
txn->resp_body.lastmod = mailbox->index_mtime;
txn->resp_body.maxage = 3600; /* 1 hr */
txn->flags.cc |= CC_MAXAGE | CC_REVALIDATE; /* don't use stale data */
if (precond != HTTP_NOT_MODIFIED) break;
default:
/* We failed a precondition - don't perform the request */
ret = precond;
goto done;
}
/* Setup for chunked response */
txn->flags.te |= TE_CHUNKED;
txn->flags.vary |= VARY_ACCEPT;
txn->resp_body.type = mime->content_type;
/* Set filename of resource */
r = annotatemore_lookup(mailbox->name, displayname_annot, /* shared */ NULL, &attrib);
if (r || !attrib.len) buf_setcstr(&attrib, strrchr(mailbox->name, '.') + 1);
buf_reset(&txn->buf);
buf_printf(&txn->buf, "%s.%s", buf_cstring(&attrib), mime->file_ext);
txn->resp_body.fname = buf_cstring(&txn->buf);
/* Short-circuit for HEAD request */
if (txn->meth == METH_HEAD) {
response_header(HTTP_OK, txn);
return 0;
}
/* iCalendar data in response should not be transformed */
txn->flags.cc |= CC_NOTRANSFORM;
/* Create hash table for TZIDs */
construct_hash_table(&tzid_table, 10, 1);
/* Begin (converted) iCalendar stream */
sep = mime->begin_stream(buf);
write_body(HTTP_OK, txn, buf_cstring(buf), buf_len(buf));
for (r = 0, recno = 1; recno <= mailbox->i.num_records; recno++) {
const char *data;
size_t len;
icalcomponent *ical;
if (mailbox_read_index_record(mailbox, recno, &record)) continue;
if (record.system_flags & (FLAG_EXPUNGED | FLAG_DELETED)) continue;
/* Map and parse existing iCalendar resource */
if (mailbox_map_message(mailbox, record.uid, &data, &len)) continue;
ical = icalparser_parse_string(data + record.header_size);
mailbox_unmap_message(mailbox, record.uid, &data, &len);
if (ical) {
icalcomponent *comp;
for (comp = icalcomponent_get_first_component(ical,
ICAL_ANY_COMPONENT);
comp;
comp = icalcomponent_get_next_component(ical,
ICAL_ANY_COMPONENT)) {
char *cal_str;
icalcomponent_kind kind = icalcomponent_isa(comp);
/* Don't duplicate any TZIDs in our iCalendar */
if (kind == ICAL_VTIMEZONE_COMPONENT) {
icalproperty *prop =
icalcomponent_get_first_property(comp,
ICAL_TZID_PROPERTY);
const char *tzid = icalproperty_get_tzid(prop);
if (hash_lookup(tzid, &tzid_table)) continue;
else hash_insert(tzid, (void *)0xDEADBEEF, &tzid_table);
}
/* Include this component in our iCalendar */
if (r++ && *sep) {
/* Add separator, if necessary */
buf_reset(buf);
buf_printf_markup(buf, 0, sep);
write_body(0, txn, buf_cstring(buf), buf_len(buf));
}
cal_str = mime->to_string(comp);
write_body(0, txn, cal_str, strlen(cal_str));
free(cal_str);
}
icalcomponent_free(ical);
}
}
free_hash_table(&tzid_table, NULL);
/* End (converted) iCalendar stream */
mime->end_stream(buf);
write_body(0, txn, buf_cstring(buf), buf_len(buf));
/* End of output */
write_body(0, txn, NULL, 0);
done:
buf_free(&attrib);
mailbox_close(&mailbox);
return ret;
}
/*
* mboxlist_findall() callback function to list calendars
*/
static int list_cb(char *name,
int matchlen __attribute__((unused)),
int maycreate __attribute__((unused)),
void *rock)
{
struct transaction_t *txn = (struct transaction_t *) rock;
struct buf *body = &txn->resp_body.payload;
struct buf *url = &txn->buf;
static size_t inboxlen = 0;
static size_t outboxlen = 0;
char *shortname;
mbentry_t *mbentry = NULL;
size_t len;
int r;
static const char *displayname_annot =
ANNOT_NS "<" XML_NS_DAV ">displayname";
struct buf displayname = BUF_INITIALIZER;
if (!inboxlen) inboxlen = strlen(SCHED_INBOX) - 1;
if (!outboxlen) outboxlen = strlen(SCHED_OUTBOX) - 1;
shortname = strrchr(name, '.') + 1;
len = strlen(shortname);
/* Don't list scheduling Inbox/Outbox */
if ((len == inboxlen && !strncmp(shortname, SCHED_INBOX, inboxlen)) ||
(len == outboxlen && !strncmp(shortname, SCHED_OUTBOX, outboxlen)))
goto done;
/* Don't list deleted mailboxes */
if (mboxname_isdeletedmailbox(name, 0)) goto done;
/* Lookup the mailbox and make sure its readable */
r = http_mlookup(name, &mbentry, NULL);
if (r || !mbentry->acl || !(cyrus_acl_myrights(httpd_authstate, mbentry->acl) & ACL_READ))
goto done;
/* Send a body chunk once in a while */
if (buf_len(body) > PROT_BUFSIZE) {
write_body(0, txn, buf_cstring(body), buf_len(body));
buf_reset(body);
}
/* Lookup DAV:displayname */
r = annotatemore_lookup(name, displayname_annot,
/* shared */ NULL, &displayname);
if (r || !displayname.len) buf_setcstr(&displayname, shortname);
/* Add available calendar with link */
len = buf_len(url);
buf_printf_markup(body, 3, "<li><a href=\"%s%s\">%s</a></li>",
buf_cstring(url), shortname, buf_cstring(&displayname));
done:
buf_free(&displayname);
mboxlist_entry_free(&mbentry);
return 0;
}
/* Create a HTML document listing all calendars available to the user */
static int list_calendars(struct transaction_t *txn,
struct meth_params *gparams)
{
int ret = 0, precond;
time_t lastmod = compile_time;
char mboxlist[MAX_MAILBOX_PATH+1];
struct stat sbuf;
static char etag[63];
unsigned level = 0;
struct buf *body = &txn->resp_body.payload;
const char *proto = NULL, *host = NULL;
/* stat() mailboxes.db for Last-Modified and ETag */
snprintf(mboxlist, MAX_MAILBOX_PATH, "%s%s", config_dir, FNAME_MBOXLIST);
stat(mboxlist, &sbuf);
lastmod = MAX(compile_time, sbuf.st_mtime);
sprintf(etag, "%ld-%ld-%ld", compile_time, sbuf.st_mtime, sbuf.st_size);
/* Check any preconditions */
precond = gparams->check_precond(txn, NULL, etag, lastmod);
switch (precond) {
case HTTP_OK:
case HTTP_NOT_MODIFIED:
/* Fill in ETag, Last-Modified, and Expires */
txn->resp_body.etag = etag;
txn->resp_body.lastmod = lastmod;
txn->resp_body.maxage = 86400; /* 24 hrs */
txn->flags.cc |= CC_MAXAGE;
if (precond != HTTP_NOT_MODIFIED) break;
default:
/* We failed a precondition - don't perform the request */
ret = precond;
goto done;
}
/* Setup for chunked response */
txn->flags.te |= TE_CHUNKED;
txn->resp_body.type = "text/html; charset=utf-8";
/* Short-circuit for HEAD request */
if (txn->meth == METH_HEAD) {
response_header(HTTP_OK, txn);
goto done;
}
/* Send HTML header */
buf_reset(body);
buf_printf_markup(body, level, HTML_DOCTYPE);
buf_printf_markup(body, level++, "<html>");
buf_printf_markup(body, level++, "<head>");
buf_printf_markup(body, level, "<title>%s</title>", "Available Calendars");
buf_printf_markup(body, --level, "</head>");
buf_printf_markup(body, level++, "<body>");
buf_printf_markup(body, level, "<h2>%s</h2>", "Available Calendars");
buf_printf_markup(body, level++, "<ul>");
write_body(HTTP_OK, txn, buf_cstring(body), buf_len(body));
buf_reset(body);
/* Create base URL for calendars */
http_proto_host(txn->req_hdrs, &proto, &host);
assert(!buf_len(&txn->buf));
buf_printf(&txn->buf, "%s://%s%s", proto, host, txn->req_tgt.path);
/* Generate list of calendars */
strlcat(txn->req_tgt.mboxname, ".%", sizeof(txn->req_tgt.mboxname));
mboxlist_findall(NULL, txn->req_tgt.mboxname, 1, httpd_userid,
httpd_authstate, list_cb, txn);
if (buf_len(body)) write_body(0, txn, buf_cstring(body), buf_len(body));
/* Finish HTML */
buf_reset(body);
buf_printf_markup(body, --level, "</ul>");
buf_printf_markup(body, --level, "</body>");
buf_printf_markup(body, --level, "</html>");
write_body(0, txn, buf_cstring(body), buf_len(body));
/* End of output */
write_body(0, txn, NULL, 0);
done:
return ret;
}
/* Perform a GET/HEAD request on a CalDAV resource */
static int meth_get(struct transaction_t *txn, void *params)
{
struct meth_params *gparams = (struct meth_params *) params;
int r, rights;
mbentry_t *mbentry = NULL;
/* Parse the path */
if ((r = gparams->parse_path(txn->req_uri->path,
&txn->req_tgt, &txn->error.desc))) return r;
/* GET an individual resource */
if (txn->req_tgt.resource) return meth_get_dav(txn, gparams);
/* Locate the mailbox */
r = http_mlookup(txn->req_tgt.mboxname, &mbentry, NULL);
if (r) {
syslog(LOG_ERR, "mlookup(%s) failed: %s",
txn->req_tgt.mboxname, error_message(r));
txn->error.desc = error_message(r);
switch (r) {
case IMAP_PERMISSION_DENIED: return HTTP_FORBIDDEN;
case IMAP_MAILBOX_NONEXISTENT: return HTTP_NOT_FOUND;
default: return HTTP_SERVER_ERROR;
}
}
/* Check ACL for current user */
rights = mbentry->acl ? cyrus_acl_myrights(httpd_authstate, mbentry->acl) : 0;
if ((rights & DACL_READ) != DACL_READ) {
/* DAV:need-privileges */
txn->error.precond = DAV_NEED_PRIVS;
txn->error.resource = txn->req_tgt.path;
txn->error.rights = DACL_READ;
mboxlist_entry_free(&mbentry);
return HTTP_FORBIDDEN;
}
if (mbentry->server) {
/* Remote mailbox */
struct backend *be;
be = proxy_findserver(mbentry->server, &http_protocol, proxy_userid,
&backend_cached, NULL, NULL, httpd_in);
mboxlist_entry_free(&mbentry);
if (!be) return HTTP_UNAVAILABLE;
return http_pipe_req_resp(be, txn);
}
mboxlist_entry_free(&mbentry);
/* Local Mailbox */
/* Get an entire calendar collection */
if (txn->req_tgt.collection) return dump_calendar(txn, gparams);
/* GET a list of calendars under calendar-home-set */
else return list_calendars(txn, gparams);
}
/* Perform a busy time request, if necessary */
static int caldav_post(struct transaction_t *txn)
{
int ret = 0, r, rights;
char orgid[MAX_MAILBOX_NAME+1] = "";
mbentry_t *mbentry = NULL;
const char **hdr;
struct mime_type_t *mime = NULL;
icalcomponent *ical = NULL, *comp;
icalcomponent_kind kind = 0;
icalproperty_method meth = 0;
icalproperty *prop = NULL;
const char *uid = NULL, *organizer = NULL;
struct sched_param sparam;
if (!(namespace_calendar.allow & ALLOW_CAL_SCHED) || !txn->req_tgt.flags) {
/* POST to regular calendar collection */
return HTTP_CONTINUE;
}
else if (txn->req_tgt.flags == TGT_SCHED_INBOX) {
/* Don't allow POST to schedule-inbox */
return HTTP_NOT_ALLOWED;
}
/* POST to schedule-outbox */
/* Check Content-Type */
if ((hdr = spool_getheader(txn->req_hdrs, "Content-Type"))) {
for (mime = caldav_mime_types; mime->content_type; mime++) {
if (is_mediatype(mime->content_type, hdr[0])) break;
}
}
if (!mime || !mime->content_type) {
txn->error.precond = CALDAV_SUPP_DATA;
return HTTP_BAD_REQUEST;
}
/* Locate the mailbox */
r = http_mlookup(txn->req_tgt.mboxname, &mbentry, NULL);
if (r) {
syslog(LOG_ERR, "mlookup(%s) failed: %s",
txn->req_tgt.mboxname, error_message(r));
txn->error.desc = error_message(r);
switch (r) {
case IMAP_PERMISSION_DENIED: return HTTP_FORBIDDEN;
case IMAP_MAILBOX_NONEXISTENT: return HTTP_NOT_FOUND;
default: return HTTP_SERVER_ERROR;
}
}
/* Get rights for current user */
rights = mbentry->acl ? cyrus_acl_myrights(httpd_authstate, mbentry->acl) : 0;
mboxlist_entry_free(&mbentry);
/* Read body */
txn->req_body.flags |= BODY_DECODE;
r = read_body(httpd_in, txn->req_hdrs, &txn->req_body, &txn->error.desc);
if (r) {
txn->flags.conn = CONN_CLOSE;
return r;
}
/* Make sure we have a body */
if (!buf_len(&txn->req_body.payload)) {
txn->error.desc = "Missing request body\r\n";
return HTTP_BAD_REQUEST;
}
/* Parse the iCal data for important properties */
ical = mime->from_string(buf_cstring(&txn->req_body.payload));
if (!ical || !icalrestriction_check(ical)) {
txn->error.precond = CALDAV_VALID_DATA;
ret = HTTP_BAD_REQUEST;
goto done;
}
meth = icalcomponent_get_method(ical);
comp = icalcomponent_get_first_real_component(ical);
if (comp) {
uid = icalcomponent_get_uid(comp);
kind = icalcomponent_isa(comp);
prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
}
/* Check method preconditions */
if (!meth || !uid || !prop) {
txn->error.precond = CALDAV_VALID_SCHED;
ret = HTTP_BAD_REQUEST;
goto done;
}
/* Organizer MUST be local to use CalDAV Scheduling */
organizer = icalproperty_get_organizer(prop);
if (organizer) {
if (!caladdress_lookup(organizer, &sparam) &&
!(sparam.flags & SCHEDTYPE_REMOTE)) {
strlcpy(orgid, sparam.userid, sizeof(orgid));
mboxname_hiersep_toexternal(&httpd_namespace, orgid, 0);
}
}
if (strncmp(orgid, txn->req_tgt.user, txn->req_tgt.userlen)) {
txn->error.precond = CALDAV_VALID_ORGANIZER;
ret = HTTP_FORBIDDEN;
goto done;
}
switch (kind) {
case ICAL_VFREEBUSY_COMPONENT:
if (meth == ICAL_METHOD_REQUEST)
if (!(rights & DACL_SCHEDFB)) {
/* DAV:need-privileges */
txn->error.precond = DAV_NEED_PRIVS;
txn->error.resource = txn->req_tgt.path;
txn->error.rights = DACL_SCHEDFB;
ret = HTTP_FORBIDDEN;
}
else ret = sched_busytime_query(txn, mime, ical);
else {
txn->error.precond = CALDAV_VALID_SCHED;
ret = HTTP_BAD_REQUEST;
}
break;
default:
txn->error.precond = CALDAV_VALID_SCHED;
ret = HTTP_BAD_REQUEST;
}
done:
if (ical) icalcomponent_free(ical);
return ret;
}
static const char *get_icalrestriction_errstr(icalcomponent *ical)
{
icalcomponent *comp;
for (comp = icalcomponent_get_first_component(ical, ICAL_ANY_COMPONENT);
comp;
comp = icalcomponent_get_next_component(ical, ICAL_ANY_COMPONENT)) {
icalproperty *prop =
icalcomponent_get_first_property(comp, ICAL_XLICERROR_PROPERTY);
if (prop) return icalproperty_get_xlicerror(prop);
}
return NULL;
}
/* Perform a PUT request
*
* preconditions:
* CALDAV:valid-calendar-data
* CALDAV:valid-calendar-object-resource
* CALDAV:supported-calendar-component
* CALDAV:no-uid-conflict (DAV:href)
* CALDAV:max-resource-size
* CALDAV:min-date-time
* CALDAV:max-date-time
* CALDAV:max-instances
* CALDAV:max-attendees-per-instance
*/
static int caldav_put(struct transaction_t *txn,
struct mime_type_t *mime,
struct mailbox *mailbox,
struct caldav_db *davdb,
unsigned flags)
{
int ret;
icalcomponent *ical = NULL, *comp, *nextcomp;
icalcomponent_kind kind;
icalproperty_kind recip_kind;
icalproperty *prop;
const char *uid, *organizer = NULL;
/* Parse and validate the iCal data */
ical = mime->from_string(buf_cstring(&txn->req_body.payload));
if (!ical || (icalcomponent_isa(ical) != ICAL_VCALENDAR_COMPONENT)) {
txn->error.precond = CALDAV_VALID_DATA;
ret = HTTP_FORBIDDEN;
goto done;
}
else if (icalcomponent_count_errors(ical) || !icalrestriction_check(ical)) {
txn->error.precond = CALDAV_VALID_OBJECT;
if ((txn->error.desc = get_icalrestriction_errstr(ical))) {
assert(!buf_len(&txn->buf));
buf_setcstr(&txn->buf, txn->error.desc);
txn->error.desc = buf_cstring(&txn->buf);
}
ret = HTTP_FORBIDDEN;
goto done;
}
comp = icalcomponent_get_first_real_component(ical);
#ifdef HAVE_RSCALE
/* Make sure we support the provided RSCALE in an RRULE */
prop = icalcomponent_get_first_property(comp, ICAL_RRULE_PROPERTY);
if (prop) {
struct icalrecurrencetype rt = icalproperty_get_rrule(prop);
if (*rt.rscale) {
UEnumeration *en;
UErrorCode stat = U_ZERO_ERROR;
const char *rscale;
en = ucal_getKeywordValuesForLocale("calendar", NULL, FALSE, &stat);
while ((rscale = uenum_next(en, NULL, &stat))) {
if (!strcasecmp(rscale, rt.rscale)) break;
}
uenum_close(en);
if (!rscale) {
txn->error.precond = CALDAV_SUPP_RSCALE;
ret = HTTP_FORBIDDEN;
goto done;
}
}
}
#endif /* HAVE_RSCALE */
/* Make sure iCal UIDs [and ORGANIZERs] in all components are the same */
kind = icalcomponent_isa(comp);
uid = icalcomponent_get_uid(comp);
prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
if (prop) organizer = icalproperty_get_organizer(prop);
while ((nextcomp =
icalcomponent_get_next_component(ical, kind))) {
const char *nextuid = icalcomponent_get_uid(nextcomp);
if (!nextuid || strcmp(uid, nextuid)) {
txn->error.precond = CALDAV_VALID_OBJECT;
ret = HTTP_FORBIDDEN;
goto done;
}
if (organizer) {
const char *nextorg = NULL;
prop = icalcomponent_get_first_property(nextcomp,
ICAL_ORGANIZER_PROPERTY);
if (prop) nextorg = icalproperty_get_organizer(prop);
if (!nextorg || strcmp(organizer, nextorg)) {
txn->error.precond = CALDAV_SAME_ORGANIZER;
ret = HTTP_FORBIDDEN;
goto done;
}
}
}
switch (kind) {
case ICAL_VEVENT_COMPONENT:
case ICAL_VTODO_COMPONENT:
case ICAL_VPOLL_COMPONENT:
recip_kind = (kind == ICAL_VPOLL_COMPONENT) ?
ICAL_VOTER_PROPERTY : ICAL_ATTENDEE_PROPERTY;
if ((namespace_calendar.allow & ALLOW_CAL_SCHED) && organizer
/* XXX Hack for Outlook */
&& icalcomponent_get_first_property(comp, recip_kind)) {
/* Scheduling object resource */
const char *userid;
struct caldav_data *cdata;
struct sched_param sparam;
icalcomponent *oldical = NULL;
/* Construct userid corresponding to mailbox */
userid = mboxname_to_userid(txn->req_tgt.mboxname);
/* Make sure iCal UID is unique for this user */
caldav_lookup_uid(davdb, uid, 0, &cdata);
/* XXX Check errors */
if (cdata->dav.mailbox &&
(strcmp(cdata->dav.mailbox, txn->req_tgt.mboxname) ||
strcmp(cdata->dav.resource, txn->req_tgt.resource))) {
buf_printf(&txn->buf, "%s/user/%s/%s/%s",
namespace_calendar.prefix,
userid, strrchr(cdata->dav.mailbox, '.')+1,
cdata->dav.resource);
txn->error.resource = buf_cstring(&txn->buf);
ret = HTTP_FORBIDDEN;
goto done;
}
if (cdata->organizer) {
/* Don't allow ORGANIZER to be changed */
const char *p = organizer;
if (!strncmp(p, "mailto:", 7)) p += 7;
if (strcmp(cdata->organizer, p)) {
ret = HTTP_FORBIDDEN;
goto done;
}
}
/* Lookup the organizer */
if (caladdress_lookup(organizer, &sparam)) {
syslog(LOG_ERR,
"meth_put: failed to process scheduling message in %s"
" (org=%s)",
txn->req_tgt.mboxname, organizer);
txn->error.desc = "Failed to lookup organizer address\r\n";
ret = HTTP_SERVER_ERROR;
goto done;
}
if (cdata->dav.imap_uid) {
/* Update existing object */
struct index_record record;
const char *data;
size_t len;
int r;
/* Load message containing the resource and parse iCal data */
r = mailbox_find_index_record(mailbox, cdata->dav.imap_uid, &record);
if (!r) r = mailbox_map_message(mailbox, record.uid, &data, &len);
if (r) {
txn->error.desc = "Failed to read record \r\n";
ret = HTTP_SERVER_ERROR;
goto done;
}
oldical = icalparser_parse_string(data + record.header_size);
mailbox_unmap_message(mailbox, record.uid, &data, &len);
}
if (!strcmp(sparam.userid, userid)) {
/* Organizer scheduling object resource */
sched_request(organizer, &sparam, oldical, ical, 0);
}
else {
/* Attendee scheduling object resource */
if (!oldical) {
/* Can't reply to a non-existent invitation */
ret = HTTP_FORBIDDEN;
goto done;
}
sched_reply(userid, oldical, ical);
}
if (oldical) icalcomponent_free(oldical);
flags |= NEW_STAG;
}
break;
default:
/* Nothing else to do */
break;
}
/* Store resource at target */
ret = store_resource(txn, ical, mailbox, txn->req_tgt.resource,
davdb, OVERWRITE_CHECK, flags);
if (flags & PREFER_REP) {
struct resp_body_t *resp_body = &txn->resp_body;
const char **hdr;
char *data;
if ((hdr = spool_getheader(txn->req_hdrs, "Accept")))
mime = get_accept_type(hdr, caldav_mime_types);
if (!mime) goto done;
switch (ret) {
case HTTP_NO_CONTENT:
ret = HTTP_OK;
case HTTP_CREATED:
/* Convert into requested MIME type */
data = mime->to_string(ical);
/* Fill in Content-Type, Content-Length */
resp_body->type = mime->content_type;
resp_body->len = strlen(data);
/* Fill in Content-Location */
resp_body->loc = txn->req_tgt.path;
/* Fill in Expires and Cache-Control */
resp_body->maxage = 3600; /* 1 hr */
txn->flags.cc = CC_MAXAGE
| CC_REVALIDATE /* don't use stale data */
| CC_NOTRANSFORM; /* don't alter iCal data */
/* Output current representation */
write_body(ret, txn, data, resp_body->len);
free(data);
ret = 0;
break;
default:
/* failure - do nothing */
break;
}
}
done:
if (ical) icalcomponent_free(ical);
return ret;
}
/* Append a new busytime period to the busytime array */
static void add_busytime(icalcomponent *comp, struct icaltime_span *span,
void *rock)
{
struct busytime *busytime = (struct busytime *) rock;
int is_date = icaltime_is_date(icalcomponent_get_dtstart(comp));
icaltimezone *utc = icaltimezone_get_utc_timezone();
struct icalperiodtype *newp;
/* Grow the array, if necessary */
if (busytime->len == busytime->alloc) {
busytime->alloc += 100; /* XXX arbitrary */
busytime->busy = xrealloc(busytime->busy,
busytime->alloc *
sizeof(struct icalperiodtype));
}
/* Add new busytime */
newp = &busytime->busy[busytime->len++];
newp->start = icaltime_from_timet_with_zone(span->start, is_date, utc);
newp->start.is_date = 0; /* MUST be DATE-TIME */
newp->end = icaltime_from_timet_with_zone(span->end, is_date, utc);
newp->end.is_date = 0; /* MUST be DATE-TIME */
newp->duration = icaldurationtype_null_duration();
}
/* See if the current resource matches the specified filter
* (comp-type and/or time-range). Returns 1 if match, 0 otherwise.
*/
static int apply_calfilter(struct propfind_ctx *fctx, void *data)
{
struct calquery_filter *calfilter =
(struct calquery_filter *) fctx->filter_crit;
struct caldav_data *cdata = (struct caldav_data *) data;
int match = 1;
if (calfilter->comp) {
/* Perform CALDAV:comp-filter filtering */
if (!(cdata->comp_type & calfilter->comp)) return 0;
}
/* Skip transparent resources */
if (calfilter->check_transp && cdata->transp) return 0;
if (!icaltime_is_null_time(calfilter->start)) {
/* Perform CALDAV:time-range filtering */
struct icaltimetype dtstart = icaltime_from_string(cdata->dtstart);
struct icaltimetype dtend = icaltime_from_string(cdata->dtend);
if (icaltime_compare(dtend, calfilter->start) <= 0) {
/* Component is earlier than range */
return 0;
}
else if (icaltime_compare(dtstart, calfilter->end) >= 0) {
/* Component is later than range */
return 0;
}
else if (!cdata->recurring && !calfilter->save_busytime) {
/* Component is within range, non-recurring,
and we don't need to save busytime */
return 1;
}
else {
/* Component is within range and recurring.
* Need to mmap() and parse iCalendar object
* to perform complete check of each recurrence.
*/
struct busytime *busytime = &calfilter->busytime;
const char *data;
size_t len;
icalcomponent *ical, *comp;
icalcomponent_kind kind;
icaltimezone *utc = icaltimezone_get_utc_timezone();
icaltime_span rangespan;
unsigned firstr, lastr;
/* XXX - error */
if (mailbox_map_message(fctx->mailbox, fctx->record->uid, &data, &len))
return 0;
ical = icalparser_parse_string(data + fctx->record->header_size);
mailbox_unmap_message(fctx->mailbox, fctx->record->uid, &data, &len);
comp = icalcomponent_get_first_real_component(ical);
kind = icalcomponent_isa(comp);
/* XXX This code assumes that the first VEVENT will contain
* the recurrence rule and the subsequent VEVENTs will
* be the overrides. Technically this doesn't have to be
* the case, but it appears to be true in practice.
*/
/* Create a span for the given time-range */
rangespan.start =
icaltime_as_timet_with_zone(calfilter->start, utc);
rangespan.end =
icaltime_as_timet_with_zone(calfilter->end, utc);
/* Mark start of where recurrences will be added */
firstr = busytime->len;
/* Add all recurring busytime in specified time-range */
icalcomponent_foreach_recurrence(comp,
calfilter->start, calfilter->end,
add_busytime, busytime);
/* Mark end of where recurrences were added */
lastr = busytime->len;
/* XXX Should we sort busytime array, so we can use bsearch()? */
/* Handle overridden recurrences */
while ((comp = icalcomponent_get_next_component(ical, kind))) {
icalproperty *prop;
struct icaltimetype recurid;
icalparameter *param;
icaltime_span recurspan;
unsigned n;
/* The *_get_recurrenceid() functions don't appear
to deal with timezones properly, so we do it ourselves */
prop =
icalcomponent_get_first_property(comp,
ICAL_RECURRENCEID_PROPERTY);
recurid = icalproperty_get_recurrenceid(prop);
param =
icalproperty_get_first_parameter(prop, ICAL_TZID_PARAMETER);
if (param) {
const char *tzid = icalparameter_get_tzid(param);
icaltimezone *tz = NULL;
tz = icalcomponent_get_timezone(ical, tzid);
if (!tz) {
tz = icaltimezone_get_builtin_timezone_from_tzid(tzid);
}
if (tz) icaltime_set_timezone(&recurid, tz);
}
recurid =
icaltime_convert_to_zone(recurid,
icaltimezone_get_utc_timezone());
/* Check if this overridden instance is in our array */
/* XXX Should we replace this linear search with bsearch() */
for (n = firstr; n < lastr; n++) {
if (!icaltime_compare(recurid,
busytime->busy[n].start)) {
/* Remove the instance
by sliding all future instances into its place */
/* XXX Doesn't handle the RANGE=THISANDFUTURE param */
busytime->len--;
memmove(&busytime->busy[n], &busytime->busy[n+1],
sizeof(struct icalperiodtype) *
(busytime->len - n));
lastr--;
break;
}
}
/* Check if the new instance is in our time-range */
recurspan = icaltime_span_new(icalcomponent_get_dtstart(comp),
icalcomponent_get_dtend(comp), 1);
if (icaltime_span_overlaps(&recurspan, &rangespan)) {
/* Add this instance to the array */
add_busytime(comp, &recurspan, busytime);
}
}
if (lastr == firstr) match = 0;
if (!calfilter->save_busytime) busytime->len = 0;
icalcomponent_free(ical);
}
}
return match;
}
static int is_valid_timerange(const struct icaltimetype start,
const struct icaltimetype end)
{
return (icaltime_is_valid_time(start) && icaltime_is_valid_time(end) &&
!icaltime_is_date(start) && !icaltime_is_date(end) &&
(icaltime_is_utc(start) || start.zone) &&
(icaltime_is_utc(end) || end.zone));
}
static int parse_comp_filter(xmlNodePtr root, struct calquery_filter *filter,
struct error_t *error)
{
int ret = 0;
xmlNodePtr node;
/* Parse elements of filter */
for (node = root; node; node = node->next) {
if (node->type == XML_ELEMENT_NODE) {
if (!xmlStrcmp(node->name, BAD_CAST "comp-filter")) {
xmlChar *name = xmlGetProp(node, BAD_CAST "name");
if (!filter->comp) {
if (!xmlStrcmp(name, BAD_CAST "VCALENDAR"))
filter->comp = CAL_COMP_VCALENDAR;
else {
error->precond = CALDAV_VALID_FILTER;
ret = HTTP_FORBIDDEN;
}
}
else if (filter->comp == CAL_COMP_VCALENDAR) {
if (!xmlStrcmp(name, BAD_CAST "VCALENDAR") ||
!xmlStrcmp(name, BAD_CAST "VALARM")) {
error->precond = CALDAV_VALID_FILTER;
ret = HTTP_FORBIDDEN;
}
else if (!xmlStrcmp(name, BAD_CAST "VEVENT"))
filter->comp |= CAL_COMP_VEVENT;
else if (!xmlStrcmp(name, BAD_CAST "VTODO"))
filter->comp |= CAL_COMP_VTODO;
else if (!xmlStrcmp(name, BAD_CAST "VJOURNAL"))
filter->comp |= CAL_COMP_VJOURNAL;
else if (!xmlStrcmp(name, BAD_CAST "VFREEBUSY"))
filter->comp |= CAL_COMP_VFREEBUSY;
else if (!xmlStrcmp(name, BAD_CAST "VTIMEZONE"))
filter->comp |= CAL_COMP_VTIMEZONE;
else if (!xmlStrcmp(name, BAD_CAST "VAVAILABILITY"))
filter->comp |= CAL_COMP_VAVAILABILITY;
else if (!xmlStrcmp(name, BAD_CAST "VPOLL"))
filter->comp |= CAL_COMP_VPOLL;
else {
error->precond = CALDAV_SUPP_FILTER;
ret = HTTP_FORBIDDEN;
}
}
else if (filter->comp & (CAL_COMP_VEVENT | CAL_COMP_VTODO)) {
if (!xmlStrcmp(name, BAD_CAST "VALARM"))
filter->comp |= CAL_COMP_VALARM;
else {
error->precond = CALDAV_VALID_FILTER;
ret = HTTP_FORBIDDEN;
}
}
else {
error->precond = CALDAV_SUPP_FILTER;
ret = HTTP_FORBIDDEN;
}
xmlFree(name);
if (!ret)
ret = parse_comp_filter(node->children, filter, error);
if (ret) return ret;
}
else if (!xmlStrcmp(node->name, BAD_CAST "time-range")) {
icaltimezone *utc = icaltimezone_get_utc_timezone();
xmlChar *start, *end;
if (!(filter->comp & (CAL_COMP_VEVENT | CAL_COMP_VTODO))) {
error->precond = CALDAV_VALID_FILTER;
return HTTP_FORBIDDEN;
}
start = xmlGetProp(node, BAD_CAST "start");
if (start) {
filter->start = icaltime_from_string((char *) start);
xmlFree(start);
}
else {
filter->start =
icaltime_from_timet_with_zone(INT_MIN, 0, utc);
}
end = xmlGetProp(node, BAD_CAST "end");
if (end) {
filter->end = icaltime_from_string((char *) end);
xmlFree(end);
}
else {
filter->end =
icaltime_from_timet_with_zone(INT_MAX, 0, utc);
}
if (!is_valid_timerange(filter->start, filter->end)) {
error->precond = CALDAV_VALID_FILTER;
return HTTP_FORBIDDEN;
}
}
else {
error->precond = CALDAV_SUPP_FILTER;
return HTTP_FORBIDDEN;
}
}
}
return ret;
}
/* Callback to fetch DAV:getcontenttype */
static int propfind_getcontenttype(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr resp __attribute__((unused)),
struct propstat propstat[],
void *rock __attribute__((unused)))
{
buf_setcstr(&fctx->buf, "text/calendar; charset=utf-8");
if (fctx->data) {
struct caldav_data *cdata = (struct caldav_data *) fctx->data;
const char *comp = NULL;
switch (cdata->comp_type) {
case CAL_COMP_VEVENT: comp = "VEVENT"; break;
case CAL_COMP_VTODO: comp = "VTODO"; break;
case CAL_COMP_VJOURNAL: comp = "VJOURNAL"; break;
case CAL_COMP_VFREEBUSY: comp = "VFREEBUSY"; break;
case CAL_COMP_VAVAILABILITY: comp = "VAVAILABILITY"; break;
case CAL_COMP_VPOLL: comp = "VPOLL"; break;
}
if (comp) buf_printf(&fctx->buf, "; component=%s", comp);
}
xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
name, ns, BAD_CAST buf_cstring(&fctx->buf), 0);
return 0;
}
/* Callback to fetch DAV:resourcetype */
static int propfind_restype(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr resp,
struct propstat propstat[],
void *rock __attribute__((unused)))
{
xmlNodePtr node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV],
&propstat[PROPSTAT_OK], name, ns, NULL, 0);
if (!fctx->record) {
xmlNewChild(node, NULL, BAD_CAST "collection", NULL);
if (fctx->req_tgt->collection) {
ensure_ns(fctx->ns, NS_CALDAV, resp->parent, XML_NS_CALDAV, "C");
if (!strcmp(fctx->req_tgt->collection, SCHED_INBOX)) {
xmlNewChild(node, fctx->ns[NS_CALDAV],
BAD_CAST "schedule-inbox", NULL);
}
else if (!strcmp(fctx->req_tgt->collection, SCHED_OUTBOX)) {
xmlNewChild(node, fctx->ns[NS_CALDAV],
BAD_CAST "schedule-outbox", NULL);
}
else {
xmlNewChild(node, fctx->ns[NS_CALDAV],
BAD_CAST "calendar", NULL);
}
}
}
return 0;
}
/* Callback to fetch DAV:supported-report-set */
static int propfind_reportset(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr resp,
struct propstat propstat[],
void *rock __attribute__((unused)))
{
xmlNodePtr s, r, top;
top = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
name, ns, NULL, 0);
if (fctx->req_tgt->collection && !fctx->req_tgt->resource) {
s = xmlNewChild(top, NULL, BAD_CAST "supported-report", NULL);
r = xmlNewChild(s, NULL, BAD_CAST "report", NULL);
xmlNewChild(r, fctx->ns[NS_DAV], BAD_CAST "sync-collection", NULL);
}
ensure_ns(fctx->ns, NS_CALDAV, resp->parent, XML_NS_CALDAV, "C");
s = xmlNewChild(top, NULL, BAD_CAST "supported-report", NULL);
r = xmlNewChild(s, NULL, BAD_CAST "report", NULL);
xmlNewChild(r, fctx->ns[NS_CALDAV], BAD_CAST "calendar-query", NULL);
s = xmlNewChild(top, NULL, BAD_CAST "supported-report", NULL);
r = xmlNewChild(s, NULL, BAD_CAST "report", NULL);
xmlNewChild(r, fctx->ns[NS_CALDAV], BAD_CAST "calendar-multiget", NULL);
s = xmlNewChild(top, NULL, BAD_CAST "supported-report", NULL);
r = xmlNewChild(s, NULL, BAD_CAST "report", NULL);
xmlNewChild(r, fctx->ns[NS_CALDAV], BAD_CAST "free-busy-query", NULL);
return 0;
}
/* Callback to prescreen/fetch CALDAV:calendar-data */
static int propfind_caldata(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr resp __attribute__((unused)),
struct propstat propstat[],
void *rock)
{
xmlNodePtr prop = (xmlNodePtr) rock;
const char *data = NULL;
size_t datalen = 0;
const char *map = NULL;
size_t maplen = 0;
int r;
if (propstat) {
if (!fctx->record) return HTTP_NOT_FOUND;
mailbox_map_message(fctx->mailbox, fctx->record->uid, &map, &maplen);
data = map + fctx->record->header_size;
datalen = maplen - fctx->record->header_size;
}
r = propfind_getdata(name, ns, fctx, propstat, prop, caldav_mime_types,
CALDAV_SUPP_DATA, data, datalen);
if (propstat)
mailbox_unmap_message(fctx->mailbox, fctx->record->uid, &map, &maplen);
return r;
}
/* Callback to fetch CALDAV:calendar-home-set,
* CALDAV:schedule-inbox-URL, CALDAV:schedule-outbox-URL,
* and CALDAV:schedule-default-calendar-URL
*/
int propfind_calurl(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr resp __attribute__((unused)),
struct propstat propstat[],
void *rock)
{
xmlNodePtr node;
const char *cal = (const char *) rock;
if (!(namespace_calendar.enabled && fctx->req_tgt->user))
return HTTP_NOT_FOUND;
/* sched-def-cal-URL only defined on sched-inbox-URL */
if (!xmlStrcmp(name, BAD_CAST "schedule-default-calendar-URL") &&
(!fctx->req_tgt->collection ||
strcmp(fctx->req_tgt->collection, SCHED_INBOX)))
return HTTP_NOT_FOUND;
node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
name, ns, NULL, 0);
buf_reset(&fctx->buf);
buf_printf(&fctx->buf, "%s/user/%.*s/%s", namespace_calendar.prefix,
(int) fctx->req_tgt->userlen, fctx->req_tgt->user,
cal ? cal : "");
xml_add_href(node, fctx->ns[NS_DAV], buf_cstring(&fctx->buf));
return 0;
}
/* Callback to fetch CALDAV:supported-calendar-component-set */
static const struct cal_comp_t {
const char *name;
unsigned long type;
} cal_comps[] = {
{ "VEVENT", CAL_COMP_VEVENT },
{ "VTODO", CAL_COMP_VTODO },
{ "VJOURNAL", CAL_COMP_VJOURNAL },
{ "VFREEBUSY", CAL_COMP_VFREEBUSY },
#ifdef HAVE_VAVAILABILITY
{ "VAVAILABILITY", CAL_COMP_VAVAILABILITY },
#endif
#ifdef HAVE_VPOLL
{ "VPOLL", CAL_COMP_VPOLL },
#endif
// { "VTIMEZONE", CAL_COMP_VTIMEZONE },
// { "VALARM", CAL_COMP_VALARM },
{ NULL, 0 }
};
static int propfind_calcompset(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr resp __attribute__((unused)),
struct propstat propstat[],
void *rock __attribute__((unused)))
{
const char *prop_annot = ANNOT_NS "CALDAV:supported-calendar-component-set";
struct buf attrib = BUF_INITIALIZER;
unsigned long types = 0;
xmlNodePtr set, node;
const struct cal_comp_t *comp;
int r = 0;
if (!fctx->req_tgt->collection) return HTTP_NOT_FOUND;
r = annotatemore_lookup(fctx->mailbox->name, prop_annot,
/* shared */ NULL, &attrib);
if (r) return HTTP_SERVER_ERROR;
if (attrib.len)
types = strtoul(buf_cstring(&attrib), NULL, 10);
else
types = -1; /* ALL components types */
buf_free(&attrib);
if (!types) return HTTP_NOT_FOUND;
set = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
name, ns, NULL, 0);
/* Create "comp" elements from the stored bitmask */
for (comp = cal_comps; comp->name; comp++) {
if (types & comp->type) {
node = xmlNewChild(set, fctx->ns[NS_CALDAV],
BAD_CAST "comp", NULL);
xmlNewProp(node, BAD_CAST "name", BAD_CAST comp->name);
}
}
return 0;
}
/* Callback to write supported-calendar-component-set property */
static int proppatch_calcompset(xmlNodePtr prop, unsigned set,
struct proppatch_ctx *pctx,
struct propstat propstat[],
void *rock __attribute__((unused)))
{
int r = 0;
unsigned precond = 0;
if (set && (pctx->meth == METH_MKCOL || pctx->meth == METH_MKCALENDAR)) {
/* "Writeable" for MKCOL/MKCALENDAR only */
xmlNodePtr cur;
unsigned long types = 0;
/* Work through the given list of components */
for (cur = prop->children; cur; cur = cur->next) {
xmlChar *name;
const struct cal_comp_t *comp;
/* Make sure its a "comp" element with a "name" */
if (cur->type != XML_ELEMENT_NODE) continue;
if (xmlStrcmp(cur->name, BAD_CAST "comp") ||
!(name = xmlGetProp(cur, BAD_CAST "name"))) break;
/* Make sure we have a valid component type */
for (comp = cal_comps;
comp->name && xmlStrcmp(name, BAD_CAST comp->name); comp++);
xmlFree(name);
if (comp->name) types |= comp->type; /* found match in our list */
else break; /* no match - invalid type */
}
if (!cur) {
/* All component types are valid */
const char *prop_annot =
ANNOT_NS "CALDAV:supported-calendar-component-set";
annotate_state_t *astate = NULL;
buf_reset(&pctx->buf);
buf_printf(&pctx->buf, "%lu", types);
r = mailbox_get_annotate_state(pctx->mailbox, 0, &astate);
if (!r) r = annotate_state_write(astate, prop_annot, /*userid*/NULL, &pctx->buf);
if (!r) {
xml_add_prop(HTTP_OK, pctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
prop->name, prop->ns, NULL, 0);
}
else {
xml_add_prop(HTTP_SERVER_ERROR, pctx->ns[NS_DAV],
&propstat[PROPSTAT_ERROR],
prop->name, prop->ns, NULL, 0);
}
return 0;
}
/* Invalid component type */
precond = CALDAV_SUPP_COMP;
}
else {
/* Protected property */
precond = DAV_PROT_PROP;
}
xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV], &propstat[PROPSTAT_FORBID],
prop->name, prop->ns, NULL, precond);
*pctx->ret = HTTP_FORBIDDEN;
return 0;
}
/* Callback to fetch CALDAV:supported-calendar-data */
static int propfind_suppcaldata(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr resp __attribute__((unused)),
struct propstat propstat[],
void *rock __attribute__((unused)))
{
xmlNodePtr node;
struct mime_type_t *mime;
if (!fctx->req_tgt->collection) return HTTP_NOT_FOUND;
node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
name, ns, NULL, 0);
assert(!buf_len(&fctx->buf));
for (mime = caldav_mime_types; mime->content_type; mime++) {
xmlNodePtr type = xmlNewChild(node, fctx->ns[NS_CALDAV],
BAD_CAST "calendar-data", NULL);
/* Trim any charset from content-type */
buf_reset(&fctx->buf);
buf_printf(&fctx->buf, "%.*s",
(int) strcspn(mime->content_type, ";"), mime->content_type);
xmlNewProp(type, BAD_CAST "content-type",
BAD_CAST buf_cstring(&fctx->buf));
if (mime->version)
xmlNewProp(type, BAD_CAST "version", BAD_CAST mime->version);
}
buf_reset(&fctx->buf);
return 0;
}
/* Callback to fetch CALDAV:schedule-tag */
static int propfind_schedtag(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr resp __attribute__((unused)),
struct propstat propstat[],
void *rock __attribute__((unused)))
{
struct caldav_data *cdata = (struct caldav_data *) fctx->data;
if (!cdata->sched_tag) return HTTP_NOT_FOUND;
/* add DQUOTEs */
buf_reset(&fctx->buf);
buf_printf(&fctx->buf, "\"%s\"", cdata->sched_tag);
xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
name, ns, BAD_CAST buf_cstring(&fctx->buf), 0);
return 0;
}
/* Callback to fetch CALDAV:calendar-user-address-set */
int propfind_caluseraddr(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr resp __attribute__((unused)),
struct propstat propstat[],
void *rock __attribute__((unused)))
{
xmlNodePtr node;
if (!(namespace_calendar.enabled && fctx->req_tgt->user))
return HTTP_NOT_FOUND;
node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
name, ns, NULL, 0);
/* XXX This needs to be done via an LDAP/DB lookup */
buf_reset(&fctx->buf);
buf_printf(&fctx->buf, "mailto:%.*s@%s", (int) fctx->req_tgt->userlen,
fctx->req_tgt->user, config_servername);
xmlNewChild(node, fctx->ns[NS_DAV], BAD_CAST "href",
BAD_CAST buf_cstring(&fctx->buf));
return 0;
}
/* Callback to fetch CALDAV:calendar-user-type */
int propfind_calusertype(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr resp __attribute__((unused)),
struct propstat propstat[],
void *rock __attribute__((unused)))
{
const char *type = fctx->req_tgt->user ? "INDIVIDUAL" : "UNKNOWN";
if (!namespace_calendar.enabled) return HTTP_NOT_FOUND;
xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
name, ns, BAD_CAST type, 0);
return 0;
}
/* Callback to fetch CALDAV:schedule-calendar-transp */
static int propfind_caltransp(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr resp __attribute__((unused)),
struct propstat propstat[],
void *rock __attribute__((unused)))
{
const char *prop_annot = ANNOT_NS "CALDAV:schedule-calendar-transp";
struct buf attrib = BUF_INITIALIZER;
xmlNodePtr node;
int r = 0;
if (!fctx->req_tgt->collection) return HTTP_NOT_FOUND;
r = annotatemore_lookup(fctx->mailbox->name, prop_annot,
/* shared */ NULL, &attrib);
if (r) return HTTP_SERVER_ERROR;
if (!attrib.len) return HTTP_NOT_FOUND;
node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
name, ns, NULL, 0);
xmlNewChild(node, fctx->ns[NS_CALDAV], BAD_CAST buf_cstring(&attrib), NULL);
buf_free(&attrib);
return 0;
}
/* Callback to write schedule-calendar-transp property */
static int proppatch_caltransp(xmlNodePtr prop, unsigned set,
struct proppatch_ctx *pctx,
struct propstat propstat[],
void *rock __attribute__((unused)))
{
int r;
if (pctx->req_tgt->collection && !pctx->req_tgt->resource) {
const char *prop_annot =
ANNOT_NS "CALDAV:schedule-calendar-transp";
annotate_state_t *astate = NULL;
buf_reset(&pctx->buf);
if (set) {
xmlNodePtr cur;
/* Find the value */
for (cur = prop->children; cur; cur = cur->next) {
/* Make sure its a value we understand */
if (cur->type != XML_ELEMENT_NODE) continue;
if (!xmlStrcmp(cur->name, BAD_CAST "opaque") ||
!xmlStrcmp(cur->name, BAD_CAST "transparent")) {
buf_setcstr(&pctx->buf, (const char *)cur->name);
break;
}
else {
/* Unknown value */
xml_add_prop(HTTP_CONFLICT, pctx->ns[NS_DAV],
&propstat[PROPSTAT_CONFLICT],
prop->name, prop->ns, NULL, 0);
*pctx->ret = HTTP_FORBIDDEN;
return 0;
}
}
}
r = mailbox_get_annotate_state(pctx->mailbox, 0, &astate);
if (!r) r = annotate_state_write(astate, prop_annot, /*userid*/NULL, &pctx->buf);
if (!r) {
xml_add_prop(HTTP_OK, pctx->ns[NS_DAV],
&propstat[PROPSTAT_OK], prop->name, prop->ns, NULL, 0);
}
else {
xml_add_prop(HTTP_SERVER_ERROR, pctx->ns[NS_DAV],
&propstat[PROPSTAT_ERROR],
prop->name, prop->ns, NULL, 0);
}
}
else {
xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV],
&propstat[PROPSTAT_FORBID],
prop->name, prop->ns, NULL, 0);
*pctx->ret = HTTP_FORBIDDEN;
}
return 0;
}
/* Callback to prescreen/fetch CALDAV:calendar-timezone/availability */
static int propfind_tz_avail(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr resp __attribute__((unused)),
struct propstat propstat[],
void *rock)
{
xmlNodePtr prop = (xmlNodePtr) rock;
struct buf attrib = BUF_INITIALIZER;
int ret = 0;
if (propstat) {
int r = 0;
buf_reset(&fctx->buf);
buf_printf(&fctx->buf, ANNOT_NS "<%s>%s",
(const char *) ns->href, name);
if (fctx->mailbox && !fctx->record) {
r = annotatemore_lookup(fctx->mailbox->name,
buf_cstring(&fctx->buf),
/* shared */ NULL, &attrib);
}
if (r) {
ret = HTTP_SERVER_ERROR;
goto done;
}
if (!attrib.len) {
ret = HTTP_NOT_FOUND;
goto done;
}
}
buf_cstring(&attrib); /* ensure a valid pointer */
ret = propfind_getdata(name, ns, fctx, propstat, prop, caldav_mime_types,
CALDAV_SUPP_DATA, attrib.s, attrib.len);
done:
buf_free(&attrib);
return ret;
}
/* Callback to write calendar-timezone property */
static int proppatch_timezone(xmlNodePtr prop, unsigned set,
struct proppatch_ctx *pctx,
struct propstat propstat[],
void *rock __attribute__((unused)))
{
int r;
if (pctx->req_tgt->collection && !pctx->req_tgt->resource) {
xmlChar *type, *ver = NULL, *freeme = NULL;
struct buf buf = BUF_INITIALIZER;
struct mime_type_t *mime;
icalcomponent *ical = NULL;
unsigned valid = 1;
type = xmlGetProp(prop, BAD_CAST "content-type");
if (type) ver = xmlGetProp(prop, BAD_CAST "version");
/* Check/find requested MIME type */
for (mime = caldav_mime_types; type && mime->content_type; mime++) {
if (is_mediatype(mime->content_type, (const char *) type)) {
if (ver &&
(!mime->version || xmlStrcmp(ver, BAD_CAST mime->version))) {
continue;
}
break;
}
}
if (!mime) {
xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV],
&propstat[PROPSTAT_FORBID],
prop->name, prop->ns, NULL,
CALDAV_SUPP_DATA);
*pctx->ret = HTTP_FORBIDDEN;
valid = 0;
}
else if (!mime->content_type) {
xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV],
&propstat[PROPSTAT_FORBID],
prop->name, prop->ns, NULL,
CALDAV_SUPP_DATA);
*pctx->ret = HTTP_FORBIDDEN;
valid = 0;
}
else if (set) {
freeme = xmlNodeGetContent(prop);
/* Parse and validate the iCal data */
ical = mime->from_string((const char *)freeme);
if (!ical || (icalcomponent_isa(ical) != ICAL_VCALENDAR_COMPONENT)) {
xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV],
&propstat[PROPSTAT_FORBID],
prop->name, prop->ns, NULL,
CALDAV_VALID_DATA);
*pctx->ret = HTTP_FORBIDDEN;
valid = 0;
}
else if (!icalcomponent_get_first_component(ical,
ICAL_VTIMEZONE_COMPONENT)
|| icalcomponent_get_first_real_component(ical)) {
xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV],
&propstat[PROPSTAT_FORBID],
prop->name, prop->ns, NULL,
CALDAV_VALID_OBJECT);
*pctx->ret = HTTP_FORBIDDEN;
valid = 0;
}
else if (mime != caldav_mime_types) {
buf_setcstr(&buf, icalcomponent_as_ical_string(ical));
}
else {
buf_setcstr(&buf, (const char *)freeme);
}
}
if (valid) {
annotate_state_t *astate = NULL;
buf_reset(&pctx->buf);
buf_printf(&pctx->buf, ANNOT_NS "<%s>%s",
(const char *) prop->ns->href, prop->name);
r = mailbox_get_annotate_state(pctx->mailbox, 0, &astate);
if (!r) r = annotate_state_write(astate, buf_cstring(&pctx->buf), /*userid*/NULL, &buf);
if (!r) {
xml_add_prop(HTTP_OK, pctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
prop->name, prop->ns, NULL, 0);
}
else {
xml_add_prop(HTTP_SERVER_ERROR, pctx->ns[NS_DAV],
&propstat[PROPSTAT_ERROR],
prop->name, prop->ns, NULL, 0);
}
}
if (ical) icalcomponent_free(ical);
if (freeme) xmlFree(freeme);
if (type) xmlFree(type);
if (ver) xmlFree(ver);
buf_free(&buf);
}
else {
xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV],
&propstat[PROPSTAT_FORBID], prop->name, prop->ns, NULL, 0);
*pctx->ret = HTTP_FORBIDDEN;
}
return 0;
}
/* Callback to write calendar-availability property */
static int proppatch_availability(xmlNodePtr prop, unsigned set,
struct proppatch_ctx *pctx,
struct propstat propstat[],
void *rock __attribute__((unused)))
{
/* XXX - fixme */
if (pctx->req_tgt->collection && !pctx->req_tgt->resource) {
struct buf buf = BUF_INITIALIZER;
xmlChar *type, *ver = NULL, *freeme = NULL;
struct mime_type_t *mime;
icalcomponent *ical = NULL;
unsigned valid = 1;
type = xmlGetProp(prop, BAD_CAST "content-type");
if (type) ver = xmlGetProp(prop, BAD_CAST "version");
/* Check/find requested MIME type */
for (mime = caldav_mime_types; type && mime->content_type; mime++) {
if (is_mediatype(mime->content_type, (const char *) type)) {
if (ver &&
(!mime->version || xmlStrcmp(ver, BAD_CAST mime->version))) {
continue;
}
break;
}
}
if (!mime->content_type) {
xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV],
&propstat[PROPSTAT_FORBID],
prop->name, prop->ns, NULL,
CALDAV_SUPP_DATA);
*pctx->ret = HTTP_FORBIDDEN;
valid = 0;
}
else if (set) {
freeme = xmlNodeGetContent(prop);
/* Parse and validate the iCal data */
ical = mime->from_string((const char *)freeme);
if (!ical || (icalcomponent_isa(ical) != ICAL_VCALENDAR_COMPONENT)) {
xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV],
&propstat[PROPSTAT_FORBID],
prop->name, prop->ns, NULL,
CALDAV_VALID_DATA);
*pctx->ret = HTTP_FORBIDDEN;
valid = 0;
}
else if (!icalcomponent_get_first_component(ical,
ICAL_VAVAILABILITY_COMPONENT)) {
xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV],
&propstat[PROPSTAT_FORBID],
prop->name, prop->ns, NULL,
CALDAV_VALID_OBJECT);
*pctx->ret = HTTP_FORBIDDEN;
valid = 0;
}
else if (mime != caldav_mime_types) {
buf_setcstr(&buf, icalcomponent_as_ical_string(ical));
}
else {
buf_setcstr(&buf, (const char *)freeme);
}
}
if (valid) {
struct annotate_state *astate = NULL;
int r;
buf_reset(&pctx->buf);
buf_printf(&pctx->buf, ANNOT_NS "<%s>%s",
(const char *) prop->ns->href, prop->name);
r = mailbox_get_annotate_state(pctx->mailbox, 0, &astate);
r = annotate_state_write(astate, buf_cstring(&pctx->buf), /*userid*/NULL, &buf);
if (!r) {
xml_add_prop(HTTP_OK, pctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
prop->name, prop->ns, NULL, 0);
}
else {
xml_add_prop(HTTP_SERVER_ERROR, pctx->ns[NS_DAV],
&propstat[PROPSTAT_ERROR],
prop->name, prop->ns, NULL, 0);
}
}
if (ical) icalcomponent_free(ical);
if (freeme) xmlFree(freeme);
if (type) xmlFree(type);
if (ver) xmlFree(ver);
buf_free(&buf);
}
else {
xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV],
&propstat[PROPSTAT_FORBID], prop->name, prop->ns, NULL, 0);
*pctx->ret = HTTP_FORBIDDEN;
}
return 0;
}
/* Callback to fetch CALDAV:supported-rscale-set */
static int propfind_rscaleset(const xmlChar *name, xmlNsPtr ns,
struct propfind_ctx *fctx,
xmlNodePtr resp __attribute__((unused)),
struct propstat propstat[],
void *rock __attribute__((unused)))
{
assert(name && ns && fctx && propstat);
if (fctx->req_tgt->resource) return HTTP_NOT_FOUND;
#ifdef HAVE_RSCALE
if (icalrecur_rscale_token_handling_is_supported()) {
xmlNodePtr top;
UEnumeration *en;
UErrorCode status = U_ZERO_ERROR;
const char *rscale;
top = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
name, ns, NULL, 0);
en = ucal_getKeywordValuesForLocale("calendar", NULL, FALSE, &status);
while ((rscale = uenum_next(en, NULL, &status))) {
xmlNewChild(top, fctx->ns[NS_CALDAV],
BAD_CAST "supported-rscale", BAD_CAST rscale);
}
uenum_close(en);
return 0;
}
#endif
return HTTP_NOT_FOUND;
}
static int report_cal_query(struct transaction_t *txn,
xmlNodePtr inroot, struct propfind_ctx *fctx)
{
int ret = 0;
xmlNodePtr node;
struct calquery_filter calfilter;
memset(&calfilter, 0, sizeof(struct calquery_filter));
calfilter.save_busytime = 0;
fctx->filter_crit = &calfilter;
fctx->open_db = (db_open_proc_t) &my_caldav_open;
fctx->close_db = (db_close_proc_t) &my_caldav_close;
fctx->lookup_resource = (db_lookup_proc_t) &caldav_lookup_resource;
fctx->foreach_resource = (db_foreach_proc_t) &caldav_foreach;
fctx->proc_by_resource = &propfind_by_resource;
/* Parse children element of report */
for (node = inroot->children; node; node = node->next) {
if (node->type == XML_ELEMENT_NODE) {
if (!xmlStrcmp(node->name, BAD_CAST "filter")) {
ret = parse_comp_filter(node->children, &calfilter, &txn->error);
if (ret) return ret;
else fctx->filter = apply_calfilter;
}
else if (!xmlStrcmp(node->name, BAD_CAST "timezone")) {
xmlChar *tz = NULL;
icalcomponent *ical = NULL;
syslog(LOG_WARNING, "REPORT calendar-query w/timezone");
tz = xmlNodeGetContent(node);
ical = icalparser_parse_string((const char *) tz);
if (!ical ||
(icalcomponent_isa(ical) != ICAL_VCALENDAR_COMPONENT) ||
!icalcomponent_get_first_component(ical,
ICAL_VTIMEZONE_COMPONENT)
|| icalcomponent_get_first_real_component(ical)) {
txn->error.precond = CALDAV_VALID_DATA;
ret = HTTP_FORBIDDEN;
}
if (tz) xmlFree(tz);
if (ical) icalcomponent_free(ical);
if (ret) return ret;
}
}
}
if (fctx->depth++ > 0) {
/* Calendar collection(s) */
if (txn->req_tgt.collection) {
/* Add response for target calendar collection */
propfind_by_collection(txn->req_tgt.mboxname, 0, 0, fctx);
}
else {
/* Add responses for all contained calendar collections */
strlcat(txn->req_tgt.mboxname, ".%", sizeof(txn->req_tgt.mboxname));
mboxlist_findall(NULL, /* internal namespace */
txn->req_tgt.mboxname, 1, httpd_userid,
httpd_authstate, propfind_by_collection, fctx);
}
if (fctx->davdb) my_caldav_close(fctx->davdb);
ret = *fctx->ret;
}
/* RRULEs still populate busytime array */
if (calfilter.busytime.busy) free(calfilter.busytime.busy);
return (ret ? ret : HTTP_MULTI_STATUS);
}
static int report_cal_multiget(struct transaction_t *txn,
xmlNodePtr inroot, struct propfind_ctx *fctx)
{
int r, ret = 0;
struct request_target_t tgt;
struct mailbox *mailbox = NULL;
xmlNodePtr node;
struct buf uri = BUF_INITIALIZER;
memset(&tgt, 0, sizeof(struct request_target_t));
tgt.namespace = URL_NS_CALENDAR;
/* Get props for each href */
for (node = inroot->children; node; node = node->next) {
if ((node->type == XML_ELEMENT_NODE) &&
!xmlStrcmp(node->name, BAD_CAST "href")) {
xmlChar *href = xmlNodeListGetString(inroot->doc, node->children, 1);
int len = xmlStrlen(href);
struct caldav_data *cdata;
buf_ensure(&uri, len);
xmlURIUnescapeString((const char *) href, len, uri.s);
xmlFree(href);
/* Parse the path */
if ((r = caldav_parse_path(uri.s, &tgt, &fctx->err->desc))) {
ret = r;
goto done;
}
fctx->req_tgt = &tgt;
/* Check if we already have this mailbox open */
if (!mailbox || strcmp(mailbox->name, tgt.mboxname)) {
if (mailbox) mailbox_close(&mailbox);
/* Open mailbox for reading */
r = mailbox_open_irl(tgt.mboxname, &mailbox);
if (r) {
syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s",
tgt.mboxname, error_message(r));
txn->error.desc = error_message(r);
ret = HTTP_SERVER_ERROR;
goto done;
}
fctx->mailbox = mailbox;
}
/* Open the DAV DB corresponding to the mailbox */
fctx->davdb = my_caldav_open(fctx->mailbox);
/* Find message UID for the resource */
r = caldav_lookup_resource(fctx->davdb,
tgt.mboxname, tgt.resource, 0, &cdata);
if (r) {
ret = HTTP_NOT_FOUND;
goto done;
}
cdata->dav.resource = tgt.resource;
/* XXX Check errors */
propfind_by_resource(fctx, cdata);
my_caldav_close(fctx->davdb);
}
}
done:
mailbox_close(&mailbox);
buf_free(&uri);
return (ret ? ret : HTTP_MULTI_STATUS);
}
/* caldav_foreach() callback to find busytime of a resource */
static int busytime_by_resource(void *rock, void *data)
{
struct propfind_ctx *fctx = (struct propfind_ctx *) rock;
struct dav_data *ddata = (struct dav_data *) data;
struct index_record record;
int r;
if (!ddata->imap_uid) return 0;
/* Fetch index record for the resource */
r = mailbox_find_index_record(fctx->mailbox, ddata->imap_uid, &record);
if (r) return 0;
fctx->record = &record;
(void) apply_calfilter(fctx, data);
buf_free(&fctx->msg_buf);
fctx->record = NULL;
return 0;
}
/* mboxlist_findall() callback to find busytime of a collection */
static int busytime_by_collection(char *mboxname, int matchlen,
int maycreate, void *rock)
{
struct propfind_ctx *fctx = (struct propfind_ctx *) rock;
struct calquery_filter *calfilter =
(struct calquery_filter *) fctx->filter_crit;
if (calfilter && calfilter->check_transp) {
/* Check if the collection is marked as transparent */
struct buf attrib = BUF_INITIALIZER;
const char *prop_annot =
ANNOT_NS "CALDAV:schedule-calendar-transp";
if (!annotatemore_lookup(mboxname, prop_annot, /* shared */ NULL, &attrib)) {
if (!strcmp(buf_cstring(&attrib), "transparent")) {
buf_free(&attrib);
return 0;
}
buf_free(&attrib);
}
}
return propfind_by_collection(mboxname, matchlen, maycreate, rock);
}
/* Compare start times of busytime period -- used for sorting */
static int compare_busytime(const void *b1, const void *b2)
{
struct icalperiodtype *a = (struct icalperiodtype *) b1;
struct icalperiodtype *b = (struct icalperiodtype *) b2;
return icaltime_compare(a->start, b->start);
}
/* Create an iCalendar object containing busytime of all specified resources */
static icalcomponent *busytime_query_local(struct transaction_t *txn,
struct propfind_ctx *fctx,
char mailboxname[],
icalproperty_method method,
const char *uid,
const char *organizer,
const char *attendee)
{
struct calquery_filter *calfilter =
(struct calquery_filter *) fctx->filter_crit;
struct busytime *busytime = &calfilter->busytime;
icalcomponent *cal = NULL;
fctx->open_db = (db_open_proc_t) &my_caldav_open;
fctx->close_db = (db_close_proc_t) &my_caldav_close;
fctx->lookup_resource = (db_lookup_proc_t) &caldav_lookup_resource;
fctx->foreach_resource = (db_foreach_proc_t) &caldav_foreach;
fctx->proc_by_resource = &busytime_by_resource;
/* Gather up all of the busytime */
if (fctx->depth > 0) {
/* Calendar collection(s) */
/* XXX Check DACL_READFB on all calendars */
if (txn->req_tgt.collection) {
/* Get busytime for target calendar collection */
busytime_by_collection(mailboxname, 0, 0, fctx);
}
else {
/* Get busytime for all contained calendar collections */
strlcat(mailboxname, ".%", MAX_MAILBOX_PATH);
mboxlist_findall(NULL, /* internal namespace */
mailboxname, 1, httpd_userid,
httpd_authstate, busytime_by_collection, fctx);
}
if (fctx->davdb) my_caldav_close(fctx->davdb);
}
if (!*fctx->ret) {
icalcomponent *fb;
icalproperty *prop;
time_t now = time(0);
unsigned n;
/* Construct iCalendar object with VFREEBUSY component */
cal = icalcomponent_vanew(ICAL_VCALENDAR_COMPONENT,
icalproperty_new_version("2.0"),
icalproperty_new_prodid(ical_prodid),
0);
if (method) icalcomponent_set_method(cal, method);
fb = icalcomponent_vanew(ICAL_VFREEBUSY_COMPONENT,
icalproperty_new_dtstamp(
icaltime_from_timet_with_zone(
now,
0,
icaltimezone_get_utc_timezone())),
icalproperty_new_dtstart(calfilter->start),
icalproperty_new_dtend(calfilter->end),
0);
if (uid) icalcomponent_set_uid(fb, uid);
if (organizer) {
prop = icalproperty_new_organizer(organizer);
icalcomponent_add_property(fb, prop);
}
if (attendee) {
prop = icalproperty_new_attendee(attendee);
icalcomponent_add_property(fb, prop);
}
icalcomponent_add_component(cal, fb);
/* Sort busytime periods by start time */
qsort(busytime->busy, busytime->len, sizeof(struct icalperiodtype),
compare_busytime);
/* Add busytime periods to VFREEBUSY component, coalescing as needed */
for (n = 0; n < busytime->len; n++) {
if ((n+1 < busytime->len) &&
icaltime_compare(busytime->busy[n].end,
busytime->busy[n+1].start) >= 0) {
/* Periods overlap -- coalesce into next busytime */
memcpy(&busytime->busy[n+1].start, &busytime->busy[n].start,
sizeof(struct icaltimetype));
if (icaltime_compare(busytime->busy[n].end,
busytime->busy[n+1].end) > 0) {
memcpy(&busytime->busy[n+1].end, &busytime->busy[n].end,
sizeof(struct icaltimetype));
}
}
else {
icalproperty *busy =
icalproperty_new_freebusy(busytime->busy[n]);
icalcomponent_add_property(fb, busy);
}
}
}
return cal;
}
static int report_fb_query(struct transaction_t *txn,
xmlNodePtr inroot, struct propfind_ctx *fctx)
{
int ret = 0;
const char **hdr;
struct mime_type_t *mime;
struct calquery_filter calfilter;
xmlNodePtr node;
icalcomponent *cal;
/* Can not be run against a resource */
if (txn->req_tgt.resource) return HTTP_FORBIDDEN;
/* Check requested MIME type:
1st entry in caldav_mime_types array MUST be default MIME type */
if ((hdr = spool_getheader(txn->req_hdrs, "Accept")))
mime = get_accept_type(hdr, caldav_mime_types);
else mime = caldav_mime_types;
if (!mime) return HTTP_NOT_ACCEPTABLE;
memset(&calfilter, 0, sizeof(struct calquery_filter));
calfilter.comp = CAL_COMP_VEVENT | CAL_COMP_VFREEBUSY;
calfilter.start = icaltime_from_timet_with_zone(INT_MIN, 0, NULL);
calfilter.end = icaltime_from_timet_with_zone(INT_MAX, 0, NULL);
calfilter.save_busytime = 1;
fctx->filter = apply_calfilter;
fctx->filter_crit = &calfilter;
/* Parse children element of report */
for (node = inroot->children; node; node = node->next) {
if (node->type == XML_ELEMENT_NODE) {
if (!xmlStrcmp(node->name, BAD_CAST "time-range")) {
xmlChar *start, *end;
start = xmlGetProp(node, BAD_CAST "start");
if (start) {
calfilter.start = icaltime_from_string((char *) start);
xmlFree(start);
}
end = xmlGetProp(node, BAD_CAST "end");
if (end) {
calfilter.end = icaltime_from_string((char *) end);
xmlFree(end);
}
if (!is_valid_timerange(calfilter.start, calfilter.end)) {
return HTTP_BAD_REQUEST;
}
}
}
}
cal = busytime_query_local(txn, fctx, txn->req_tgt.mboxname,
0, NULL, NULL, NULL);
if (calfilter.busytime.busy) free(calfilter.busytime.busy);
if (cal) {
/* Output the iCalendar object as text/calendar */
char *cal_str = mime->to_string(cal);
icalcomponent_free(cal);
txn->resp_body.type = mime->content_type;
/* iCalendar data in response should not be transformed */
txn->flags.cc |= CC_NOTRANSFORM;
write_body(HTTP_OK, txn, cal_str, strlen(cal_str));
free(cal_str);
}
else ret = HTTP_NOT_FOUND;
return ret;
}
/* Store the iCal data in the specified calendar/resource */
static int store_resource(struct transaction_t *txn, icalcomponent *ical,
struct mailbox *mailbox, const char *resource,
struct caldav_db *caldavdb, int overwrite,
unsigned flags)
{
int ret = HTTP_CREATED, r;
icalcomponent *comp;
icalcomponent_kind kind;
icalproperty_method meth;
icalproperty *prop;
unsigned mykind = 0;
char *header;
const char *organizer = NULL;
const char *prop_annot = ANNOT_NS "CALDAV:supported-calendar-component-set";
struct buf attrib = BUF_INITIALIZER;
struct buf headbuf = BUF_INITIALIZER;
struct index_record oldrecord;
struct caldav_data *cdata;
quota_t qdiffs[QUOTA_NUMRESOURCES] = QUOTA_DIFFS_DONTCARE_INITIALIZER;
FILE *f = NULL;
struct stagemsg *stage;
const char *uid, *ics;
uint32_t expunge_uid = 0;
time_t now = time(NULL);
char datestr[80];
struct appendstate as;
static char sched_tag[64];
static unsigned store_count = 0;
/* Check for supported component type */
comp = icalcomponent_get_first_real_component(ical);
kind = icalcomponent_isa(comp);
switch (kind) {
case ICAL_VEVENT_COMPONENT: mykind = CAL_COMP_VEVENT; break;
case ICAL_VTODO_COMPONENT: mykind = CAL_COMP_VTODO; break;
case ICAL_VJOURNAL_COMPONENT: mykind = CAL_COMP_VJOURNAL; break;
case ICAL_VFREEBUSY_COMPONENT: mykind = CAL_COMP_VFREEBUSY; break;
case ICAL_VAVAILABILITY_COMPONENT: mykind = CAL_COMP_VAVAILABILITY; break;
#ifdef HAVE_VPOLL
case ICAL_VPOLL_COMPONENT: mykind = CAL_COMP_VPOLL; break;
#endif
default:
txn->error.precond = CALDAV_SUPP_COMP;
return HTTP_FORBIDDEN;
}
if (!annotatemore_lookup(mailbox->name, prop_annot,
/* shared */ NULL, &attrib)
&& attrib.len) {
unsigned long supp_comp = strtoul(buf_cstring(&attrib), NULL, 10);
buf_free(&attrib);
if (!(mykind & supp_comp)) {
txn->error.precond = CALDAV_SUPP_COMP;
return HTTP_FORBIDDEN;
}
}
/* Find iCalendar UID for the current resource, if exists */
uid = icalcomponent_get_uid(comp);
caldav_lookup_resource(caldavdb,
mailbox->name, resource, 0, &cdata);
if (cdata->ical_uid && strcmp(cdata->ical_uid, uid)) {
/* CALDAV:no-uid-conflict */
txn->error.precond = CALDAV_UID_CONFLICT;
return HTTP_FORBIDDEN;
}
/* XXX - theoretical race, but the mailbox is locked, so nothing
* else can ACTUALLY change it */
if (cdata->dav.imap_uid) {
/* Fetch index record for the resource */
r = mailbox_find_index_record(mailbox, cdata->dav.imap_uid, &oldrecord);
if (r) return HTTP_SERVER_ERROR;
if (overwrite == OVERWRITE_CHECK) {
/* Check any preconditions */
const char *etag = message_guid_encode(&oldrecord.guid);
time_t lastmod = oldrecord.internaldate;
int precond = caldav_check_precond(txn, cdata,
etag, lastmod);
if (precond != HTTP_OK)
return HTTP_PRECOND_FAILED;
}
expunge_uid = cdata->dav.imap_uid;
}
/* Check for existing iCalendar UID */
caldav_lookup_uid(caldavdb, uid, 0, &cdata);
if (!(flags & NO_DUP_CHECK) &&
cdata->dav.mailbox && !strcmp(cdata->dav.mailbox, mailbox->name) &&
strcmp(cdata->dav.resource, resource)) {
/* CALDAV:no-uid-conflict */
const char *owner = mboxname_to_userid(cdata->dav.mailbox);
txn->error.precond = CALDAV_UID_CONFLICT;
assert(!buf_len(&txn->buf));
buf_printf(&txn->buf, "%s/user/%s/%s/%s",
namespace_calendar.prefix, owner,
strrchr(cdata->dav.mailbox, '.')+1, cdata->dav.resource);
txn->error.resource = buf_cstring(&txn->buf);
return HTTP_FORBIDDEN;
}
/* Prepare to stage the message */
if (!(f = append_newstage(mailbox->name, now, 0, &stage))) {
syslog(LOG_ERR, "append_newstage(%s) failed", mailbox->name);
txn->error.desc = "append_newstage() failed\r\n";
return HTTP_SERVER_ERROR;
}
/* Remove all X-LIC-ERROR properties*/
icalcomponent_strip_errors(ical);
ics = icalcomponent_as_ical_string(ical);
/* Create iMIP header for resource */
prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
if (prop) {
organizer = icalproperty_get_organizer(prop)+7;
header = charset_encode_mimeheader(organizer, 0);
fprintf(f, "From: %s\r\n", header);
free(header);
}
else if (strchr(proxy_userid, '@')) {
/* XXX This needs to be done via an LDAP/DB lookup */
header = charset_encode_mimeheader(proxy_userid, 0);
fprintf(f, "From: %s\r\n", header);
free(header);
}
else {
struct buf *headbuf = &txn->buf;
assert(!headbuf->len);
buf_printf(headbuf, "%s@%s", proxy_userid, config_servername);
header = charset_encode_mimeheader(headbuf->s, headbuf->len);
buf_reset(headbuf);
fprintf(f, "From: %s\r\n", header);
free(header);
}
header = charset_encode_mimeheader(icalcomponent_get_summary(comp), 0);
fprintf(f, "Subject: %s\r\n", header);
free(header);
time_to_rfc822(icaltime_as_timet_with_zone(icalcomponent_get_dtstamp(comp),
icaltimezone_get_utc_timezone()),
datestr, sizeof(datestr));
fprintf(f, "Date: %s\r\n", datestr);
/* XXX - validate uid for mime safety? */
if (strchr(uid, '@')) {
fprintf(f, "Message-ID: <%s>\r\n", uid);
}
else {
fprintf(f, "Message-ID: <%s@%s>\r\n", uid, config_servername);
}
fprintf(f, "Content-Type: text/calendar; charset=utf-8");
if ((meth = icalcomponent_get_method(ical)) != ICAL_METHOD_NONE) {
fprintf(f, "; method=%s", icalproperty_method_to_string(meth));
}
fprintf(f, "; component=%s\r\n", icalcomponent_kind_to_string(kind));
fprintf(f, "Content-Length: %u\r\n", (unsigned) strlen(ics));
fprintf(f, "Content-Disposition: inline; filename=\"%s\"", resource);
if (organizer) {
const char *stag;
if (flags & NEW_STAG) {
sprintf(sched_tag, "%d-%ld-%u", getpid(), now, store_count++);
stag = sched_tag;
}
else stag = cdata->sched_tag;
if (stag) fprintf(f, ";\r\n\tschedule-tag=%s", stag);
}
fprintf(f, "\r\n");
/* XXX Check domain of data and use appropriate CTE */
fprintf(f, "MIME-Version: 1.0\r\n");
fprintf(f, "\r\n");
/* Write the iCal data to the file */
fprintf(f, "%s", ics);
qdiffs[QUOTA_STORAGE] = ftell(f);
fclose(f);
qdiffs[QUOTA_MESSAGE] = 1;
/* Prepare to append the iMIP message to calendar mailbox */
if ((r = append_setup_mbox(&as, mailbox, NULL, NULL, 0, qdiffs, 0, 0, EVENT_MESSAGE_NEW|EVENT_CALENDAR))) {
syslog(LOG_ERR, "append_setup(%s) failed: %s",
mailbox->name, error_message(r));
ret = HTTP_SERVER_ERROR;
txn->error.desc = "append_setup() failed\r\n";
}
else {
struct body *body = NULL;
/* Append the iMIP file to the calendar mailbox */
if ((r = append_fromstage(&as, &body, stage, now, NULL, 0, 0))) {
syslog(LOG_ERR, "append_fromstage() failed");
ret = HTTP_SERVER_ERROR;
txn->error.desc = "append_fromstage() failed\r\n";
}
if (body) {
message_free_body(body);
free(body);
}
if (r) append_abort(&as);
else {
/* Commit the append to the calendar mailbox */
if ((r = append_commit(&as))) {
syslog(LOG_ERR, "append_commit() failed");
ret = HTTP_SERVER_ERROR;
txn->error.desc = "append_commit() failed\r\n";
}
else {
/* append_commit() returns a write-locked index */
struct index_record newrecord;
/* Read index record for new message (always the last one) */
mailbox_read_index_record(mailbox, mailbox->i.num_records,
&newrecord);
if (expunge_uid) {
/* Now that we have the replacement message in place
and the mailbox locked, re-read the old record
and see if we should overwrite it. Either way,
one of our records will have to be expunged.
*/
int userflag;
ret = HTTP_NO_CONTENT;
/* Perform the actual expunge */
r = mailbox_user_flag(mailbox, DFLAG_UNBIND, &userflag, 1);
if (!r) {
oldrecord.user_flags[userflag/32] |= 1<<(userflag&31);
oldrecord.system_flags |= FLAG_EXPUNGED;
r = mailbox_rewrite_index_record(mailbox, &oldrecord);
}
if (r) {
syslog(LOG_ERR, "expunging record (%s) failed: %s",
mailbox->name, error_message(r));
txn->error.desc = error_message(r);
ret = HTTP_SERVER_ERROR;
}
}
if (!r) {
struct resp_body_t *resp_body = &txn->resp_body;
if (cdata->organizer && (flags & NEW_STAG)) {
resp_body->stag = sched_tag;
}
if ((flags & PREFER_REP) || !(flags & NEW_STAG)) {
/* Tell client about the new resource */
resp_body->lastmod = newrecord.internaldate;
resp_body->etag = message_guid_encode(&newrecord.guid);
}
}
}
}
}
append_removestage(stage);
buf_free(&headbuf);
return ret;
}
int caladdress_lookup(const char *addr, struct sched_param *param)
{
char *p;
int islocal = 1, found = 1;
static char userid[MAX_MAILBOX_BUFFER];
memset(param, 0, sizeof(struct sched_param));
if (!addr) return HTTP_NOT_FOUND;
p = (char *) addr;
if (!strncmp(addr, "mailto:", 7)) p += 7;
/* XXX Do LDAP/DB/socket lookup to see if user is local */
/* XXX Hack until real lookup stuff is written */
strlcpy(userid, p, sizeof(userid));
if ((p = strchr(userid, '@'))) *p++ = '\0';
if (islocal) {
/* User is in a local domain */
int r;
char mailboxname[MAX_MAILBOX_NAME];
mbentry_t *mbentry = NULL;
struct mboxname_parts parts;
if (!found) return HTTP_NOT_FOUND;
else param->userid = userid;
/* Lookup user's cal-home-set to see if its on this server */
mboxname_userid_to_parts(userid, &parts);
parts.box = xstrdupnull(config_getstring(IMAPOPT_CALENDARPREFIX));
mboxname_parts_to_internal(&parts, mailboxname);
mboxname_free_parts(&parts);
r = http_mlookup(mailboxname, &mbentry, NULL);
if (!r) {
param->server = xstrdupnull(mbentry->server); /* XXX - memory leak */
mboxlist_entry_free(&mbentry);
if (param->server) param->flags |= SCHEDTYPE_ISCHEDULE;
}
/* Fall through and try remote */
}
/* User is outside of our domain(s) -
Do remote scheduling (default = iMIP) */
param->flags |= SCHEDTYPE_REMOTE;
#ifdef WITH_DKIM
/* Do iSchedule DNS SRV lookup */
/* XXX If success, set server, port,
and flags |= SCHEDTYPE_ISCHEDULE [ | SCHEDTYPE_SSL ] */
#endif
return 0;
}
/* Send an iMIP request for attendees in 'ical' */
static int imip_send(icalcomponent *ical)
{
icalcomponent *comp;
icalproperty *prop;
icalproperty_method meth;
icalcomponent_kind kind;
const char *argv[8], *organizer, *subject;
FILE *sm;
pid_t pid;
int r;
time_t t = time(NULL);
char datestr[80];
static unsigned send_count = 0;
icalproperty_kind recip_kind;
const char *(*get_recipient)(const icalproperty *);
meth = icalcomponent_get_method(ical);
comp = icalcomponent_get_first_real_component(ical);
kind = icalcomponent_isa(comp);
prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
organizer = icalproperty_get_organizer(prop) + 7;
if (kind == ICAL_VPOLL_COMPONENT) {
recip_kind = ICAL_VOTER_PROPERTY;
get_recipient = &icalproperty_get_voter;
}
else {
recip_kind = ICAL_ATTENDEE_PROPERTY;
get_recipient = &icalproperty_get_attendee;
}
argv[0] = "sendmail";
argv[1] = "-f";
argv[2] = organizer;
argv[3] = "-i";
argv[4] = "-N";
argv[5] = "failure,delay";
argv[6] = "-t";
argv[7] = NULL;
pid = open_sendmail(argv, &sm);
if (sm == NULL) return HTTP_UNAVAILABLE;
/* Create iMIP message */
fprintf(sm, "From: %s\r\n", organizer);
for (prop = icalcomponent_get_first_property(comp, recip_kind);
prop;
prop = icalcomponent_get_next_property(comp, recip_kind)) {
fprintf(sm, "To: %s\r\n", get_recipient(prop) + 7);
}
subject = icalcomponent_get_summary(comp);
if (!subject) {
fprintf(sm, "Subject: %s %s\r\n", icalcomponent_kind_to_string(kind),
icalproperty_method_to_string(meth));
}
else fprintf(sm, "Subject: %s\r\n", subject);
time_to_rfc822(t, datestr, sizeof(datestr));
fprintf(sm, "Date: %s\r\n", datestr);
fprintf(sm, "Message-ID: <cmu-httpd-%u-%ld-%u@%s>\r\n",
getpid(), t, send_count++, config_servername);
fprintf(sm, "Content-Type: text/calendar; charset=utf-8");
fprintf(sm, "; method=%s; component=%s \r\n",
icalproperty_method_to_string(meth),
icalcomponent_kind_to_string(kind));
fputs("Content-Disposition: inline\r\n", sm);
fputs("MIME-Version: 1.0\r\n", sm);
fputs("\r\n", sm);
fputs(icalcomponent_as_ical_string(ical), sm);
fclose(sm);
while (waitpid(pid, &r, 0) < 0);
return r;
}
/* Add a <response> XML element for 'recipient' to 'root' */
xmlNodePtr xml_add_schedresponse(xmlNodePtr root, xmlNsPtr dav_ns,
xmlChar *recipient, xmlChar *status)
{
xmlNodePtr resp, recip;
resp = xmlNewChild(root, NULL, BAD_CAST "response", NULL);
recip = xmlNewChild(resp, NULL, BAD_CAST "recipient", NULL);
if (dav_ns) xmlNewChild(recip, dav_ns, BAD_CAST "href", recipient);
else xmlNodeAddContent(recip, recipient);
if (status)
xmlNewChild(resp, NULL, BAD_CAST "request-status", status);
return resp;
}
struct remote_rock {
struct transaction_t *txn;
icalcomponent *ical;
xmlNodePtr root;
xmlNsPtr *ns;
};
/* Send an iTIP busytime request to remote attendees via iMIP or iSchedule */
static void busytime_query_remote(const char *server __attribute__((unused)),
void *data, void *rock)
{
struct sched_param *remote = (struct sched_param *) data;
struct remote_rock *rrock = (struct remote_rock *) rock;
icalcomponent *comp;
struct proplist *list;
xmlNodePtr resp;
const char *status = NULL;
int r;
comp = icalcomponent_get_first_real_component(rrock->ical);
/* Add the attendees to the iTIP request */
for (list = remote->props; list; list = list->next) {
icalcomponent_add_property(comp, list->prop);
}
if (remote->flags == SCHEDTYPE_REMOTE) {
/* Use iMIP */
r = imip_send(rrock->ical);
if (!r) status = REQSTAT_SENT;
else status = REQSTAT_TEMPFAIL;
}
else {
/* Use iSchedule */
xmlNodePtr xml;
r = isched_send(remote, NULL, rrock->ical, &xml);
if (r) status = REQSTAT_TEMPFAIL;
else if (xmlStrcmp(xml->name, BAD_CAST "schedule-response")) {
if (r) status = REQSTAT_TEMPFAIL;
}
else {
xmlNodePtr cur;
/* Process each response element */
for (cur = xml->children; cur; cur = cur->next) {
xmlNodePtr node;
xmlChar *recip = NULL, *status = NULL, *content = NULL;
if (cur->type != XML_ELEMENT_NODE) continue;
for (node = cur->children; node; node = node->next) {
if (node->type != XML_ELEMENT_NODE) continue;
if (!xmlStrcmp(node->name, BAD_CAST "recipient"))
recip = xmlNodeGetContent(node);
else if (!xmlStrcmp(node->name, BAD_CAST "request-status"))
status = xmlNodeGetContent(node);
else if (!xmlStrcmp(node->name, BAD_CAST "calendar-data"))
content = xmlNodeGetContent(node);
}
resp =
xml_add_schedresponse(rrock->root,
!(rrock->txn->req_tgt.allow & ALLOW_ISCHEDULE) ?
rrock->ns[NS_DAV] : NULL,
recip, status);
xmlFree(status);
xmlFree(recip);
if (content) {
xmlNodePtr cdata =
xmlNewTextChild(resp, NULL,
BAD_CAST "calendar-data", NULL);
xmlAddChild(cdata,
xmlNewCDataBlock(rrock->root->doc,
content,
xmlStrlen(content)));
xmlFree(content);
/* iCal data in resp SHOULD NOT be transformed */
rrock->txn->flags.cc |= CC_NOTRANSFORM;
}
}
xmlFreeDoc(xml->doc);
}
}
/* Report request-status (if necesary)
* Remove the attendees from the iTIP request and hash bucket
*/
for (list = remote->props; list; list = list->next) {
if (status) {
const char *attendee = icalproperty_get_attendee(list->prop);
xml_add_schedresponse(rrock->root,
!(rrock->txn->req_tgt.allow & ALLOW_ISCHEDULE) ?
rrock->ns[NS_DAV] : NULL,
BAD_CAST attendee,
BAD_CAST status);
}
icalcomponent_remove_property(comp, list->prop);
icalproperty_free(list->prop);
}
if (remote->server) free(remote->server);
}
static void free_sched_param(void *data)
{
struct sched_param *sched_param = (struct sched_param *) data;
if (sched_param) {
struct proplist *prop, *next;
for (prop = sched_param->props; prop; prop = next) {
next = prop->next;
free(prop);
}
free(sched_param);
}
}
/* Perform a Busy Time query based on given VFREEBUSY component */
/* NOTE: This function is destructive of 'ical' */
int sched_busytime_query(struct transaction_t *txn,
struct mime_type_t *mime, icalcomponent *ical)
{
int ret = 0;
static const char *calendarprefix = NULL;
icalcomponent *comp;
char mailboxname[MAX_MAILBOX_BUFFER];
icalproperty *prop = NULL, *next;
const char *uid = NULL, *organizer = NULL;
struct sched_param sparam;
struct auth_state *org_authstate = NULL;
xmlNodePtr root = NULL;
xmlNsPtr ns[NUM_NAMESPACE];
struct propfind_ctx fctx;
struct calquery_filter calfilter;
struct hash_table remote_table;
struct sched_param *remote = NULL;
if (!calendarprefix) {
calendarprefix = config_getstring(IMAPOPT_CALENDARPREFIX);
}
comp = icalcomponent_get_first_real_component(ical);
uid = icalcomponent_get_uid(comp);
prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
organizer = icalproperty_get_organizer(prop);
/* XXX Do we need to do more checks here? */
if (caladdress_lookup(organizer, &sparam) ||
(sparam.flags & SCHEDTYPE_REMOTE))
org_authstate = auth_newstate("anonymous");
else
org_authstate = auth_newstate(sparam.userid);
/* Start construction of our schedule-response */
if (!(root =
init_xml_response("schedule-response",
(txn->req_tgt.allow & ALLOW_ISCHEDULE) ? NS_ISCHED :
NS_CALDAV, NULL, ns))) {
ret = HTTP_SERVER_ERROR;
txn->error.desc = "Unable to create XML response\r\n";
goto done;
}
/* Populate our filter and propfind context for local attendees */
memset(&calfilter, 0, sizeof(struct calquery_filter));
calfilter.comp = CAL_COMP_VEVENT | CAL_COMP_VFREEBUSY;
calfilter.start = icalcomponent_get_dtstart(comp);
calfilter.end = icalcomponent_get_dtend(comp);
calfilter.check_transp = 1;
calfilter.save_busytime = 1;
memset(&fctx, 0, sizeof(struct propfind_ctx));
fctx.req_tgt = &txn->req_tgt;
fctx.depth = 2;
fctx.userid = proxy_userid;
fctx.int_userid = httpd_userid;
fctx.userisadmin = httpd_userisadmin;
fctx.authstate = org_authstate;
fctx.reqd_privs = 0; /* handled by CALDAV:schedule-deliver on Inbox */
fctx.filter = apply_calfilter;
fctx.filter_crit = &calfilter;
fctx.err = &txn->error;
fctx.ret = &ret;
fctx.fetcheddata = 0;
/* Create hash table for any remote attendee servers */
construct_hash_table(&remote_table, 10, 1);
assert(!buf_len(&txn->buf));
/* Process each attendee */
for (prop = icalcomponent_get_first_property(comp, ICAL_ATTENDEE_PROPERTY);
prop;
prop = next) {
const char *attendee;
int r;
next = icalcomponent_get_next_property(comp, ICAL_ATTENDEE_PROPERTY);
/* Remove each attendee so we can add in only those
that reside on a given remote server later */
icalcomponent_remove_property(comp, prop);
/* Is attendee remote or local? */
attendee = icalproperty_get_attendee(prop);
r = caladdress_lookup(attendee, &sparam);
/* Don't allow scheduling of remote users via an iSchedule request */
if ((sparam.flags & SCHEDTYPE_REMOTE) &&
(txn->req_tgt.allow & ALLOW_ISCHEDULE)) {
r = HTTP_FORBIDDEN;
}
if (r) {
xml_add_schedresponse(root,
!(txn->req_tgt.allow & ALLOW_ISCHEDULE) ?
ns[NS_DAV] : NULL,
BAD_CAST attendee, BAD_CAST REQSTAT_NOUSER);
icalproperty_free(prop);
}
else if (sparam.flags) {
/* Remote attendee */
struct proplist *newprop;
const char *key;
if (sparam.flags == SCHEDTYPE_REMOTE) {
/* iMIP - collect attendees under empty key (no server) */
key = "";
}
else {
/* iSchedule - collect attendees by server */
key = sparam.server;
}
remote = hash_lookup(key, &remote_table);
if (!remote) {
/* New remote - add it to the hash table */
remote = xzmalloc(sizeof(struct sched_param));
if (sparam.server) remote->server = xstrdup(sparam.server);
remote->port = sparam.port;
remote->flags = sparam.flags;
hash_insert(key, remote, &remote_table);
}
newprop = xmalloc(sizeof(struct proplist));
newprop->prop = prop;
newprop->next = remote->props;
remote->props = newprop;
}
else {
/* Local attendee on this server */
xmlNodePtr resp;
const char *userid = sparam.userid;
mbentry_t *mbentry = NULL;
icalcomponent *busy = NULL;
resp =
xml_add_schedresponse(root,
!(txn->req_tgt.allow & ALLOW_ISCHEDULE) ?
ns[NS_DAV] : NULL,
BAD_CAST attendee, NULL);
/* XXX - BROKEN WITH DOMAIN SPLIT, POS */
/* Check ACL of ORGANIZER on attendee's Scheduling Inbox */
snprintf(mailboxname, sizeof(mailboxname),
"user.%s.%s.Inbox", userid, calendarprefix);
r = mboxlist_lookup(mailboxname, &mbentry, NULL);
if (r) {
syslog(LOG_INFO, "mboxlist_lookup(%s) failed: %s",
mailboxname, error_message(r));
xmlNewChild(resp, NULL, BAD_CAST "request-status",
BAD_CAST REQSTAT_REJECTED);
}
else {
/* Start query at attendee's calendar-home-set */
snprintf(mailboxname, sizeof(mailboxname),
"user.%s.%s", userid, calendarprefix);
fctx.davdb = NULL;
fctx.req_tgt->collection = NULL;
calfilter.busytime.len = 0;
busy = busytime_query_local(txn, &fctx, mailboxname,
ICAL_METHOD_REPLY, uid,
organizer, attendee);
}
if (busy) {
xmlNodePtr cdata;
char *fb_str = mime->to_string(busy);
icalcomponent_free(busy);
xmlNewChild(resp, NULL, BAD_CAST "request-status",
BAD_CAST REQSTAT_SUCCESS);
cdata = xmlNewTextChild(resp, NULL,
BAD_CAST "calendar-data", NULL);
/* Trim any charset from content-type */
buf_reset(&txn->buf);
buf_printf(&txn->buf, "%.*s",
(int) strcspn(mime->content_type, ";"),
mime->content_type);
xmlNewProp(cdata, BAD_CAST "content-type",
BAD_CAST buf_cstring(&txn->buf));
if (mime->version)
xmlNewProp(cdata, BAD_CAST "version",
BAD_CAST mime->version);
xmlAddChild(cdata,
xmlNewCDataBlock(root->doc, BAD_CAST fb_str,
strlen(fb_str)));
free(fb_str);
/* iCalendar data in response should not be transformed */
txn->flags.cc |= CC_NOTRANSFORM;
}
else {
xmlNewChild(resp, NULL, BAD_CAST "request-status",
BAD_CAST REQSTAT_NOUSER);
}
icalproperty_free(prop);
}
}
buf_reset(&txn->buf);
if (remote) {
struct remote_rock rrock = { txn, ical, root, ns };
hash_enumerate(&remote_table, busytime_query_remote, &rrock);
}
free_hash_table(&remote_table, free_sched_param);
/* Output the XML response */
if (!ret) xml_response(HTTP_OK, txn, root->doc);
done:
if (org_authstate) auth_freestate(org_authstate);
if (calfilter.busytime.busy) free(calfilter.busytime.busy);
if (root) xmlFreeDoc(root->doc);
return ret;
}
static void free_sched_data(void *data)
{
struct sched_data *sched_data = (struct sched_data *) data;
if (sched_data) {
if (sched_data->itip) icalcomponent_free(sched_data->itip);
free(sched_data);
}
}
#define SCHEDSTAT_PENDING "1.0"
#define SCHEDSTAT_SENT "1.1"
#define SCHEDSTAT_DELIVERED "1.2"
#define SCHEDSTAT_SUCCESS "2.0"
#define SCHEDSTAT_PARAM "2.3"
#define SCHEDSTAT_NOUSER "3.7"
#define SCHEDSTAT_NOPRIVS "3.8"
#define SCHEDSTAT_TEMPFAIL "5.1"
#define SCHEDSTAT_PERMFAIL "5.2"
#define SCHEDSTAT_REJECTED "5.3"
/* Deliver scheduling object to a remote recipient */
static void sched_deliver_remote(const char *recipient,
struct sched_param *sparam,
struct sched_data *sched_data)
{
int r;
if (sparam->flags == SCHEDTYPE_REMOTE) {
/* Use iMIP */
r = imip_send(sched_data->itip);
if (!r) {
sched_data->status =
sched_data->ischedule ? REQSTAT_SENT : SCHEDSTAT_SENT;
}
else {
sched_data->status = sched_data->ischedule ?
REQSTAT_TEMPFAIL : SCHEDSTAT_TEMPFAIL;
}
}
else {
/* Use iSchedule */
xmlNodePtr xml;
r = isched_send(sparam, recipient, sched_data->itip, &xml);
if (r) {
sched_data->status = sched_data->ischedule ?
REQSTAT_TEMPFAIL : SCHEDSTAT_TEMPFAIL;
}
else if (xmlStrcmp(xml->name, BAD_CAST "schedule-response")) {
sched_data->status = sched_data->ischedule ?
REQSTAT_TEMPFAIL : SCHEDSTAT_TEMPFAIL;
}
else {
xmlNodePtr cur;
/* Process each response element */
for (cur = xml->children; cur; cur = cur->next) {
xmlNodePtr node;
xmlChar *recip = NULL, *status = NULL;
static char statbuf[1024];
if (cur->type != XML_ELEMENT_NODE) continue;
for (node = cur->children; node; node = node->next) {
if (node->type != XML_ELEMENT_NODE) continue;
if (!xmlStrcmp(node->name, BAD_CAST "recipient"))
recip = xmlNodeGetContent(node);
else if (!xmlStrcmp(node->name,
BAD_CAST "request-status"))
status = xmlNodeGetContent(node);
}
if (!strncmp((const char *) status, "2.0", 3)) {
sched_data->status = sched_data->ischedule ?
REQSTAT_DELIVERED : SCHEDSTAT_DELIVERED;
}
else {
if (sched_data->ischedule)
strlcpy(statbuf, (const char *) status, sizeof(statbuf));
else
strlcpy(statbuf, (const char *) status, 4);
sched_data->status = statbuf;
}
xmlFree(status);
xmlFree(recip);
}
}
}
}
#ifdef HAVE_VPOLL
/*
* deliver_merge_reply() helper function
*
* Merge VOTER responses into VPOLL subcomponents
*/
static void deliver_merge_vpoll_reply(icalcomponent *ical, icalcomponent *reply)
{
icalproperty *mastervoterp;
const char *voter;
icalcomponent *comp;
mastervoterp = icalcomponent_get_first_property(reply, ICAL_VOTER_PROPERTY);
voter = icalproperty_get_voter(mastervoterp);
/* Process each existing VPOLL subcomponent */
for (comp = icalcomponent_get_first_component(ical, ICAL_ANY_COMPONENT);
comp;
comp = icalcomponent_get_next_component(ical, ICAL_ANY_COMPONENT)) {
icalproperty *itemid, *voterp;
int id;
itemid =
icalcomponent_get_first_property(comp, ICAL_POLLITEMID_PROPERTY);
if (!itemid) continue;
id = icalproperty_get_pollitemid(itemid);
/* Remove any existing voter property from the subcomponent */
for (voterp = icalcomponent_get_first_property(comp,
ICAL_VOTER_PROPERTY);
voterp && strcmp(voter, icalproperty_get_voter(voterp));
voterp = icalcomponent_get_next_property(comp,
ICAL_VOTER_PROPERTY));
if (voterp) {
icalcomponent_remove_property(comp, voterp);
icalproperty_free(voterp);
}
/* Find matching poll-item-id in the reply */
for (itemid = icalcomponent_get_first_property(reply,
ICAL_POLLITEMID_PROPERTY);
itemid && (id != icalproperty_get_pollitemid(itemid));
itemid = icalcomponent_get_next_property(reply,
ICAL_POLLITEMID_PROPERTY));
if (itemid) {
icalparameter *param;
/* Add a VOTER property with params from the reply */
voterp = icalproperty_new_clone(mastervoterp);
for (param =
icalproperty_get_first_parameter(itemid,
ICAL_ANY_PARAMETER);
param;
param =
icalproperty_get_next_parameter(itemid,
ICAL_ANY_PARAMETER)) {
switch (icalparameter_isa(param)) {
case ICAL_PUBLICCOMMENT_PARAMETER:
case ICAL_RESPONSE_PARAMETER:
icalproperty_add_parameter(voterp,
icalparameter_new_clone(param));
break;
default:
break;
}
}
icalcomponent_add_property(comp, voterp);
}
}
}
/* sched_reply() helper function
*
* Add voter responses to VPOLL reply and remove subcomponents
*
*/
static void sched_vpoll_reply(icalcomponent *poll, const char *voter)
{
icalcomponent *item, *next;
icalproperty *prop;
for (item = icalcomponent_get_first_component(poll, ICAL_ANY_COMPONENT);
item;
item = next) {
next = icalcomponent_get_next_component(poll, ICAL_ANY_COMPONENT);
prop = icalcomponent_get_first_property(item, ICAL_POLLITEMID_PROPERTY);
if (prop) {
int id = icalproperty_get_pollitemid(prop);
for (prop = icalcomponent_get_first_property(item,
ICAL_VOTER_PROPERTY);
prop;
prop =
icalcomponent_get_next_property(item,
ICAL_VOTER_PROPERTY)) {
if (!strcmp(voter, icalproperty_get_voter(prop))) {
icalproperty *itemid = icalproperty_new_pollitemid(id);
icalparameter *param;
for (param =
icalproperty_get_first_parameter(prop,
ICAL_ANY_PARAMETER);
param;
param =
icalproperty_get_next_parameter(prop,
ICAL_ANY_PARAMETER)) {
switch (icalparameter_isa(param)) {
case ICAL_PUBLICCOMMENT_PARAMETER:
case ICAL_RESPONSE_PARAMETER:
icalproperty_add_parameter(itemid,
icalparameter_new_clone(param));
break;
default:
break;
}
}
icalcomponent_add_property(poll, itemid);
}
}
}
icalcomponent_remove_component(poll, item);
icalcomponent_free(item);
}
}
struct pollstatus {
icalcomponent *item;
struct hash_table voter_table;
};
static void free_pollstatus(void *data)
{
struct pollstatus *status = (struct pollstatus *) data;
if (status) {
free_hash_table(&status->voter_table, NULL);
free(status);
}
}
static int deliver_merge_pollstatus(icalcomponent *ical, icalcomponent *request)
{
int deliver_inbox = 0;
struct hash_table comp_table;
icalcomponent *poll, *sub;
icalproperty *prop;
const char *itemid, *voter;
/* Add each sub-component of old object to hash table for comparison */
construct_hash_table(&comp_table, 10, 1);
poll = icalcomponent_get_first_component(ical, ICAL_VPOLL_COMPONENT);
for (sub = icalcomponent_get_first_component(poll, ICAL_ANY_COMPONENT);
sub;
sub = icalcomponent_get_next_component(poll, ICAL_ANY_COMPONENT)) {
struct pollstatus *status = xmalloc(sizeof(struct pollstatus));
status->item = sub;
prop = icalcomponent_get_first_property(sub, ICAL_POLLITEMID_PROPERTY);
itemid = icalproperty_get_value_as_string(prop);
hash_insert(itemid, status, &comp_table);
/* Add each VOTER to voter hash table */
construct_hash_table(&status->voter_table, 10, 1);
for (prop = icalcomponent_get_first_property(sub, ICAL_VOTER_PROPERTY);
prop;
prop =
icalcomponent_get_next_property(sub, ICAL_VOTER_PROPERTY)) {
voter = icalproperty_get_voter(prop);
hash_insert(voter, prop, &status->voter_table);
}
}
/* Process each sub-component in the iTIP request */
poll = icalcomponent_get_first_component(request, ICAL_VPOLL_COMPONENT);
for (sub = icalcomponent_get_first_component(poll, ICAL_ANY_COMPONENT);
sub;
sub = icalcomponent_get_next_component(poll, ICAL_ANY_COMPONENT)) {
struct pollstatus *status;
prop = icalcomponent_get_first_property(sub, ICAL_POLLITEMID_PROPERTY);
itemid = icalproperty_get_value_as_string(prop);
status = hash_del(itemid, &comp_table);
if (status) {
for (prop = icalcomponent_get_first_property(sub,
ICAL_VOTER_PROPERTY);
prop;
prop = icalcomponent_get_next_property(sub,
ICAL_VOTER_PROPERTY)) {
icalproperty *oldvoter;
voter = icalproperty_get_voter(prop);
oldvoter = hash_del(voter, &status->voter_table);
if (oldvoter) {
icalcomponent_remove_property(status->item, oldvoter);
icalproperty_free(oldvoter);
}
icalcomponent_add_property(status->item,
icalproperty_new_clone(prop));
}
free_pollstatus(status);
}
}
free_hash_table(&comp_table, free_pollstatus);
return deliver_inbox;
}
static void sched_pollstatus(const char *organizer,
struct sched_param *sparam, icalcomponent *ical)
{
struct auth_state *authstate;
struct sched_data sched_data;
icalcomponent *itip, *comp;
icalproperty *prop;
/* XXX Do we need to do more checks here? */
if (sparam->flags & SCHEDTYPE_REMOTE)
authstate = auth_newstate("anonymous");
else
authstate = auth_newstate(sparam->userid);
memset(&sched_data, 0, sizeof(struct sched_data));
sched_data.force_send = ICAL_SCHEDULEFORCESEND_NONE;
/* Create a shell for our iTIP request objects */
itip = icalcomponent_vanew(ICAL_VCALENDAR_COMPONENT,
icalproperty_new_version("2.0"),
icalproperty_new_prodid(ical_prodid),
icalproperty_new_method(ICAL_METHOD_POLLSTATUS),
0);
/* Copy over any CALSCALE property */
prop = icalcomponent_get_first_property(ical, ICAL_CALSCALE_PROPERTY);
if (prop) icalcomponent_add_property(itip, icalproperty_new_clone(prop));
/* Process each VPOLL in resource */
for (comp = icalcomponent_get_first_component(ical, ICAL_VPOLL_COMPONENT);
comp;
comp =icalcomponent_get_next_component(ical, ICAL_VPOLL_COMPONENT)) {
icalcomponent *stat, *poll, *sub;
struct strlist *voters = NULL;
/* Make a working copy of the iTIP */
stat = icalcomponent_new_clone(itip);
/* Make a working copy of the VPOLL and add to pollstatus */
poll = icalcomponent_new_clone(comp);
icalcomponent_add_component(stat, poll);
/* Make list of VOTERs (stripping SCHEDULE-STATUS */
for (prop = icalcomponent_get_first_property(poll, ICAL_VOTER_PROPERTY);
prop;
prop =
icalcomponent_get_next_property(poll, ICAL_VOTER_PROPERTY)) {
const char *voter = icalproperty_get_voter(prop);
if (strcmp(voter, organizer))
appendstrlist(&voters, (char *) icalproperty_get_voter(prop));
icalproperty_remove_parameter_by_name(prop, "SCHEDULE-STATUS");
}
/* Process each sub-component of VPOLL */
for (sub = icalcomponent_get_first_component(poll, ICAL_ANY_COMPONENT);
sub;
sub = icalcomponent_get_next_component(poll, ICAL_ANY_COMPONENT)) {
icalproperty *next;
/* Strip all properties other than POLL-ITEM-ID and VOTER */
for (prop =
icalcomponent_get_first_property(sub, ICAL_ANY_PROPERTY);
prop; prop = next) {
next = icalcomponent_get_next_property(sub, ICAL_ANY_PROPERTY);
switch (icalproperty_isa(prop)) {
case ICAL_POLLITEMID_PROPERTY:
case ICAL_VOTER_PROPERTY:
break;
default:
icalcomponent_remove_property(sub, prop);
icalproperty_free(prop);
break;
}
}
}
/* Attempt to deliver to each voter in the list - removing as we go */
while (voters) {
struct strlist *next = voters->next;
sched_data.itip = stat;
sched_deliver(voters->s, &sched_data, authstate);
free(voters->s);
free(voters);
voters = next;
}
icalcomponent_free(stat);
}
icalcomponent_free(itip);
auth_freestate(authstate);
}
#else /* HAVE_VPOLL */
static void
deliver_merge_vpoll_reply(icalcomponent *ical __attribute__((unused)),
icalcomponent *reply __attribute__((unused)))
{
return;
}
static void sched_vpoll_reply(icalcomponent *poll __attribute__((unused)),
const char *voter __attribute__((unused)))
{
return;
}
static int
deliver_merge_pollstatus(icalcomponent *ical __attribute__((unused)),
icalcomponent *request __attribute__((unused)))
{
return 0;
}
static void sched_pollstatus(const char *organizer __attribute__((unused)),
struct sched_param *sparam __attribute__((unused)),
icalcomponent *ical __attribute__((unused)))
{
return;
}
#endif /* HAVE_VPOLL */
static const char *deliver_merge_reply(icalcomponent *ical,
icalcomponent *reply)
{
struct hash_table comp_table;
icalcomponent *comp, *itip;
icalcomponent_kind kind;
icalproperty *prop, *att;
icalparameter *param;
icalparameter_partstat partstat = ICAL_PARTSTAT_NONE;
icalparameter_rsvp rsvp = ICAL_RSVP_NONE;
const char *recurid, *attendee = NULL, *req_stat = SCHEDSTAT_SUCCESS;
icalproperty_kind recip_kind;
const char *(*get_recipient)(const icalproperty *);
/* Add each component of old object to hash table for comparison */
construct_hash_table(&comp_table, 10, 1);
comp = icalcomponent_get_first_real_component(ical);
kind = icalcomponent_isa(comp);
do {
prop =
icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
if (prop) recurid = icalproperty_get_value_as_string(prop);
else recurid = "";
hash_insert(recurid, comp, &comp_table);
} while ((comp = icalcomponent_get_next_component(ical, kind)));
if (kind == ICAL_VPOLL_COMPONENT) {
recip_kind = ICAL_VOTER_PROPERTY;
get_recipient = &icalproperty_get_voter;
}
else {
recip_kind = ICAL_ATTENDEE_PROPERTY;
get_recipient = &icalproperty_get_attendee;
}
/* Process each component in the iTIP reply */
for (itip = icalcomponent_get_first_component(reply, kind);
itip;
itip = icalcomponent_get_next_component(reply, kind)) {
/* Lookup this comp in the hash table */
prop =
icalcomponent_get_first_property(itip, ICAL_RECURRENCEID_PROPERTY);
if (prop) recurid = icalproperty_get_value_as_string(prop);
else recurid = "";
comp = hash_lookup(recurid, &comp_table);
if (!comp) {
/* New recurrence overridden by attendee.
Create a new recurrence from master component. */
comp = icalcomponent_new_clone(hash_lookup("", &comp_table));
/* Add RECURRENCE-ID */
icalcomponent_add_property(comp, icalproperty_new_clone(prop));
/* Remove RRULE */
prop = icalcomponent_get_first_property(comp, ICAL_RRULE_PROPERTY);
if (prop) {
icalcomponent_remove_property(comp, prop);
icalproperty_free(prop);
}
/* Replace DTSTART, DTEND, SEQUENCE */
prop =
icalcomponent_get_first_property(comp, ICAL_DTSTART_PROPERTY);
if (prop) {
icalcomponent_remove_property(comp, prop);
icalproperty_free(prop);
}
prop =
icalcomponent_get_first_property(itip, ICAL_DTSTART_PROPERTY);
if (prop)
icalcomponent_add_property(comp, icalproperty_new_clone(prop));
prop =
icalcomponent_get_first_property(comp, ICAL_DTEND_PROPERTY);
if (prop) {
icalcomponent_remove_property(comp, prop);
icalproperty_free(prop);
}
prop =
icalcomponent_get_first_property(itip, ICAL_DTEND_PROPERTY);
if (prop)
icalcomponent_add_property(comp, icalproperty_new_clone(prop));
prop =
icalcomponent_get_first_property(comp, ICAL_SEQUENCE_PROPERTY);
if (prop) {
icalcomponent_remove_property(comp, prop);
icalproperty_free(prop);
}
prop =
icalcomponent_get_first_property(itip, ICAL_SEQUENCE_PROPERTY);
if (prop)
icalcomponent_add_property(comp, icalproperty_new_clone(prop));
icalcomponent_add_component(ical, comp);
}
/* Get the sending attendee */
att = icalcomponent_get_first_property(itip, recip_kind);
attendee = get_recipient(att);
param = icalproperty_get_first_parameter(att, ICAL_PARTSTAT_PARAMETER);
if (param) partstat = icalparameter_get_partstat(param);
param = icalproperty_get_first_parameter(att, ICAL_RSVP_PARAMETER);
if (param) rsvp = icalparameter_get_rsvp(param);
prop =
icalcomponent_get_first_property(itip, ICAL_REQUESTSTATUS_PROPERTY);
if (prop) {
struct icalreqstattype rq = icalproperty_get_requeststatus(prop);
req_stat = icalenum_reqstat_code(rq.code);
}
/* Find matching attendee in existing object */
for (prop =
icalcomponent_get_first_property(comp, recip_kind);
prop && strcmp(attendee, get_recipient(prop));
prop =
icalcomponent_get_next_property(comp, recip_kind));
if (!prop) {
/* Attendee added themselves to this recurrence */
prop = icalproperty_new_clone(att);
icalcomponent_add_property(comp, prop);
}
/* Set PARTSTAT */
if (partstat != ICAL_PARTSTAT_NONE) {
param = icalparameter_new_partstat(partstat);
icalproperty_set_parameter(prop, param);
}
/* Set RSVP */
icalproperty_remove_parameter_by_kind(prop, ICAL_RSVP_PARAMETER);
if (rsvp != ICAL_RSVP_NONE) {
param = icalparameter_new_rsvp(rsvp);
icalproperty_add_parameter(prop, param);
}
/* Set SCHEDULE-STATUS */
param = icalparameter_new_schedulestatus(req_stat);
icalproperty_set_parameter(prop, param);
/* Handle VPOLL reply */
if (kind == ICAL_VPOLL_COMPONENT) deliver_merge_vpoll_reply(comp, itip);
}
free_hash_table(&comp_table, NULL);
return attendee;
}
static int deliver_merge_request(const char *attendee,
icalcomponent *ical, icalcomponent *request)
{
int deliver_inbox = 0;
struct hash_table comp_table;
icalcomponent *comp, *itip;
icalcomponent_kind kind = ICAL_NO_COMPONENT;
icalproperty *prop;
icalparameter *param;
const char *tzid, *recurid;
/* Add each VTIMEZONE of old object to hash table for comparison */
construct_hash_table(&comp_table, 10, 1);
for (comp =
icalcomponent_get_first_component(ical, ICAL_VTIMEZONE_COMPONENT);
comp;
comp =
icalcomponent_get_next_component(ical, ICAL_VTIMEZONE_COMPONENT)) {
prop = icalcomponent_get_first_property(comp, ICAL_TZID_PROPERTY);
tzid = icalproperty_get_tzid(prop);
hash_insert(tzid, comp, &comp_table);
}
/* Process each VTIMEZONE in the iTIP request */
for (itip = icalcomponent_get_first_component(request,
ICAL_VTIMEZONE_COMPONENT);
itip;
itip = icalcomponent_get_next_component(request,
ICAL_VTIMEZONE_COMPONENT)) {
/* Lookup this TZID in the hash table */
prop = icalcomponent_get_first_property(itip, ICAL_TZID_PROPERTY);
tzid = icalproperty_get_tzid(prop);
comp = hash_lookup(tzid, &comp_table);
if (comp) {
/* Remove component from old object */
icalcomponent_remove_component(ical, comp);
icalcomponent_free(comp);
}
/* Add new/modified component from iTIP request */
icalcomponent_add_component(ical, icalcomponent_new_clone(itip));
}
free_hash_table(&comp_table, NULL);
/* Add each component of old object to hash table for comparison */
construct_hash_table(&comp_table, 10, 1);
comp = icalcomponent_get_first_real_component(ical);
if (comp) kind = icalcomponent_isa(comp);
for (; comp; comp = icalcomponent_get_next_component(ical, kind)) {
prop =
icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
if (prop) recurid = icalproperty_get_value_as_string(prop);
else recurid = "";
hash_insert(recurid, comp, &comp_table);
}
/* Process each component in the iTIP request */
itip = icalcomponent_get_first_real_component(request);
if (kind == ICAL_NO_COMPONENT) kind = icalcomponent_isa(itip);
for (; itip; itip = icalcomponent_get_next_component(request, kind)) {
icalcomponent *new_comp = icalcomponent_new_clone(itip);
/* Lookup this comp in the hash table */
prop =
icalcomponent_get_first_property(itip, ICAL_RECURRENCEID_PROPERTY);
if (prop) recurid = icalproperty_get_value_as_string(prop);
else recurid = "";
comp = hash_lookup(recurid, &comp_table);
if (comp) {
int old_seq, new_seq;
/* Check if this is something more than an update */
/* XXX Probably need to check PARTSTAT=NEEDS-ACTION
and RSVP=TRUE as well */
old_seq = icalcomponent_get_sequence(comp);
new_seq = icalcomponent_get_sequence(itip);
if (new_seq > old_seq) deliver_inbox = 1;
/* Copy over any COMPLETED, PERCENT-COMPLETE,
or TRANSP properties */
prop =
icalcomponent_get_first_property(comp, ICAL_COMPLETED_PROPERTY);
if (prop) {
icalcomponent_add_property(new_comp,
icalproperty_new_clone(prop));
}
prop =
icalcomponent_get_first_property(comp,
ICAL_PERCENTCOMPLETE_PROPERTY);
if (prop) {
icalcomponent_add_property(new_comp,
icalproperty_new_clone(prop));
}
prop =
icalcomponent_get_first_property(comp, ICAL_TRANSP_PROPERTY);
if (prop) {
icalcomponent_add_property(new_comp,
icalproperty_new_clone(prop));
}
/* Copy over any ORGANIZER;SCHEDULE-STATUS */
/* XXX Do we only do this iff PARTSTAT!=NEEDS-ACTION */
prop =
icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
param = icalproperty_get_schedulestatus_parameter(prop);
if (param) {
param = icalparameter_new_clone(param);
prop =
icalcomponent_get_first_property(new_comp,
ICAL_ORGANIZER_PROPERTY);
icalproperty_add_parameter(prop, param);
}
/* Remove component from old object */
icalcomponent_remove_component(ical, comp);
icalcomponent_free(comp);
}
else {
/* New component */
deliver_inbox = 1;
}
if (config_allowsched == IMAP_ENUM_CALDAV_ALLOWSCHEDULING_APPLE &&
kind == ICAL_VEVENT_COMPONENT) {
/* Make VEVENT component transparent if recipient ATTENDEE
PARTSTAT=NEEDS-ACTION (for compatibility with CalendarServer) */
for (prop =
icalcomponent_get_first_property(new_comp,
ICAL_ATTENDEE_PROPERTY);
prop && strcmp(icalproperty_get_attendee(prop), attendee);
prop =
icalcomponent_get_next_property(new_comp,
ICAL_ATTENDEE_PROPERTY));
param =
icalproperty_get_first_parameter(prop, ICAL_PARTSTAT_PARAMETER);
if (param &&
icalparameter_get_partstat(param) ==
ICAL_PARTSTAT_NEEDSACTION) {
prop =
icalcomponent_get_first_property(new_comp,
ICAL_TRANSP_PROPERTY);
if (prop)
icalproperty_set_transp(prop, ICAL_TRANSP_TRANSPARENT);
else {
prop = icalproperty_new_transp(ICAL_TRANSP_TRANSPARENT);
icalcomponent_add_property(new_comp, prop);
}
}
}
/* Add new/modified component from iTIP request */
icalcomponent_add_component(ical, new_comp);
}
free_hash_table(&comp_table, NULL);
return deliver_inbox;
}
/* Deliver scheduling object to local recipient */
static void sched_deliver_local(const char *recipient,
struct sched_param *sparam,
struct sched_data *sched_data,
struct auth_state *authstate)
{
int r = 0, rights, reqd_privs, deliver_inbox = 1;
const char *userid = sparam->userid, *attendee = NULL;
static struct buf resource = BUF_INITIALIZER;
static unsigned sched_count = 0;
const char *mailboxname = NULL;
mbentry_t *mbentry = NULL;
struct mailbox *mailbox = NULL, *inbox = NULL;
struct caldav_db *caldavdb = NULL;
struct caldav_data *cdata;
icalcomponent *ical = NULL;
icalproperty_method method;
icalcomponent_kind kind;
icalcomponent *comp;
icalproperty *prop;
struct transaction_t txn;
/* Check ACL of sender on recipient's Scheduling Inbox */
mailboxname = caldav_mboxname(userid, SCHED_INBOX);
r = mboxlist_lookup(mailboxname, &mbentry, NULL);
if (r) {
syslog(LOG_INFO, "mboxlist_lookup(%s) failed: %s",
mailboxname, error_message(r));
sched_data->status =
sched_data->ischedule ? REQSTAT_REJECTED : SCHEDSTAT_REJECTED;
goto done;
}
rights = cyrus_acl_myrights(authstate, mbentry->acl);
mboxlist_entry_free(&mbentry);
reqd_privs = sched_data->is_reply ? DACL_REPLY : DACL_INVITE;
if (!(rights & reqd_privs)) {
sched_data->status =
sched_data->ischedule ? REQSTAT_NOPRIVS : SCHEDSTAT_NOPRIVS;
goto done;
}
/* Open recipient's Inbox for reading */
if ((r = mailbox_open_irl(mailboxname, &inbox))) {
syslog(LOG_ERR, "mailbox_open_irl(%s) failed: %s",
mailboxname, error_message(r));
sched_data->status =
sched_data->ischedule ? REQSTAT_TEMPFAIL : SCHEDSTAT_TEMPFAIL;
goto done;
}
/* Get METHOD of the iTIP message */
method = icalcomponent_get_method(sched_data->itip);
/* Search for iCal UID in recipient's calendars */
caldavdb = caldav_open_userid(userid, CALDAV_CREATE);
if (!caldavdb) {
sched_data->status =
sched_data->ischedule ? REQSTAT_TEMPFAIL : SCHEDSTAT_TEMPFAIL;
goto done;
}
caldav_lookup_uid(caldavdb,
icalcomponent_get_uid(sched_data->itip), 0, &cdata);
if (cdata->dav.mailbox) {
mailboxname = cdata->dav.mailbox;
buf_setcstr(&resource, cdata->dav.resource);
}
else if (sched_data->is_reply) {
/* Can't find object belonging to organizer - ignore reply */
sched_data->status =
sched_data->ischedule ? REQSTAT_PERMFAIL : SCHEDSTAT_PERMFAIL;
goto done;
}
else if (method == ICAL_METHOD_CANCEL || method == ICAL_METHOD_POLLSTATUS) {
/* Can't find object belonging to attendee - we're done */
sched_data->status =
sched_data->ischedule ? REQSTAT_SUCCESS : SCHEDSTAT_DELIVERED;
goto done;
}
else {
/* Can't find object belonging to attendee - use default calendar */
mailboxname = caldav_mboxname(userid, SCHED_DEFAULT);
buf_reset(&resource);
buf_printf(&resource, "%s.ics",
icalcomponent_get_uid(sched_data->itip));
/* Create new attendee object */
ical = icalcomponent_vanew(ICAL_VCALENDAR_COMPONENT, 0);
/* Copy over VERSION property */
prop = icalcomponent_get_first_property(sched_data->itip,
ICAL_VERSION_PROPERTY);
icalcomponent_add_property(ical, icalproperty_new_clone(prop));
/* Copy over PRODID property */
prop = icalcomponent_get_first_property(sched_data->itip,
ICAL_PRODID_PROPERTY);
icalcomponent_add_property(ical, icalproperty_new_clone(prop));
/* Copy over any CALSCALE property */
prop = icalcomponent_get_first_property(sched_data->itip,
ICAL_CALSCALE_PROPERTY);
if (prop) {
icalcomponent_add_property(ical,
icalproperty_new_clone(prop));
}
}
/* Open recipient's calendar for reading */
r = mailbox_open_irl(mailboxname, &mailbox);
if (r) {
syslog(LOG_ERR, "mailbox_open_irl(%s) failed: %s",
mailboxname, error_message(r));
sched_data->status =
sched_data->ischedule ? REQSTAT_TEMPFAIL : SCHEDSTAT_TEMPFAIL;
goto done;
}
if (cdata->dav.imap_uid) {
struct index_record record;
struct buf msg_buf = BUF_INITIALIZER;
/* Load message containing the resource and parse iCal data */
r = mailbox_find_index_record(mailbox, cdata->dav.imap_uid, &record);
if (!r) r = mailbox_map_record(mailbox, &record, &msg_buf);
if (r) {
syslog(LOG_ERR, "mailbox_map_record(%s, %u) failed: %s",
mailbox->name, record.uid, error_message(r));
goto done;
}
ical = icalparser_parse_string(buf_base(&msg_buf) + record.header_size);
buf_free(&msg_buf);
for (comp = icalcomponent_get_first_component(sched_data->itip,
ICAL_ANY_COMPONENT);
comp;
comp = icalcomponent_get_next_component(sched_data->itip,
ICAL_ANY_COMPONENT)) {
/* Don't allow component type to be changed */
int reject = 0;
kind = icalcomponent_isa(comp);
switch (kind) {
case ICAL_VEVENT_COMPONENT:
if (cdata->comp_type != CAL_COMP_VEVENT) reject = 1;
break;
case ICAL_VTODO_COMPONENT:
if (cdata->comp_type != CAL_COMP_VTODO) reject = 1;
break;
case ICAL_VJOURNAL_COMPONENT:
if (cdata->comp_type != CAL_COMP_VJOURNAL) reject = 1;
break;
case ICAL_VFREEBUSY_COMPONENT:
if (cdata->comp_type != CAL_COMP_VFREEBUSY) reject = 1;
break;
case ICAL_VAVAILABILITY_COMPONENT:
if (cdata->comp_type != CAL_COMP_VAVAILABILITY) reject = 1;
break;
#ifdef HAVE_VPOLL
case ICAL_VPOLL_COMPONENT:
if (cdata->comp_type != CAL_COMP_VPOLL) reject = 1;
break;
#endif
default:
break;
}
/* Don't allow ORGANIZER to be changed */
if (!reject && cdata->organizer) {
prop =
icalcomponent_get_first_property(comp,
ICAL_ORGANIZER_PROPERTY);
if (prop) {
const char *organizer =
organizer = icalproperty_get_organizer(prop);
if (!strncmp(organizer, "mailto:", 7)) organizer += 7;
if (strcmp(cdata->organizer, organizer)) reject = 1;
}
}
if (reject) {
sched_data->status = sched_data->ischedule ?
REQSTAT_REJECTED : SCHEDSTAT_REJECTED;
goto done;
}
}
}
switch (method) {
case ICAL_METHOD_CANCEL:
/* Get component type */
comp = icalcomponent_get_first_real_component(ical);
kind = icalcomponent_isa(comp);
/* Set STATUS:CANCELLED on all components */
do {
icalcomponent_set_status(comp, ICAL_STATUS_CANCELLED);
icalcomponent_set_sequence(comp,
icalcomponent_get_sequence(comp)+1);
} while ((comp = icalcomponent_get_next_component(ical, kind)));
break;
case ICAL_METHOD_REPLY:
attendee = deliver_merge_reply(ical, sched_data->itip);
break;
case ICAL_METHOD_REQUEST:
deliver_inbox = deliver_merge_request(recipient,
ical, sched_data->itip);
break;
case ICAL_METHOD_POLLSTATUS:
deliver_inbox = deliver_merge_pollstatus(ical, sched_data->itip);
break;
default:
/* Unknown METHOD -- ignore it */
syslog(LOG_ERR, "Unknown iTIP method: %s",
icalenum_method_to_string(method));
sched_data->is_reply = 0;
goto inbox;
}
/* Store the (updated) object in the recipients's calendar */
mailbox_unlock_index(mailbox, NULL);
r = store_resource(&txn, ical, mailbox, buf_cstring(&resource),
caldavdb, OVERWRITE_YES, NEW_STAG);
if (r == HTTP_CREATED || r == HTTP_NO_CONTENT) {
sched_data->status =
sched_data->ischedule ? REQSTAT_SUCCESS : SCHEDSTAT_DELIVERED;
}
else {
syslog(LOG_ERR, "store_resource(%s) failed: %s (%s)",
mailbox->name, error_message(r), txn.error.resource);
sched_data->status =
sched_data->ischedule ? REQSTAT_TEMPFAIL : SCHEDSTAT_TEMPFAIL;
goto done;
}
inbox:
if (deliver_inbox) {
/* Create a name for the new iTIP message resource */
buf_reset(&resource);
buf_printf(&resource, "%x-%d-%ld-%u.ics",
strhash(icalcomponent_get_uid(sched_data->itip)), getpid(),
time(0), sched_count++);
/* Store the message in the recipient's Inbox */
mailbox_unlock_index(inbox, NULL);
r = store_resource(&txn, sched_data->itip, inbox,
buf_cstring(&resource), caldavdb, OVERWRITE_NO, 0);
/* XXX What do we do if storing to Inbox fails? */
}
/* XXX Should this be a config option? - it might have perf implications */
if (sched_data->is_reply) {
/* Send updates to attendees - skipping sender of reply */
comp = icalcomponent_get_first_real_component(ical);
if (icalcomponent_isa(comp) == ICAL_VPOLL_COMPONENT)
sched_pollstatus(recipient, sparam, ical);
else
sched_request(recipient, sparam, NULL, ical, attendee);
}
done:
if (ical) icalcomponent_free(ical);
mailbox_close(&inbox);
mailbox_close(&mailbox);
if (caldavdb) caldav_close(caldavdb);
}
/* Deliver scheduling object to recipient's Inbox */
void sched_deliver(const char *recipient, void *data, void *rock)
{
struct sched_data *sched_data = (struct sched_data *) data;
struct auth_state *authstate = (struct auth_state *) rock;
struct sched_param sparam;
int islegal;
/* Check SCHEDULE-FORCE-SEND value */
switch (sched_data->force_send) {
case ICAL_SCHEDULEFORCESEND_NONE:
islegal = 1;
break;
case ICAL_SCHEDULEFORCESEND_REPLY:
islegal = sched_data->is_reply;
break;
case ICAL_SCHEDULEFORCESEND_REQUEST:
islegal = !sched_data->is_reply;
break;
default:
islegal = 0;
break;
}
if (!islegal) {
sched_data->status = SCHEDSTAT_PARAM;
return;
}
if (caladdress_lookup(recipient, &sparam)) {
sched_data->status =
sched_data->ischedule ? REQSTAT_NOUSER : SCHEDSTAT_NOUSER;
/* Unknown user */
return;
}
if (sparam.flags) {
/* Remote recipient */
sched_deliver_remote(recipient, &sparam, sched_data);
}
else {
/* Local recipient */
sched_deliver_local(recipient, &sparam, sched_data, authstate);
}
}
struct comp_data {
icalcomponent *comp;
icalparameter_partstat partstat;
int sequence;
};
static void free_comp_data(void *data) {
struct comp_data *comp_data = (struct comp_data *) data;
if (comp_data) {
if (comp_data->comp) icalcomponent_free(comp_data->comp);
free(comp_data);
}
}
/*
* sched_request/reply() helper function
*
* Update DTSTAMP, remove VALARMs,
* optionally remove scheduling params from ORGANIZER
*/
static void clean_component(icalcomponent *comp, int clean_org)
{
icalcomponent *alarm, *next;
icalproperty *prop;
icaltimezone *utc = icaltimezone_get_utc_timezone();
time_t now = time(NULL);
/* Replace DTSTAMP on component */
prop = icalcomponent_get_first_property(comp,
ICAL_DTSTAMP_PROPERTY);
icalcomponent_remove_property(comp, prop);
icalproperty_free(prop);
prop =
icalproperty_new_dtstamp(icaltime_from_timet_with_zone(now, 0, utc));
icalcomponent_add_property(comp, prop);
/* Remove any VALARM components */
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 (clean_org) {
/* Grab the organizer */
prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
/* Remove CalDAV Scheduling parameters from organizer */
icalproperty_remove_parameter_by_name(prop, "SCHEDULE-AGENT");
icalproperty_remove_parameter_by_name(prop, "SCHEDULE-FORCE-SEND");
}
}
/*
* sched_request() helper function
*
* Add EXDATE to master component if attendee is excluded from recurrence
*/
struct exclude_rock {
unsigned ncomp;
icalcomponent *comp;
};
static void sched_exclude(const char *attendee __attribute__((unused)),
void *data, void *rock)
{
struct sched_data *sched_data = (struct sched_data *) data;
struct exclude_rock *erock = (struct exclude_rock *) rock;
if (!(sched_data->comp_mask & (1<<erock->ncomp))) {
icalproperty *recurid, *exdate;
struct icaltimetype exdt;
icalparameter *param;
/* Fetch the RECURRENCE-ID and use it to create a new EXDATE */
recurid = icalcomponent_get_first_property(erock->comp,
ICAL_RECURRENCEID_PROPERTY);
exdt = icalproperty_get_recurrenceid(recurid);
exdate = icalproperty_new_exdate(exdt);
/* Copy any parameters from RECURRENCE-ID to EXDATE */
param = icalproperty_get_first_parameter(recurid, ICAL_TZID_PARAMETER);
if (param) {
icalproperty_add_parameter(exdate, icalparameter_new_clone(param));
}
param = icalproperty_get_first_parameter(recurid, ICAL_VALUE_PARAMETER);
if (param) {
icalproperty_add_parameter(exdate, icalparameter_new_clone(param));
}
/* XXX Need to handle RANGE parameter */
/* Add the EXDATE to the master component for this attendee */
icalcomponent_add_property(sched_data->master, exdate);
}
}
/*
* sched_request() helper function
*
* Process all attendees in the given component and add a
* properly modified component to the attendee's iTIP request if necessary
*/
static void process_attendees(icalcomponent *comp, unsigned ncomp,
const char *organizer, const char *att_update,
struct hash_table *att_table,
icalcomponent *itip, unsigned needs_action)
{
icalcomponent *copy;
icalproperty *prop;
icalparameter *param;
icalcomponent_kind kind = icalcomponent_isa(comp);
icalproperty_kind recip_kind;
const char *(*get_recipient)(const icalproperty *);
if (kind == ICAL_VPOLL_COMPONENT) {
recip_kind = ICAL_VOTER_PROPERTY;
get_recipient = &icalproperty_get_voter;
}
else {
recip_kind = ICAL_ATTENDEE_PROPERTY;
get_recipient = &icalproperty_get_attendee;
}
/* Strip SCHEDULE-STATUS from each attendee
and optionally set PROPSTAT=NEEDS-ACTION */
for (prop = icalcomponent_get_first_property(comp, recip_kind);
prop;
prop = icalcomponent_get_next_property(comp, recip_kind)) {
const char *attendee = get_recipient(prop);
/* Don't modify attendee == organizer */
if (!strcmp(attendee, organizer)) continue;
icalproperty_remove_parameter_by_name(prop, "SCHEDULE-STATUS");
if (needs_action) {
/* Set PARTSTAT */
param = icalparameter_new_partstat(ICAL_PARTSTAT_NEEDSACTION);
icalproperty_set_parameter(prop, param);
}
}
/* Clone a working copy of the component */
copy = icalcomponent_new_clone(comp);
clean_component(copy, 0);
/* Process each attendee */
for (prop = icalcomponent_get_first_property(copy, recip_kind);
prop;
prop = icalcomponent_get_next_property(copy, recip_kind)) {
unsigned do_sched = 1;
icalparameter_scheduleforcesend force_send =
ICAL_SCHEDULEFORCESEND_NONE;
const char *attendee = get_recipient(prop);
/* Don't schedule attendee == organizer */
if (!strcmp(attendee, organizer)) continue;
/* Don't send an update to the attendee that just sent a reply */
if (att_update && !strcmp(attendee, att_update)) continue;
/* Check CalDAV Scheduling parameters */
param = icalproperty_get_scheduleagent_parameter(prop);
if (param) {
icalparameter_scheduleagent agent =
icalparameter_get_scheduleagent(param);
if (agent != ICAL_SCHEDULEAGENT_SERVER) do_sched = 0;
icalproperty_remove_parameter_by_ref(prop, param);
}
param = icalproperty_get_scheduleforcesend_parameter(prop);
if (param) {
force_send = icalparameter_get_scheduleforcesend(param);
icalproperty_remove_parameter_by_ref(prop, param);
}
/* Create/update iTIP request for this attendee */
if (do_sched) {
struct sched_data *sched_data;
icalcomponent *new_comp;
sched_data = hash_lookup(attendee, att_table);
if (!sched_data) {
/* New attendee - add it to the hash table */
sched_data = xzmalloc(sizeof(struct sched_data));
sched_data->itip = icalcomponent_new_clone(itip);
sched_data->force_send = force_send;
hash_insert(attendee, sched_data, att_table);
}
new_comp = icalcomponent_new_clone(copy);
icalcomponent_add_component(sched_data->itip, new_comp);
sched_data->comp_mask |= (1 << ncomp);
/* XXX We assume that the master component is always first */
if (!ncomp) sched_data->master = new_comp;
}
}
/* XXX We assume that the master component is always first */
if (ncomp) {
/* Handle attendees that are excluded from this recurrence */
struct exclude_rock erock = { ncomp, copy };
hash_enumerate(att_table, sched_exclude, &erock);
}
icalcomponent_free(copy);
}
/*
* sched_request() helper function
*
* Organizer removed this component, mark it as cancelled for all attendees
*/
struct cancel_rock {
const char *organizer;
struct hash_table *att_table;
icalcomponent *itip;
};
static void sched_cancel(const char *recurid __attribute__((unused)),
void *data, void *rock)
{
struct comp_data *old_data = (struct comp_data *) data;
struct cancel_rock *crock = (struct cancel_rock *) rock;
/* Deleting the object -- set STATUS to CANCELLED for component */
icalcomponent_set_status(old_data->comp, ICAL_STATUS_CANCELLED);
// icalcomponent_set_sequence(old_data->comp, old_data->sequence+1);
process_attendees(old_data->comp, 0, crock->organizer, NULL,
crock->att_table, crock->itip, 0);
}
/*
* Compare the properties of the given kind in two components.
* Returns 0 if equal, 1 otherwise.
*
* If the property exists in neither comp, then they are equal.
* If the property exists in only 1 comp, then they are not equal.
* if the property is RDATE or EXDATE, create an MD5 hash of all
* property strings for each component and compare the hashes.
* Otherwise compare the two property strings.
*/
static unsigned propcmp(icalcomponent *oldical, icalcomponent *newical,
icalproperty_kind kind)
{
icalproperty *oldprop = icalcomponent_get_first_property(oldical, kind);
icalproperty *newprop = icalcomponent_get_first_property(newical, kind);
if (!oldprop) return (newprop != NULL);
else if (!newprop) return 1;
else if ((kind == ICAL_RDATE_PROPERTY) || (kind == ICAL_EXDATE_PROPERTY)) {
MD5_CTX ctx;
const char *str;
unsigned char old_md5[MD5_DIGEST_LENGTH], new_md5[MD5_DIGEST_LENGTH];
MD5Init(&ctx);
do {
str = icalproperty_get_value_as_string(oldprop);
MD5Update(&ctx, str, strlen(str));
} while ((oldprop = icalcomponent_get_next_property(oldical, kind)));
MD5Final(old_md5, &ctx);
MD5Init(&ctx);
do {
str = icalproperty_get_value_as_string(newprop);
MD5Update(&ctx, str, strlen(str));
} while ((newprop = icalcomponent_get_next_property(newical, kind)));
MD5Final(new_md5, &ctx);
return (memcmp(old_md5, new_md5, MD5_DIGEST_LENGTH) != 0);
}
else {
return (strcmp(icalproperty_get_value_as_string(oldprop),
icalproperty_get_value_as_string(newprop)) != 0);
}
}
/* Create and deliver an organizer scheduling request */
static void sched_request(const char *organizer, struct sched_param *sparam,
icalcomponent *oldical, icalcomponent *newical,
const char *att_update)
{
int r;
icalproperty_method method;
struct auth_state *authstate;
icalcomponent *ical, *req, *comp;
icalproperty *prop;
icalcomponent_kind kind;
struct hash_table att_table, comp_table;
const char *sched_stat = NULL, *recurid;
struct comp_data *old_data;
/* Check what kind of action we are dealing with */
if (!newical) {
/* Remove */
ical = oldical;
method = ICAL_METHOD_CANCEL;
}
else {
/* Create / Modify */
ical = newical;
method = ICAL_METHOD_REQUEST;
}
if (!att_update) {
int rights = 0;
mbentry_t *mbentry = NULL;
/* Check ACL of auth'd user on userid's Scheduling Outbox */
const char *outboxname = caldav_mboxname(sparam->userid, SCHED_OUTBOX);
r = mboxlist_lookup(outboxname, &mbentry, NULL);
if (r) {
syslog(LOG_INFO, "mboxlist_lookup(%s) failed: %s",
outboxname, error_message(r));
}
else {
rights = cyrus_acl_myrights(httpd_authstate, mbentry->acl);
mboxlist_entry_free(&mbentry);
}
if (!(rights & DACL_INVITE)) {
/* DAV:need-privileges */
sched_stat = SCHEDSTAT_NOPRIVS;
goto done;
}
}
/* Create a shell for our iTIP request objects */
req = icalcomponent_vanew(ICAL_VCALENDAR_COMPONENT,
icalproperty_new_version("2.0"),
icalproperty_new_prodid(ical_prodid),
icalproperty_new_method(method),
0);
/* XXX Make sure SEQUENCE is incremented */
/* Copy over any CALSCALE property */
prop = icalcomponent_get_first_property(ical, ICAL_CALSCALE_PROPERTY);
if (prop) {
icalcomponent_add_property(req,
icalproperty_new_clone(prop));
}
/* Copy over any VTIMEZONE components */
for (comp = icalcomponent_get_first_component(ical,
ICAL_VTIMEZONE_COMPONENT);
comp;
comp = icalcomponent_get_next_component(ical,
ICAL_VTIMEZONE_COMPONENT)) {
icalcomponent_add_component(req,
icalcomponent_new_clone(comp));
}
comp = icalcomponent_get_first_real_component(ical);
kind = icalcomponent_isa(comp);
/* Add each component of old object to hash table for comparison */
construct_hash_table(&comp_table, 10, 1);
if (!att_update && oldical) {
comp = icalcomponent_get_first_real_component(oldical);
/* If the existing object isn't a scheduling object,
we don't need to compare components, treat them as new */
if (icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY)) {
do {
old_data = xzmalloc(sizeof(struct comp_data));
old_data->comp = comp;
old_data->sequence = icalcomponent_get_sequence(comp);
prop =
icalcomponent_get_first_property(comp,
ICAL_RECURRENCEID_PROPERTY);
if (prop) recurid = icalproperty_get_value_as_string(prop);
else recurid = "";
hash_insert(recurid, old_data, &comp_table);
} while ((comp = icalcomponent_get_next_component(oldical, kind)));
}
}
/* Create hash table of attendees */
construct_hash_table(&att_table, 10, 1);
/* Process each component of new object */
if (newical) {
unsigned ncomp = 0;
comp = icalcomponent_get_first_real_component(newical);
do {
unsigned changed = 1, needs_action = 0;
prop = icalcomponent_get_first_property(comp,
ICAL_RECURRENCEID_PROPERTY);
if (prop) recurid = icalproperty_get_value_as_string(prop);
else recurid = "";
old_data = hash_del(recurid, &comp_table);
if (old_data) {
/* Per RFC 6638, Section 3.2.8: We need to compare
DTSTART, DTEND, DURATION, DUE, RRULE, RDATE, EXDATE */
if (propcmp(old_data->comp, comp, ICAL_DTSTART_PROPERTY))
needs_action = 1;
else if (propcmp(old_data->comp, comp, ICAL_DTEND_PROPERTY))
needs_action = 1;
else if (propcmp(old_data->comp, comp, ICAL_DURATION_PROPERTY))
needs_action = 1;
else if (propcmp(old_data->comp, comp, ICAL_DUE_PROPERTY))
needs_action = 1;
else if (propcmp(old_data->comp, comp, ICAL_RRULE_PROPERTY))
needs_action = 1;
else if (propcmp(old_data->comp, comp, ICAL_RDATE_PROPERTY))
needs_action = 1;
else if (propcmp(old_data->comp, comp, ICAL_EXDATE_PROPERTY))
needs_action = 1;
else if (kind == ICAL_VPOLL_COMPONENT) {
}
if (needs_action &&
(old_data->sequence >= icalcomponent_get_sequence(comp))) {
/* Make sure SEQUENCE is set properly */
icalcomponent_set_sequence(comp,
old_data->sequence + 1);
}
free(old_data);
}
if (changed) {
/* Process all attendees in created/modified components */
process_attendees(comp, ncomp++, organizer, att_update,
&att_table, req, needs_action);
}
} while ((comp = icalcomponent_get_next_component(newical, kind)));
}
if (oldical) {
/* Cancel any components that have been left behind in the old obj */
struct cancel_rock crock = { organizer, &att_table, req };
hash_enumerate(&comp_table, sched_cancel, &crock);
}
free_hash_table(&comp_table, free);
icalcomponent_free(req);
/* Attempt to deliver requests to attendees */
/* XXX Do we need to do more checks here? */
if (sparam->flags & SCHEDTYPE_REMOTE)
authstate = auth_newstate("anonymous");
else
authstate = auth_newstate(sparam->userid);
hash_enumerate(&att_table, sched_deliver, authstate);
auth_freestate(authstate);
done:
if (newical) {
unsigned ncomp = 0;
icalproperty_kind recip_kind;
const char *(*get_recipient)(const icalproperty *);
/* Set SCHEDULE-STATUS for each attendee in organizer object */
comp = icalcomponent_get_first_real_component(newical);
kind = icalcomponent_isa(comp);
if (kind == ICAL_VPOLL_COMPONENT) {
recip_kind = ICAL_VOTER_PROPERTY;
get_recipient = &icalproperty_get_voter;
}
else {
recip_kind = ICAL_ATTENDEE_PROPERTY;
get_recipient = &icalproperty_get_attendee;
}
do {
for (prop =
icalcomponent_get_first_property(comp, recip_kind);
prop;
prop =
icalcomponent_get_next_property(comp, recip_kind)) {
const char *stat = NULL;
const char *attendee = get_recipient(prop);
/* Don't set status if attendee == organizer */
if (!strcmp(attendee, organizer)) continue;
if (sched_stat) stat = sched_stat;
else {
struct sched_data *sched_data;
sched_data = hash_lookup(attendee, &att_table);
if (sched_data && (sched_data->comp_mask & (1 << ncomp)))
stat = sched_data->status;
}
if (stat) {
/* Set SCHEDULE-STATUS */
icalparameter *param;
param = icalparameter_new_schedulestatus(stat);
icalproperty_set_parameter(prop, param);
}
}
ncomp++;
} while ((comp = icalcomponent_get_next_component(newical, kind)));
}
/* Cleanup */
if (!sched_stat) free_hash_table(&att_table, free_sched_data);
}
/*
* sched_reply() helper function
*
* Remove all attendees from 'comp' other than the one corresponding to 'userid'
*
* Returns the new trimmed component (must be freed by caller)
* Optionally returns the 'attendee' property, his/her 'propstat',
* and the 'recurid' of the component
*/
static icalcomponent *trim_attendees(icalcomponent *comp, const char *userid,
icalproperty **attendee,
icalparameter_partstat *partstat,
const char **recurid)
{
icalcomponent *copy;
icalproperty *prop, *nextprop, *myattendee = NULL;
icalcomponent_kind kind;
icalproperty_kind recip_kind;
const char *(*get_recipient)(const icalproperty *);
if (partstat) *partstat = ICAL_PARTSTAT_NONE;
/* Clone a working copy of the component */
copy = icalcomponent_new_clone(comp);
kind = icalcomponent_isa(comp);
if (kind == ICAL_VPOLL_COMPONENT) {
recip_kind = ICAL_VOTER_PROPERTY;
get_recipient = &icalproperty_get_voter;
}
else {
recip_kind = ICAL_ATTENDEE_PROPERTY;
get_recipient = &icalproperty_get_attendee;
}
/* Locate userid in the attendee list (stripping others) */
for (prop = icalcomponent_get_first_property(copy, recip_kind);
prop;
prop = nextprop) {
const char *att = get_recipient(prop);
struct sched_param sparam;
nextprop = icalcomponent_get_next_property(copy, recip_kind);
if (!myattendee &&
!caladdress_lookup(att, &sparam) &&
!(sparam.flags & SCHEDTYPE_REMOTE) &&
!strcmp(sparam.userid, userid)) {
/* Found it */
myattendee = prop;
if (partstat) {
/* Get the PARTSTAT */
icalparameter *param =
icalproperty_get_first_parameter(myattendee,
ICAL_PARTSTAT_PARAMETER);
if (param) *partstat = icalparameter_get_partstat(param);
}
}
else {
/* Some other attendee, remove it */
icalcomponent_remove_property(copy, prop);
icalproperty_free(prop);
}
}
if (attendee) *attendee = myattendee;
if (recurid) {
prop = icalcomponent_get_first_property(copy,
ICAL_RECURRENCEID_PROPERTY);
if (prop) *recurid = icalproperty_get_value_as_string(prop);
else *recurid = "";
}
return copy;
}
/*
* sched_reply() helper function
*
* Attendee removed this component, mark it as declined for the organizer.
*/
static void sched_decline(const char *recurid __attribute__((unused)),
void *data, void *rock)
{
struct comp_data *old_data = (struct comp_data *) data;
icalcomponent *itip = (icalcomponent *) rock;
icalproperty *myattendee;
icalparameter *param;
/* Don't send a decline for cancelled components */
if (icalcomponent_get_status(old_data->comp) == ICAL_STATUS_CANCELLED)
return;
myattendee = icalcomponent_get_first_property(old_data->comp,
ICAL_ATTENDEE_PROPERTY);
param = icalparameter_new_partstat(ICAL_PARTSTAT_DECLINED);
icalproperty_set_parameter(myattendee, param);
clean_component(old_data->comp, 1);
icalcomponent_add_component(itip, old_data->comp);
}
/* Create and deliver an attendee scheduling reply */
static void sched_reply(const char *userid,
icalcomponent *oldical, icalcomponent *newical)
{
int r, rights = 0;
mbentry_t *mbentry = NULL;
const char *outboxname;
icalcomponent *ical;
struct sched_data *sched_data;
struct auth_state *authstate;
icalcomponent *comp;
icalproperty *prop;
icalparameter *param;
icalcomponent_kind kind;
icalparameter_scheduleforcesend force_send = ICAL_SCHEDULEFORCESEND_NONE;
const char *organizer, *recurid;
struct hash_table comp_table;
struct comp_data *old_data;
/* Check what kind of action we are dealing with */
if (!newical) {
/* Remove */
ical = oldical;
}
else {
/* Create / Modify */
ical = newical;
}
/* Check CalDAV Scheduling parameters on the organizer */
comp = icalcomponent_get_first_real_component(ical);
kind = icalcomponent_isa(comp);
prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
organizer = icalproperty_get_organizer(prop);
param = icalproperty_get_scheduleagent_parameter(prop);
if (param &&
icalparameter_get_scheduleagent(param) != ICAL_SCHEDULEAGENT_SERVER) {
/* We are not supposed to send replies to the organizer */
return;
}
param = icalproperty_get_scheduleforcesend_parameter(prop);
if (param) force_send = icalparameter_get_scheduleforcesend(param);
sched_data = xzmalloc(sizeof(struct sched_data));
sched_data->is_reply = 1;
sched_data->force_send = force_send;
/* Check ACL of auth'd user on userid's Scheduling Outbox */
outboxname = caldav_mboxname(userid, SCHED_OUTBOX);
r = mboxlist_lookup(outboxname, &mbentry, NULL);
if (r) {
syslog(LOG_INFO, "mboxlist_lookup(%s) failed: %s",
outboxname, error_message(r));
}
else {
rights = cyrus_acl_myrights(httpd_authstate, mbentry->acl);
mboxlist_entry_free(&mbentry);
}
if (!(rights & DACL_REPLY)) {
/* DAV:need-privileges */
if (newical) sched_data->status = SCHEDSTAT_NOPRIVS;
goto done;
}
/* Create our reply iCal object */
sched_data->itip =
icalcomponent_vanew(ICAL_VCALENDAR_COMPONENT,
icalproperty_new_version("2.0"),
icalproperty_new_prodid(ical_prodid),
icalproperty_new_method(ICAL_METHOD_REPLY),
0);
/* XXX Make sure SEQUENCE is incremented */
/* Copy over any CALSCALE property */
prop = icalcomponent_get_first_property(ical, ICAL_CALSCALE_PROPERTY);
if (prop) {
icalcomponent_add_property(sched_data->itip,
icalproperty_new_clone(prop));
}
/* Copy over any VTIMEZONE components */
for (comp = icalcomponent_get_first_component(ical,
ICAL_VTIMEZONE_COMPONENT);
comp;
comp = icalcomponent_get_next_component(ical,
ICAL_VTIMEZONE_COMPONENT)) {
icalcomponent_add_component(sched_data->itip,
icalcomponent_new_clone(comp));
}
/* Add each component of old object to hash table for comparison */
construct_hash_table(&comp_table, 10, 1);
if (oldical) {
comp = icalcomponent_get_first_real_component(oldical);
do {
old_data = xzmalloc(sizeof(struct comp_data));
old_data->comp = trim_attendees(comp, userid, NULL,
&old_data->partstat, &recurid);
hash_insert(recurid, old_data, &comp_table);
} while ((comp = icalcomponent_get_next_component(oldical, kind)));
}
/* Process each component of new object */
if (newical) {
unsigned ncomp = 0;
comp = icalcomponent_get_first_real_component(newical);
do {
icalcomponent *copy;
icalproperty *myattendee;
icalparameter_partstat partstat;
int changed = 1;
copy = trim_attendees(comp, userid,
&myattendee, &partstat, &recurid);
if (myattendee) {
/* Found our userid */
old_data = hash_del(recurid, &comp_table);
if (old_data) {
if (kind == ICAL_VPOLL_COMPONENT) {
/* VPOLL replies always override existing votes */
sched_vpoll_reply(copy,
icalproperty_get_voter(myattendee));
}
else {
/* XXX Need to check EXDATE */
/* Compare PARTSTAT in the two components */
if (old_data->partstat == partstat) {
changed = 0;
}
}
free_comp_data(old_data);
}
}
else {
/* Our user isn't in this component */
/* XXX Can this actually happen? */
changed = 0;
}
if (changed) {
clean_component(copy, 1);
icalcomponent_add_component(sched_data->itip, copy);
sched_data->comp_mask |= (1 << ncomp);
}
else icalcomponent_free(copy);
ncomp++;
} while ((comp = icalcomponent_get_next_component(newical, kind)));
}
/* Decline any components that have been left behind in the old obj */
hash_enumerate(&comp_table, sched_decline, sched_data->itip);
free_hash_table(&comp_table, free_comp_data);
done:
if (sched_data->itip &&
icalcomponent_get_first_real_component(sched_data->itip)) {
/* We built a reply object */
if (!sched_data->status) {
/* Attempt to deliver reply to organizer */
authstate = auth_newstate(userid);
sched_deliver(organizer, sched_data, authstate);
auth_freestate(authstate);
}
if (newical) {
unsigned ncomp = 0;
/* Set SCHEDULE-STATUS for organizer in attendee object */
comp = icalcomponent_get_first_real_component(newical);
do {
if (sched_data->comp_mask & (1 << ncomp)) {
prop =
icalcomponent_get_first_property(comp,
ICAL_ORGANIZER_PROPERTY);
param =
icalparameter_new_schedulestatus(sched_data->status);
icalproperty_add_parameter(prop, param);
}
ncomp++;
} while ((comp = icalcomponent_get_next_component(newical, kind)));
}
}
/* Cleanup */
free_sched_data(sched_data);
}

File Metadata

Mime Type
text/x-c
Expires
Sat, Apr 4, 2:32 AM (1 w, 2 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18749154
Default Alt Text
http_caldav.c (177 KB)

Event Timeline