Page MenuHomePhorge

http_caldav_sched.c
No OneTemporary

Authored By
Unknown
Size
114 KB
Referenced Files
None
Subscribers
None

http_caldav_sched.c

/* http_caldav_sched.c -- Routines for dealing with CALDAV scheduling in httpd
*
* Copyright (c) 1994-2015 Carnegie Mellon University. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The name "Carnegie Mellon University" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For permission or any legal
* details, please contact
* Carnegie Mellon University
* Center for Technology Transfer and Enterprise Creation
* 4615 Forbes Avenue
* Suite 302
* Pittsburgh, PA 15213
* (412) 268-7393, fax: (412) 268-7395
* innovation@andrew.cmu.edu
*
* 4. Redistributions of any form whatsoever must retain the following
* acknowledgment:
* "This product includes software developed by Computing Services
* at Carnegie Mellon University (http://www.cmu.edu/computing/)."
*
* CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
* THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
* FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#include <config.h>
#include <syslog.h>
#include <jansson.h>
#include <libical/ical.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <libxml/HTMLparser.h>
#include <libxml/tree.h>
#include "httpd.h"
#include "http_caldav_sched.h"
#include "http_dav.h"
#include "http_proxy.h"
#include "jmap_ical.h"
#include "jmap_util.h"
#include "notify.h"
#include "crc32.h"
#include "smtpclient.h"
#include "strhash.h"
#include "times.h"
#include "xmalloc.h"
#include "xstrlcat.h"
#include "xstrlcpy.h"
/* generated headers are not necessarily in current directory */
#include "imap/http_err.h"
#include "imap/imap_err.h"
static const char *get_organizer(icalcomponent *comp);
static int partstat_changed(icalcomponent *oldcomp,
icalcomponent *newcomp, const char *attendee);
int caladdress_lookup(const char *addr, struct caldav_sched_param *param,
const strarray_t *schedule_addresses)
{
const char *userid = addr;
int i;
if (!addr) return HTTP_NOT_FOUND;
if (!strncasecmp(userid, "mailto:", 7)) userid += 7;
char *addresses = schedule_addresses ? strarray_join(schedule_addresses, ",") : xstrdup("NULL");
syslog(LOG_DEBUG,
"caladdress_lookup(userid: '%s', schedule_addresses: '%s')",
userid, addresses);
free(addresses);
memset(param, 0, sizeof(struct caldav_sched_param));
param->userid = xstrdup(userid);
if (schedule_addresses) {
for (i = 0; i < strarray_size(schedule_addresses); i++) {
const char *item = strarray_nth(schedule_addresses, i);
if (!strncasecmp(item, "mailto:", 7)) item += 7;
if (strcasecmp(item, userid)) continue;
// found one!
param->isyou = 1;
return 0; // myself is always local
}
}
// does this user have an inbox on this machine?
/* XXX Do LDAP/DB/socket lookup to see if user is local */
/* XXX Hack until real lookup stuff is written */
int islocal = 0;
const char *at = strchr(userid, '@');
if (at) {
struct strlist *domains = cua_domains;
for (; domains && strcmp(at+1, domains->s); domains = domains->next);
if (domains) islocal = 1;
}
if (islocal) {
mbentry_t *mbentry = NULL;
/* Lookup user's cal-home-set to see if its on this server */
mbname_t *mbname = mbname_from_userid(userid);
mbname_push_boxes(mbname, config_getstring(IMAPOPT_CALENDARPREFIX));
int r = http_mlookup(mbname_intname(mbname), &mbentry, NULL);
mbname_free(&mbname);
if (!r) {
param->server = xstrdupnull(mbentry->server); /* freed by sched_param_fini */
mboxlist_entry_free(&mbentry);
if (param->server) param->flags |= SCHEDTYPE_ISCHEDULE;
return 0;
}
}
/* User is outside of our domain(s) -
Do remote scheduling (default = iMIP) */
param->flags |= SCHEDTYPE_REMOTE;
/* Do iSchedule DNS SRV lookup */
/* XXX If success, set server, port,
and flags |= SCHEDTYPE_ISCHEDULE [ | SCHEDTYPE_SSL ] */
#ifdef IOPTEST /* CalConnect ioptest */
if (!strcmp(p, "example.com")) {
param->server = xstrdup("ischedule.example.com");
param->port = 8008;
param->flags |= SCHEDTYPE_ISCHEDULE;
}
else if (!strcmp(p, "mysite.edu")) {
param->server = xstrdup("ischedule.mysite.edu");
param->port = 8080;
param->flags |= SCHEDTYPE_ISCHEDULE;
}
else if (!strcmp(p, "bedework.org")) {
param->server = xstrdup("www.bedework.org");
param->port = 80;
param->flags |= SCHEDTYPE_ISCHEDULE;
}
#endif /* IOPTEST */
return 0;
}
struct address_t {
const char *addr;
const char *name;
char *qpname;
const char *role;
const char *partstat;
struct address_t *next;
};
static void add_address(struct address_t **recipients, icalproperty *prop,
const char* (*icalproperty_get_address)(icalproperty *))
{
struct address_t *new = xzmalloc(sizeof(struct address_t));
icalparameter *param;
const char *address = icalproperty_get_address(prop);
if (!address) return;
if (!strncasecmp(address, "mailto:", 7))
address += 7;
new->addr = address;
param = icalproperty_get_first_parameter(prop, ICAL_CN_PARAMETER);
if (param) {
new->name = icalparameter_get_cn(param);
new->qpname = charset_encode_mimeheader(new->name, 0, 0);
}
param = icalproperty_get_first_parameter(prop, ICAL_ROLE_PARAMETER);
if (param)
new->role = icalparameter_enum_to_string(icalparameter_get_role(param));
param = icalproperty_get_first_parameter(prop, ICAL_PARTSTAT_PARAMETER);
if (param)
new->partstat =
icalparameter_enum_to_string(icalparameter_get_partstat(param));
new->next = *recipients;
*recipients = new;
}
static void HTMLencode(struct buf *output, const char *input)
{
int inlen = strlen(input);
int outlen = 8*inlen; /* room for every char to become a named entity */
buf_ensure(output, outlen+1);
htmlEncodeEntities((unsigned char *) buf_base(output), &outlen,
(unsigned char *) input, &inlen, 0);
buf_truncate(output, outlen);
buf_replace_all(output, "\n", "\n <br>");
}
#define TEXT_INDENT " "
#define HTML_ROW "<tr><td><b>%s</b></td><td>%s</td></tr>\r\n"
/* Send an iMIP request for attendees in 'ical' */
static int imip_send_sendmail(icalcomponent *ical, const char *sender,
const char *recipient, int is_update)
{
int r;
icalcomponent *comp;
icalproperty *prop;
icalproperty_method meth;
icalcomponent_kind kind;
const char *uid, *summary, *location, *descrip, *status;
const char *msg_type, *filename;
struct address_t *recipients = NULL, *originator = NULL, *recip;
struct icaltimetype start, end;
char *cp, when[2*RFC5322_DATETIME_MAX+4], datestr[RFC5322_DATETIME_MAX+1];
char boundary[100], *mimebody, *ical_str;
size_t outlen;
struct buf plainbuf = BUF_INITIALIZER, tmpbuf = BUF_INITIALIZER, msgbuf = BUF_INITIALIZER;
pid_t p = getpid();
time_t t = time(NULL);
static unsigned send_count = 0;
meth = icalcomponent_get_method(ical);
comp = icalcomponent_get_first_real_component(ical);
kind = icalcomponent_isa(comp);
uid = icalcomponent_get_uid(comp);
/* Determine Originator and Recipient(s) based on method and component */
if (meth == ICAL_METHOD_REPLY) {
msg_type = "a RSVP";
filename = "RSVP";
prop = icalcomponent_get_first_invitee(comp);
add_address(&originator, prop, &icalproperty_get_invitee);
prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
add_address(&recipients, prop,
(const char*(*)(icalproperty *))&icalproperty_get_organizer);
}
else {
if (meth == ICAL_METHOD_CANCEL) {
msg_type = "a cancellation";
filename = "Canceled";
}
else if (is_update) {
msg_type = "an updated invitation";
filename = "Update";
}
else {
msg_type = "an invitation";
filename = "Invitation";
}
prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
add_address(&originator, prop,
(const char*(*)(icalproperty *))&icalproperty_get_organizer);
for (prop = icalcomponent_get_first_invitee(comp);
prop;
prop = icalcomponent_get_next_invitee(comp)) {
add_address(&recipients, prop, &icalproperty_get_invitee);
}
}
/* Get other useful properties/values */
summary = icalcomponent_get_summary(comp);
location = icalcomponent_get_location(comp);
descrip = icalcomponent_get_description(comp);
if ((prop = icalcomponent_get_first_property(comp, ICAL_STATUS_PROPERTY))) {
status = icalproperty_get_value_as_string(prop);
}
else status = NULL;
start = icaltime_convert_to_zone(icalcomponent_get_dtstart(comp), utc_zone);
end = icaltime_convert_to_zone(icalcomponent_get_dtend(comp), utc_zone);
cp = when;
cp += sprintf(cp, "%s, %02u %s %04u",
wday[icaltime_day_of_week(start)-1],
start.day, monthname[start.month-1], start.year);
if (!icaltime_is_date(start)) {
cp += sprintf(cp, " %02u:%02u", start.hour, start.minute);
if (start.second) cp += sprintf(cp, ":%02u", start.second);
strcpy(cp, " UTC");
}
else icaltime_adjust(&end, -1, 0, 0, 0);
if (icaltime_compare(end, start)) {
strcpy(cp, " -");
cp += 2;
if (icaltime_compare_date_only(end, start)) {
cp += sprintf(cp, " %s, %02u %s %04u",
wday[icaltime_day_of_week(end)-1],
end.day, monthname[end.month-1], end.year);
}
if (!icaltime_is_date(end)) {
cp += sprintf(cp, " %02u:%02u", end.hour, end.minute);
if (end.second) cp += sprintf(cp, ":%02u", end.second);
strcpy(cp, " UTC");
}
}
/* Create multipart/mixed + multipart/alternative iMIP message */
buf_printf(&msgbuf, "From: %s <%s>\r\n",
originator->qpname ? originator->qpname : "", sender);
for (recip = recipients; recip; recip = recip->next) {
if (strcmp(recip->addr, sender) &&
(!recipient || !strcasecmp(recip->addr, recipient))) {
buf_printf(&msgbuf, "To: %s <%s>\r\n",
recip->qpname ? recip->qpname : "", recip->addr);
}
}
buf_printf(&msgbuf, "Subject: %s: ", filename);
if (summary) {
char *mimehdr = charset_encode_mimeheader(summary, 0, 0);
buf_appendcstr(&msgbuf, mimehdr);
free(mimehdr);
}
else {
buf_appendcstr(&msgbuf, icalcomponent_kind_to_string(kind));
}
buf_appendcstr(&msgbuf, "\r\n");
time_to_rfc5322(t, datestr, sizeof(datestr));
buf_printf(&msgbuf, "Date: %s\r\n", datestr);
buf_printf(&msgbuf, "Message-ID: <cyrus-caldav-%u-%ld-%u@%s>\r\n",
p, t, send_count++, config_servername);
/* Create multipart boundary */
snprintf(boundary, sizeof(boundary), "%s=_%ld=_%ld=_%ld",
config_servername, (long) p, (long) t, (long) rand());
buf_printf(&msgbuf, "Content-Type: multipart/mixed;"
"\r\n\tboundary=\"%s_M\"\r\n", boundary);
buf_printf(&msgbuf, "iMIP-Content-ID: <%s@%s>\r\n", uid, config_servername);
buf_appendcstr(&msgbuf, "Auto-Submitted: auto-generated\r\n");
buf_appendcstr(&msgbuf, "MIME-Version: 1.0\r\n");
buf_appendcstr(&msgbuf, "\r\n");
/* preamble */
buf_appendcstr(&msgbuf, "This is a message with multiple parts in MIME format.\r\n");
/* multipart/alternative */
buf_printf(&msgbuf, "\r\n--%s_M\r\n", boundary);
buf_printf(&msgbuf, "Content-Type: multipart/alternative;"
"\r\n\tboundary=\"%s_A\"\r\n", boundary);
/* plain text part */
buf_printf(&msgbuf, "\r\n--%s_A\r\n", boundary);
buf_appendcstr(&msgbuf, "Content-Type: text/plain; charset=utf-8\r\n");
buf_appendcstr(&msgbuf, "Content-Disposition: inline\r\n");
buf_printf(&plainbuf, "You have received %s from %s <%s>\r\n\r\n", msg_type,
originator->name ? originator->name : "", originator->addr);
if (summary) {
buf_setcstr(&tmpbuf, summary);
buf_replace_all(&tmpbuf, "\n", "\r\n" TEXT_INDENT);
buf_printf(&plainbuf, "Summary : %s\r\n", buf_cstring(&tmpbuf));
}
if (location) {
buf_setcstr(&tmpbuf, location);
buf_replace_all(&tmpbuf, "\n", "\r\n" TEXT_INDENT);
buf_printf(&plainbuf, "Location : %s\r\n", buf_cstring(&tmpbuf));
}
buf_printf(&plainbuf, "When : %s\r\n", when);
if (meth == ICAL_METHOD_REPLY) {
if (originator->partstat)
buf_printf(&plainbuf, "RSVP : %s\r\n", originator->partstat);
}
else {
if (status) buf_printf(&plainbuf, "Status : %s\r\n", status);
for (cp = "Attendees : ", recip=recipients; recip; recip=recip->next) {
buf_printf(&plainbuf, "%s* %s <%s>",
cp, recip->name ? recip->name : "", recip->addr);
if (recip->role) buf_printf(&plainbuf, "\t(%s)", recip->role);
buf_appendcstr(&plainbuf, "\r\n");
cp = TEXT_INDENT;
}
if (descrip) {
buf_setcstr(&tmpbuf, descrip);
buf_replace_all(&tmpbuf, "\n", "\r\n" TEXT_INDENT);
buf_printf(&plainbuf, "Description: %s\r\n", buf_cstring(&tmpbuf));
}
}
mimebody = charset_qpencode_mimebody(buf_base(&plainbuf),
buf_len(&plainbuf), 0, &outlen);
if (outlen > buf_len(&plainbuf)) {
buf_appendcstr(&msgbuf, "Content-Transfer-Encoding: quoted-printable\r\n");
}
buf_appendcstr(&msgbuf, "\r\n");
buf_appendmap(&msgbuf, mimebody, outlen);
free(mimebody);
buf_free(&plainbuf);
/* HTML part */
buf_printf(&msgbuf, "\r\n--%s_A\r\n", boundary);
buf_printf(&msgbuf, "Content-Type: text/html; charset=utf-8\r\n");
buf_appendcstr(&msgbuf, "Content-Disposition: inline\r\n");
buf_appendcstr(&msgbuf, "\r\n");
buf_appendcstr(&msgbuf, HTML_DOCTYPE "\r\n<html><head><title></title></head><body>\r\n");
if (originator->name) {
HTMLencode(&tmpbuf, originator->name);
originator->name = buf_cstring(&tmpbuf);
}
else originator->name = originator->addr;
buf_printf(&msgbuf, "<b>You have received %s from"
" <a href=\"mailto:%s\">%s</a></b><p>\r\n",
msg_type, originator->addr, originator->name);
buf_appendcstr(&msgbuf, "<table border cellpadding=5>\r\n");
if (summary) {
HTMLencode(&tmpbuf, summary);
buf_printf(&msgbuf, HTML_ROW, "Summary", buf_cstring(&tmpbuf));
}
if (location) {
HTMLencode(&tmpbuf, location);
buf_printf(&msgbuf, HTML_ROW, "Location", buf_cstring(&tmpbuf));
}
buf_printf(&msgbuf, HTML_ROW, "When", when);
if (meth == ICAL_METHOD_REPLY) {
if (originator->partstat)
buf_printf(&msgbuf, HTML_ROW, "RSVP", originator->partstat);
}
else {
if (status) buf_printf(&msgbuf, HTML_ROW, "Status", status);
buf_appendcstr(&msgbuf, "<tr><td><b>Attendees</b></td>");
for (cp = "<td>", recip = recipients; recip; recip = recip->next) {
if (recip->name) {
HTMLencode(&tmpbuf, recip->name);
recip->name = buf_cstring(&tmpbuf);
}
else recip->name = recip->addr;
buf_printf(&msgbuf, "%s&#8226; <a href=\"mailto:%s\">%s</a>",
cp, recip->addr, recip->name);
if (recip->role) buf_printf(&msgbuf, " <i>(%s)</i>", recip->role);
cp = "\n <br>";
}
buf_appendcstr(&msgbuf, "</td></tr>\r\n");
if (descrip) {
HTMLencode(&tmpbuf, descrip);
buf_printf(&msgbuf, HTML_ROW, "Description", buf_cstring(&tmpbuf));
}
}
buf_printf(&msgbuf, "</table></body></html>\r\n");
/* iCalendar part */
buf_printf(&msgbuf, "\r\n--%s_A\r\n", boundary);
buf_printf(&msgbuf, "Content-Type: text/calendar; charset=utf-8");
buf_printf(&msgbuf, "; method=%s; component=%s \r\n",
icalproperty_method_to_string(meth),
icalcomponent_kind_to_string(kind));
buf_printf(&msgbuf, "Content-ID: <%s@%s>\r\n", uid, config_servername);
ical_str = icalcomponent_as_ical_string(ical);
mimebody = charset_qpencode_mimebody(ical_str, strlen(ical_str), 0, &outlen);
if (outlen > strlen(ical_str)) {
buf_appendcstr(&msgbuf, "Content-Transfer-Encoding: quoted-printable\r\n");
}
buf_appendcstr(&msgbuf, "\r\n");
buf_appendmap(&msgbuf, mimebody, outlen);
free(mimebody);
/* end boundary (alternative) */
buf_printf(&msgbuf, "\r\n--%s_A--\r\n", boundary);
/* application/ics part */
buf_printf(&msgbuf, "\r\n--%s_M\r\n", boundary);
buf_printf(&msgbuf,
"Content-Type: application/ics; charset=utf-8; name=\"%s.ics\"\r\n",
filename);
buf_printf(&msgbuf, "Content-Disposition: attachment; filename=\"%s.ics\"\r\n",
filename);
buf_appendcstr(&msgbuf, "Content-Transfer-Encoding: base64\r\n");
buf_appendcstr(&msgbuf, "\r\n");
charset_encode_mimebody(NULL, strlen(ical_str), NULL, &outlen,
NULL, 1 /* wrap */);
buf_ensure(&tmpbuf, outlen);
charset_encode_mimebody(ical_str, strlen(ical_str),
(char *) buf_base(&tmpbuf), &outlen,
NULL, 1 /* wrap */);
buf_appendmap(&msgbuf, buf_base(&tmpbuf), outlen);
/* end boundary (mixed) and epilogue */
buf_printf(&msgbuf, "\r\n--%s_M--\r\n\r\nEnd of MIME multipart body.\r\n", boundary);
/* Open SMTP connection */
smtpclient_t *sm = NULL;
r = smtpclient_open(&sm);
if (r) {
syslog(LOG_ERR,
"imip_send_sendmail(%s): failed to open SMTP client", recipient);
r = HTTP_UNAVAILABLE;
goto done;
}
smtpclient_set_auth(sm, httpd_userid);
smtpclient_set_notify(sm, "FAILURE,DELAY");
/* Set SMTP envelope */
smtp_envelope_t sm_env = SMTP_ENVELOPE_INITIALIZER;
smtp_envelope_set_from(&sm_env, originator->addr);
for (recip = recipients; recip; recip = recip->next) {
if (strcmp(recip->addr, originator->addr) &&
(!recipient || !strcasecmp(recip->addr, recipient))) {
smtp_envelope_add_rcpt(&sm_env, recip->addr);
}
}
/* Send message */
r = smtpclient_send(sm, &sm_env, &msgbuf);
syslog(LOG_INFO,
"imip_send_sendmail(%s): %s", recipient, error_message(r));
smtp_envelope_fini(&sm_env);
int r2 = smtpclient_close(&sm);
if (!r) r = r2;
done:
buf_free(&msgbuf);
buf_free(&tmpbuf);
free(originator->qpname);
free(originator);
do {
struct address_t *freeme = recipients;
recipients = recipients->next;
free(freeme->qpname);
free(freeme);
} while (recipients);
return r;
}
/* Send an iMIP request for attendees in 'ical' */
static int imip_send(struct sched_data *sched_data,
const char *sender, const char *recipient)
{
const char *notifier = config_getstring(IMAPOPT_IMIPNOTIFIER);
syslog(LOG_DEBUG, "imip_send(%s)", recipient);
/* if no notifier, fall back to sendmail */
if (!notifier) {
return imip_send_sendmail(sched_data->itip,
sender, recipient, sched_data->is_update);
}
const char *ical_str = icalcomponent_as_ical_string(sched_data->itip);
json_t *jsevent, *patch;
#ifdef WITH_JMAP
if (sched_data->oldical) {
jsevent = jmapical_tojmap(sched_data->oldical, NULL);
if (sched_data->newical) {
/* Updated event */
json_t *new_jsevent = jmapical_tojmap(sched_data->newical, NULL);
patch = jmap_patchobject_create(jsevent, new_jsevent);
json_decref(new_jsevent);
}
else {
/* Canceled event */
patch = json_null();
}
}
else {
/* New event */
jsevent = json_null();
patch = jmapical_tojmap(sched_data->newical, NULL);
}
#else
jsevent = json_null();
patch = json_null();
#endif
json_t *val = json_pack("{s:s s:s s:s s:o s:o s:b}",
"recipient", recipient,
"sender", sender,
"ical", ical_str,
"jsevent", jsevent,
"patch", patch,
"is_update", sched_data->is_update);
char *serial = json_dumps(val, JSON_COMPACT);
// XXX: should we be replacing httpd_userid with sender?
notify(notifier, "IMIP", NULL, httpd_userid, NULL, 0, NULL, serial, NULL);
free(serial);
json_decref(val);
return 0;
}
/* 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) xml_add_href(recip, dav_ns, (const char *) 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 caldav_sched_param *remote = (struct caldav_sched_param *) data;
struct remote_rock *rrock = (struct remote_rock *) rock;
icalcomponent *comp;
struct proplist *list;
xmlNodePtr resp;
const char *status = NULL;
int r;
syslog(LOG_DEBUG, "busytime_query_remote(server: '%s', flags: 0x%x)",
server, remote->flags);
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 -
don't bother sending, its not very useful and not well supported */
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 necessary)
* 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 sched_param_cleanup(void *data)
{
struct caldav_sched_param *sparam = (struct caldav_sched_param *) data;
if (sparam) {
sched_param_fini(sparam);
free(sparam);
}
}
/* 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;
icalproperty *prop = NULL, *next;
const char *uid = NULL, *organizer = NULL;
struct caldav_sched_param sparam;
struct auth_state *org_authstate = NULL;
xmlNodePtr root = NULL;
xmlNsPtr ns[NUM_NAMESPACE];
struct propfind_ctx fctx;
struct freebusy_filter calfilter;
struct hash_table remote_table;
struct caldav_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, NULL) ||
(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;
}
/* Need DAV for hrefs */
ensure_ns(ns, NS_DAV, root, XML_NS_DAV, "D");
/* Populate our filter and propfind context for local attendees */
memset(&calfilter, 0, sizeof(struct freebusy_filter));
calfilter.start = icalcomponent_get_dtstart(comp);
calfilter.end = icalcomponent_get_dtend(comp);
calfilter.flags = CHECK_CAL_TRANSP | CHECK_USER_AVAIL;
memset(&fctx, 0, sizeof(struct propfind_ctx));
fctx.txn = txn;
fctx.req_tgt = &txn->req_tgt;
fctx.depth = 2;
fctx.userid = httpd_userid;
fctx.userisadmin = httpd_userisadmin;
fctx.authstate = org_authstate;
fctx.reqd_privs = 0; /* handled by CALDAV:schedule-deliver on Inbox */
fctx.filter_crit = &calfilter;
fctx.ret = &ret;
/* Create hash table for any remote attendee servers */
construct_hash_table(&remote_table, 10, 1);
/* 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, NULL);
/* 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 caldav_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;
icalcomponent *busy = NULL;
mbentry_t *mbentry = NULL;
const char *status = REQSTAT_NOUSER;
resp =
xml_add_schedresponse(root,
!(txn->req_tgt.allow & ALLOW_ISCHEDULE) ?
ns[NS_DAV] : NULL,
BAD_CAST attendee, NULL);
/* Check ACL of ORGANIZER on attendee's Scheduling Inbox */
char *inboxname = caldav_mboxname(userid, SCHED_INBOX);
r = mboxlist_lookup(inboxname, &mbentry, NULL);
if (r) {
syslog(LOG_INFO, "mboxlist_lookup(%s) failed: %s",
inboxname, error_message(r));
status = REQSTAT_REJECTED;
}
else if (!(httpd_myrights(org_authstate, mbentry) & DACL_SCHEDFB)) {
status = REQSTAT_NOPRIVS;
}
else {
/* Start query at attendee's calendar-home-set */
char *mboxname = caldav_mboxname(userid, NULL);
fctx.davdb = NULL;
fctx.req_tgt->collection = NULL;
calfilter.freebusy.len = 0;
busy = busytime_query_local(txn, &fctx, mboxname,
ICAL_METHOD_REPLY, uid,
organizer, attendee);
free(mboxname);
}
mboxlist_entry_free(&mbentry);
free(inboxname);
if (busy) {
xmlNodePtr cdata;
struct buf *fb_str = mime->from_object(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 buf_base(fb_str),
buf_len(fb_str)));
buf_destroy(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 status);
}
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, sched_param_cleanup);
/* Output the XML response */
if (!ret) xml_response(HTTP_OK, txn, root->doc);
done:
if (org_authstate) auth_freestate(org_authstate);
if (calfilter.freebusy.fb) free(calfilter.freebusy.fb);
if (root) xmlFreeDoc(root->doc);
return ret;
}
#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 *sender, const char *recipient,
struct caldav_sched_param *sparam,
struct sched_data *sched_data)
{
int r;
syslog(LOG_DEBUG, "sched_deliver_remote(%s, %X)", recipient, sparam->flags);
icalcomponent_add_required_timezones(sched_data->itip);
if (sparam->flags & SCHEDTYPE_ISCHEDULE) {
/* 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);
}
}
}
else {
r = imip_send(sched_data, sender, recipient);
if (!r) {
sched_data->status =
sched_data->ischedule ? REQSTAT_SENT : SCHEDSTAT_SENT;
}
else {
sched_data->status = sched_data->ischedule ?
REQSTAT_TEMPFAIL : SCHEDSTAT_TEMPFAIL;
}
}
}
/*
* deliver_merge_reply() helper function
*
* Merge VOTER responses into VPOLL subcomponents
*/
static void deliver_merge_vpoll_reply(icalcomponent *ical, icalcomponent *reply)
{
icalcomponent *new_ballot, *vvoter;
icalproperty *voterp;
const char *voter;
/* Get VOTER from reply */
new_ballot =
icalcomponent_get_first_component(reply, ICAL_VVOTER_COMPONENT);
voterp = icalcomponent_get_first_property(new_ballot, ICAL_VOTER_PROPERTY);
voter = icalproperty_get_voter(voterp);
/* Locate VOTER in existing VPOLL */
for (vvoter =
icalcomponent_get_first_component(ical, ICAL_VVOTER_COMPONENT);
vvoter;
vvoter =
icalcomponent_get_next_component(ical, ICAL_VVOTER_COMPONENT)) {
voterp =
icalcomponent_get_first_property(vvoter, ICAL_VOTER_PROPERTY);
if (!strcmp(voter, icalproperty_get_voter(voterp))) {
icalcomponent_remove_component(ical, vvoter);
icalcomponent_free(vvoter);
break;
}
}
/* XXX Actually need to compare POLL-ITEM-IDs */
icalcomponent_add_component(ical, icalcomponent_clone(new_ballot));
}
/* sched_reply() helper function
*
* Add voter responses to VPOLL reply and remove candidate components
*
*/
static void sched_vpoll_reply(icalcomponent *poll)
{
icalcomponent *item, *next;
for (item = icalcomponent_get_first_component(poll, ICAL_ANY_COMPONENT);
item;
item = next) {
next = icalcomponent_get_next_component(poll, ICAL_ANY_COMPONENT);
switch (icalcomponent_isa(item)) {
case ICAL_VVOTER_COMPONENT:
/* Our ballot, leave it */
/* XXX Need to compare against previous votes */
break;
default:
/* Candidate component, remove it */
icalcomponent_remove_component(poll, item);
icalcomponent_free(item);
break;
}
}
}
static int deliver_merge_pollstatus(icalcomponent *ical, icalcomponent *request)
{
int deliver_inbox = 0;
icalcomponent *oldpoll, *newpoll, *vvoter, *next;
/* Remove each VVOTER from old object */
oldpoll =
icalcomponent_get_first_component(ical, ICAL_VPOLL_COMPONENT);
for (vvoter =
icalcomponent_get_first_component(oldpoll, ICAL_VVOTER_COMPONENT);
vvoter;
vvoter = next) {
next = icalcomponent_get_next_component(oldpoll, ICAL_VVOTER_COMPONENT);
icalcomponent_remove_component(oldpoll, vvoter);
icalcomponent_free(vvoter);
}
/* Add each VVOTER in the iTIP request to old object */
newpoll = icalcomponent_get_first_component(request, ICAL_VPOLL_COMPONENT);
for (vvoter =
icalcomponent_get_first_component(newpoll, ICAL_VVOTER_COMPONENT);
vvoter;
vvoter =
icalcomponent_get_next_component(newpoll, ICAL_VVOTER_COMPONENT)) {
icalcomponent_add_component(oldpoll, icalcomponent_clone(vvoter));
}
return deliver_inbox;
}
static void sched_pollstatus(const char *organizer,
struct caldav_sched_param *sparam, icalcomponent *ical,
const char *voter)
{
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_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, *next;
struct strlist *voters = NULL;
/* Make a working copy of the iTIP */
stat = icalcomponent_clone(itip);
/* Make a working copy of the VPOLL and add to pollstatus */
poll = icalcomponent_clone(comp);
icalcomponent_add_component(stat, poll);
/* Process each sub-component of VPOLL */
for (sub = icalcomponent_get_first_component(poll, ICAL_ANY_COMPONENT);
sub; sub = next) {
next = icalcomponent_get_next_component(poll, ICAL_ANY_COMPONENT);
switch (icalcomponent_isa(sub)) {
case ICAL_VVOTER_COMPONENT: {
/* Make list of VOTERs (stripping SCHEDULE-STATUS) */
const char *this_voter;
prop =
icalcomponent_get_first_property(sub, ICAL_VOTER_PROPERTY);
this_voter = icalproperty_get_voter(prop);
/* Don't update organizer or voter that triggered POLLSTATUS */
if (strcmp(this_voter, organizer) && strcmp(this_voter, voter))
appendstrlist(&voters, (char *) this_voter);
icalproperty_remove_parameter_by_name(prop, "SCHEDULE-STATUS");
break;
}
default:
/* Remove candidate components */
icalcomponent_remove_component(poll, sub);
icalcomponent_free(sub);
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(organizer, voters->s, &sched_data, authstate);
free(voters->s);
free(voters);
voters = next;
}
icalcomponent_free(stat);
}
icalcomponent_free(itip);
auth_freestate(authstate);
}
/* annoying copypaste from libical because it's not exposed */
static struct icaltimetype _get_datetime(icalcomponent *comp, icalproperty *prop)
{
icalcomponent *c;
icalparameter *param;
struct icaltimetype ret;
ret = icalvalue_get_datetime(icalproperty_get_value(prop));
if ((param = icalproperty_get_first_parameter(prop, ICAL_TZID_PARAMETER)) != NULL) {
const char *tzid = icalparameter_get_tzid(param);
icaltimezone *tz = NULL;
for (c = comp; c != NULL; c = icalcomponent_get_parent(c)) {
tz = icalcomponent_get_timezone(c, tzid);
if (tz != NULL)
break;
}
if (tz == NULL)
tz = icaltimezone_get_builtin_timezone_from_tzid(tzid);
if (tz != NULL)
ret = icaltime_set_timezone(&ret, tz);
}
return ret;
}
static icalcomponent *master_to_recurrence(icalcomponent *master, icalproperty *recurid)
{
icalproperty *prop, *next;
icalproperty *endprop = NULL;
icalproperty *startprop = NULL;
icalcomponent *comp = icalcomponent_clone(master);
for (prop = icalcomponent_get_first_property(comp, ICAL_ANY_PROPERTY);
prop; prop = next) {
next = icalcomponent_get_next_property(comp, ICAL_ANY_PROPERTY);
switch (icalproperty_isa(prop)) {
/* extract start and end for later processing */
case ICAL_DTEND_PROPERTY:
endprop = prop;
break;
case ICAL_DTSTART_PROPERTY:
startprop = prop;
break;
/* Remove all recurrence properties */
case ICAL_RRULE_PROPERTY:
case ICAL_RDATE_PROPERTY:
case ICAL_EXDATE_PROPERTY:
icalcomponent_remove_property(comp, prop);
icalproperty_free(prop);
break;
default:
break;
}
}
/* Add RECURRENCE-ID */
icalcomponent_add_property(comp, icalproperty_clone(recurid));
/* calculate a new dtend based on recurid */
struct icaltimetype start = _get_datetime(master, startprop);
struct icaltimetype newstart = _get_datetime(master, recurid);
icaltimezone *startzone = (icaltimezone *)icaltime_get_timezone(start);
icalcomponent_set_dtstart(comp, icaltime_convert_to_zone(newstart, startzone));
if (endprop) {
struct icaltimetype end = _get_datetime(master, endprop);
// calculate and re-apply the diff
struct icaldurationtype diff = icaltime_subtract(end, start);
struct icaltimetype newend = icaltime_add(newstart, diff);
icaltimezone *endzone = (icaltimezone *)icaltime_get_timezone(end);
icalcomponent_set_dtend(comp, icaltime_convert_to_zone(newend, endzone));
}
/* otherwise it will be a duration, which is still valid! */
return comp;
}
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;
/* 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)));
/* 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 = master_to_recurrence(hash_lookup("", &comp_table), 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_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_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_clone(prop));
icalcomponent_add_component(ical, comp);
}
/* Get the sending attendee */
att = icalcomponent_get_first_invitee(itip);
attendee = icalproperty_get_invitee(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_invitee(comp);
prop && strcmp(attendee, icalproperty_get_invitee(prop));
prop = icalcomponent_get_next_invitee(comp));
if (!prop) {
/* Attendee added themselves to this recurrence */
assert(icalproperty_isa(prop) != ICAL_VOTER_PROPERTY);
prop = icalproperty_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, *organizer = NULL;
/* 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);
if (!tzid) continue;
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);
if (!tzid) continue;
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_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);
organizer = get_organizer(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_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;
else if (partstat_changed(comp, itip, organizer)) 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_clone(prop));
}
prop =
icalcomponent_get_first_property(comp,
ICAL_PERCENTCOMPLETE_PROPERTY);
if (prop) {
icalcomponent_add_property(new_comp,
icalproperty_clone(prop));
}
prop =
icalcomponent_get_first_property(comp, ICAL_TRANSP_PROPERTY);
if (prop) {
icalcomponent_add_property(new_comp,
icalproperty_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_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 *sender, const char *recipient,
struct caldav_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;
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;
syslog(LOG_DEBUG, "sched_deliver_local(%s, %s, %X)", sender, recipient, sparam->flags);
/* Start with an empty (clean) transaction */
memset(&txn, 0, sizeof(struct transaction_t));
/* 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 = httpd_myrights(authstate, mbentry);
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;
syslog(LOG_DEBUG, "No scheduling receive ACL for user %s on Inbox %s",
httpd_userid, userid);
goto done;
}
/* Open recipient's Inbox for writing */
if ((r = mailbox_open_iwl(mailboxname, &inbox))) {
syslog(LOG_ERR, "mailbox_open_iwl(%s) failed: %s",
mailboxname, error_message(r));
sched_data->status =
sched_data->ischedule ? REQSTAT_TEMPFAIL : SCHEDSTAT_TEMPFAIL;
goto done;
}
free(mailboxname);
/* 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);
if (!caldavdb) {
sched_data->status =
sched_data->ischedule ? REQSTAT_TEMPFAIL : SCHEDSTAT_TEMPFAIL;
goto done;
}
caldav_lookup_uid(caldavdb,
icalcomponent_get_uid(sched_data->itip), &cdata);
if (cdata->dav.mailbox) {
mailboxname = xstrdup(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);
/* XXX - sanitize the uid? */
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_clone(prop));
/* Copy over PRODID property */
prop = icalcomponent_get_first_property(sched_data->itip,
ICAL_PRODID_PROPERTY);
icalcomponent_add_property(ical, icalproperty_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_clone(prop));
}
}
/* Open recipient's calendar for writing */
r = mailbox_open_iwl(mailboxname, &mailbox);
if (r) {
syslog(LOG_ERR, "mailbox_open_iwl(%s) failed: %s",
mailboxname, error_message(r));
sched_data->status =
sched_data->ischedule ? REQSTAT_TEMPFAIL : SCHEDSTAT_TEMPFAIL;
goto done;
}
if (cdata->dav.imap_uid) {
/* Load message containing the resource and parse iCal data */
ical = caldav_record_to_ical(mailbox, cdata, NULL, NULL);
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;
case ICAL_VPOLL_COMPONENT:
if (cdata->comp_type != CAL_COMP_VPOLL) reject = 1;
break;
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 (organizer) {
if (!strncasecmp(organizer, "mailto:", 7)) organizer += 7;
if (strcasecmp(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;
}
/* Create header cache */
txn.req_hdrs = spool_new_hdrcache();
if (!txn.req_hdrs) r = HTTP_SERVER_ERROR;
/* Store the (updated) object in the recipients's calendar */
strarray_t recipient_addresses = STRARRAY_INITIALIZER;
strarray_append(&recipient_addresses, recipient);
if (!r) r = caldav_store_resource(&txn, ical, mailbox,
buf_cstring(&resource), cdata->dav.createdmodseq,
caldavdb, NEW_STAG, recipient, &recipient_addresses);
strarray_fini(&recipient_addresses);
if (r == HTTP_CREATED || r == HTTP_NO_CONTENT) {
sched_data->status =
sched_data->ischedule ? REQSTAT_SUCCESS : SCHEDSTAT_DELIVERED;
}
else {
syslog(LOG_ERR, "caldav_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, "%s.ics", makeuuid());
/* Store the message in the recipient's Inbox */
r = caldav_store_resource(&txn, sched_data->itip, inbox,
buf_cstring(&resource), 0, caldavdb, 0, NULL, NULL);
/* 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, attendee);
else
sched_request(userid, NULL, recipient, NULL, ical); // oldical?
}
done:
if (ical) icalcomponent_free(ical);
mailbox_close(&inbox);
mailbox_close(&mailbox);
if (caldavdb) caldav_close(caldavdb);
spool_free_hdrcache(txn.req_hdrs);
buf_free(&txn.buf);
free(mailboxname);
}
/* Deliver scheduling object to recipient's Inbox */
void sched_deliver(const char *sender, 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 caldav_sched_param sparam;
int islegal;
syslog(LOG_DEBUG, "sched_deliver(%s)", recipient);
memset(&sparam, 0, sizeof(struct caldav_sched_param));
/* 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->schedule_addresses)) {
sched_data->status =
sched_data->ischedule ? REQSTAT_NOUSER : SCHEDSTAT_NOUSER;
/* Unknown user */
goto done;
}
/* don't schedule to yourself */
if (sparam.isyou) goto done;
if (sparam.flags) {
/* Remote recipient */
syslog(LOG_NOTICE, "%s scheduling delivery to %s",
(sparam.flags & SCHEDTYPE_ISCHEDULE) ? "iSchedule" : "iMIP",
recipient);
sched_deliver_remote(sender, recipient, &sparam, sched_data);
}
else {
/* Local recipient */
syslog(LOG_NOTICE, "CalDAV scheduling delivery to %s", recipient);
sched_deliver_local(sender, recipient, &sparam, sched_data, authstate);
}
done:
sched_param_fini(&sparam);
}
/*
* sched_request/reply() helper function
*
* Update DTSTAMP, remove VALARMs, remove SCHEDULE-* parameters
*/
static void clean_component(icalcomponent *comp)
{
icalcomponent *alarm, *next;
icalproperty *prop;
/* Replace DTSTAMP on component */
prop = icalcomponent_get_first_property(comp, ICAL_DTSTAMP_PROPERTY);
if (!prop) {
prop = icalproperty_new(ICAL_DTSTAMP_PROPERTY);
icalcomponent_add_property(comp, prop);
}
icalproperty_set_dtstamp(prop, icaltime_current_time_with_zone(utc_zone));
/* 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);
}
/* 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");
/* Remove CalDAV Scheduling parameters from attendees */
for (prop = icalcomponent_get_first_invitee(comp);
prop;
prop = icalcomponent_get_next_invitee(comp)) {
icalproperty_remove_parameter_by_name(prop, "SCHEDULE-AGENT");
icalproperty_remove_parameter_by_name(prop, "SCHEDULE-STATUS");
icalproperty_remove_parameter_by_name(prop, "SCHEDULE-FORCE-SEND");
}
}
/*****************************************************************************/
/*
* 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 XORed CRC32 of all
* property strings for each component (order irrelevant) and compare the CRCs.
* 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_DURATION_PROPERTY) {
struct icaldurationtype olddur = icalproperty_get_duration(oldprop);
struct icaldurationtype newdur = icalproperty_get_duration(newprop);
return (icaldurationtype_as_int(olddur) != icaldurationtype_as_int(newdur));
}
else if ((kind == ICAL_RDATE_PROPERTY) || (kind == ICAL_EXDATE_PROPERTY)) {
const char *str;
uint32_t old_crc = 0, new_crc = 0;
do {
str = icalproperty_get_value_as_string(oldprop);
if (str) old_crc ^= crc32_cstring(str);
} while ((oldprop = icalcomponent_get_next_property(oldical, kind)));
do {
str = icalproperty_get_value_as_string(newprop);
if (str) new_crc ^= crc32_cstring(str);
} while ((newprop = icalcomponent_get_next_property(newical, kind)));
return (old_crc != new_crc);
}
else {
return (strcmpsafe(icalproperty_get_value_as_string(oldprop),
icalproperty_get_value_as_string(newprop)) != 0);
}
}
/*
* sched_request() helper function
*
* Process all attendees in the given component and add them
* to the request data
*/
static void add_attendees(icalcomponent *ical,
const char *organizer, strarray_t *attendees)
{
if (!ical) return;
icalcomponent *comp = icalcomponent_get_first_real_component(ical);
/* if no organizer, this isn't a scheduling resource, so nothing else to do */
if (!icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY))
return;
icalcomponent_kind kind = icalcomponent_isa(comp);
for (; comp; comp = icalcomponent_get_next_component(ical, kind)) {
icalproperty *prop;
icalparameter *param;
for (prop = icalcomponent_get_first_invitee(comp);
prop;
prop = icalcomponent_get_next_invitee(comp)) {
const char *attendee = icalproperty_get_invitee(prop);
if (!attendee) continue;
if (!strncasecmp(attendee, "mailto:", 7)) attendee += 7;
/* Skip where attendee == organizer */
if (!strcasecmp(attendee, organizer)) continue;
/* Skip where not the server's responsibility */
param = icalproperty_get_scheduleagent_parameter(prop);
if (param) {
icalparameter_scheduleagent agent =
icalparameter_get_scheduleagent(param);
if (agent != ICAL_SCHEDULEAGENT_SERVER) continue;
}
strarray_add_case(attendees, attendee);
}
}
}
static icalproperty *find_attendee(icalcomponent *comp, const char *match)
{
if (!comp) return NULL;
icalproperty *prop = icalcomponent_get_first_invitee(comp);
for (; prop; prop = icalcomponent_get_next_invitee(comp)) {
const char *attendee = icalproperty_get_invitee(prop);
if (!attendee) continue;
if (!strncasecmp(attendee, "mailto:", 7)) attendee += 7;
/* Skip where not the server's responsibility */
icalparameter *param = icalproperty_get_scheduleagent_parameter(prop);
if (param) {
icalparameter_scheduleagent agent =
icalparameter_get_scheduleagent(param);
if (agent != ICAL_SCHEDULEAGENT_SERVER) continue;
}
if (!strcasecmp(attendee, match)) return prop;
}
return NULL;
}
static icalcomponent *find_component(icalcomponent *ical, const char *match)
{
if (!ical) return NULL;
icalcomponent *comp = icalcomponent_get_first_real_component(ical);
icalcomponent_kind kind = icalcomponent_isa(comp);
for (; comp; comp = icalcomponent_get_next_component(ical, kind)) {
icalproperty *prop =
icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
const char *recurid = "";
if (prop) recurid = icalproperty_get_value_as_string(prop);
if (!strcmpsafe(recurid, match)) return comp;
}
return NULL;
}
static icalcomponent *find_attended_component(icalcomponent *ical,
const char *recurid,
const char *attendee)
{
icalcomponent *comp = find_component(ical, recurid);
if (find_attendee(comp, attendee))
return comp;
return NULL;
}
static int has_exdate(icalcomponent *ical, struct icaltimetype test)
{
if (!ical) return 0;
icalproperty *prop =
icalcomponent_get_first_property(ical, ICAL_EXDATE_PROPERTY);
for (; prop;
prop = icalcomponent_get_next_property(ical, ICAL_EXDATE_PROPERTY)) {
struct icaltimetype exdate = icalproperty_get_exdate(prop);
if (!icaltime_compare(exdate, test)) return 1;
}
return 0;
}
static int check_changes_any(icalcomponent *old,
icalcomponent *comp, int *needs_actionp)
{
if (!old) {
if (needs_actionp) *needs_actionp = 1;
return 1;
}
int is_changed = 0;
int needs_action = 0;
/* Per RFC 6638, Section 3.2.8: We need to compare
DTSTART, DTEND, DURATION, DUE, RRULE, RDATE, EXDATE */
if (propcmp(old, comp, ICAL_DTSTART_PROPERTY))
needs_action = 1;
else if (propcmp(old, comp, ICAL_DTEND_PROPERTY))
needs_action = 1;
else if (propcmp(old, comp, ICAL_DURATION_PROPERTY))
needs_action = 1;
else if (propcmp(old, comp, ICAL_DUE_PROPERTY))
needs_action = 1;
else if (propcmp(old, comp, ICAL_RRULE_PROPERTY))
needs_action = 1;
else if (propcmp(old, comp, ICAL_RDATE_PROPERTY))
needs_action = 1;
else if (propcmp(old, comp, ICAL_EXDATE_PROPERTY))
needs_action = 1;
if (needs_action)
is_changed = 1;
else if (propcmp(old, comp, ICAL_SUMMARY_PROPERTY))
is_changed = 1;
else if (propcmp(old, comp, ICAL_LOCATION_PROPERTY))
is_changed = 1;
else if (propcmp(old, comp, ICAL_DESCRIPTION_PROPERTY))
is_changed = 1;
else if (propcmp(old, comp, ICAL_POLLWINNER_PROPERTY))
is_changed = 1;
else if (partstat_changed(old, comp, get_organizer(comp)))
is_changed = 1;
if (needs_actionp) *needs_actionp = needs_action;
return is_changed;
}
static int check_changes(icalcomponent *old, icalcomponent *comp, const char *attendee)
{
int needs_action = 0;
int res = check_changes_any(old, comp, &needs_action);
if (needs_action) {
/* Make sure SEQUENCE is set properly */
int oldseq = icalcomponent_get_sequence(old);
int newseq = icalcomponent_get_sequence(comp);
if (oldseq >= newseq) icalcomponent_set_sequence(comp, oldseq + 1);
icalproperty *prop = find_attendee(comp, attendee);
if (prop) {
icalparameter *param =
icalparameter_new_partstat(ICAL_PARTSTAT_NEEDSACTION);
icalproperty_set_parameter(prop, param);
}
}
return res;
}
icalcomponent *make_itip(icalproperty_method method, icalcomponent *ical)
{
/* Create a shell for our iTIP request objects */
icalcomponent *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 */
icalproperty *prop =
icalcomponent_get_first_property(ical, ICAL_CALSCALE_PROPERTY);
if (prop) {
icalcomponent_add_property(req, icalproperty_clone(prop));
}
/* Copy over any VTIMEZONE components */
icalcomponent *comp;
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_clone(comp));
}
return req;
}
static void schedule_set_exdate(icalcomponent *master, icalcomponent *this)
{
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(this, 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_clone(param));
}
param = icalproperty_get_first_parameter(recurid, ICAL_VALUE_PARAMETER);
if (param) {
icalproperty_add_parameter(exdate, icalparameter_clone(param));
}
/* XXX Need to handle RANGE parameter */
/* Add the EXDATE */
icalcomponent_add_property(master, exdate);
}
/* we've already tested that master contains this attendee */
static void update_attendee_status(icalcomponent *ical, strarray_t *onrecurids,
const char *onattendee, const char *status)
{
icalcomponent *comp = icalcomponent_get_first_real_component(ical);
icalcomponent_kind kind = icalcomponent_isa(comp);
for (; comp; comp = icalcomponent_get_next_component(ical, kind)) {
if (onrecurids) {
/* this recurrenceid is attended by this attendee in the new data?
* there's nothing to cancel */
icalproperty *prop =
icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
const char *recurid = "";
if (prop) recurid = icalproperty_get_value_as_string(prop);
if (strarray_find(onrecurids, recurid, 0) < 0) continue;
}
icalproperty *prop = icalcomponent_get_first_invitee(comp);
for (; prop; prop = icalcomponent_get_next_invitee(comp)) {
const char *attendee = icalproperty_get_invitee(prop);
if (!attendee) continue;
if (!strncasecmp(attendee, "mailto:", 7)) attendee += 7;
/* skip attendees other than the one we're updating */
if (onattendee && strcasecmp(attendee, onattendee)) continue;
/* mark the status */
icalparameter *param = icalparameter_new_schedulestatus(status);
icalproperty_set_parameter(prop, param);
}
}
}
static icaltimetype get_historical_cutoff()
{
int age = config_getduration(IMAPOPT_CALDAV_HISTORICAL_AGE, 'd');
icaltimetype cutoff;
if (age < 0) return icaltime_null_time();
/* Set cutoff to current time -age days */
cutoff = icaltime_current_time_with_zone(icaltimezone_get_utc_timezone());
icaltime_adjust(&cutoff, 0, 0, 0, -age);
return cutoff;
}
static int icalcomponent_is_historical(icalcomponent *comp, icaltimetype cutoff)
{
if (icaltime_is_null_time(cutoff)) return 0;
icalcomponent_kind kind = icalcomponent_isa(comp);
struct icalperiodtype span;
if (icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY)) {
/* span is just the span of the override */
span = icalcomponent_get_utc_timespan(comp, kind, NULL);
}
else {
/* span is entire span of the master */
icalcomponent *ical = icalcomponent_new_vcalendar();
icalcomponent_add_component(ical, icalcomponent_clone(comp));
span = icalrecurrenceset_get_utc_timespan(ical, kind, NULL, NULL, NULL, NULL);
icalcomponent_free(ical);
}
return (icaltime_compare(span.end, cutoff) < 0);
}
static void schedule_full_cancel(const strarray_t *schedule_addresses,
const char *organizer, const char *attendee,
icalcomponent *mastercomp, icaltimetype h_cutoff,
icalcomponent *oldical, icalcomponent *newical)
{
/* we need to send a cancel for all recurrences with this attendee,
and add exdates to the master for all without this attendee */
icalcomponent *itip = make_itip(ICAL_METHOD_CANCEL, oldical);
icalcomponent *mastercopy = icalcomponent_clone(mastercomp);
clean_component(mastercopy);
icalcomponent_add_component(itip, mastercopy);
int do_send = !icalcomponent_is_historical(mastercopy, h_cutoff);
icalcomponent *comp = icalcomponent_get_first_real_component(oldical);
icalcomponent_kind kind = icalcomponent_isa(comp);
for (; comp; comp = icalcomponent_get_next_component(oldical, kind)) {
icalproperty *prop =
icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
if (!prop) continue; /* skip master */
const char *recurid = icalproperty_get_value_as_string(prop);
/* non matching are exdates on the master */
if (!find_attendee(comp, attendee)) {
schedule_set_exdate(mastercopy, comp);
continue;
}
icalcomponent *newcomp =
find_attended_component(newical, recurid, attendee);
if (newcomp) continue; /* will be scheduled separately */
icalcomponent *copy = icalcomponent_clone(comp);
clean_component(copy);
icalcomponent_add_component(itip, copy);
if (!do_send && !icalcomponent_is_historical(copy, h_cutoff))
do_send = 1;
}
if (do_send) {
struct sched_data sched =
{ 0, 0, 0, itip, oldical, newical, ICAL_SCHEDULEFORCESEND_NONE, schedule_addresses, NULL };
sched_deliver(organizer, attendee, &sched, httpd_authstate);
}
icalcomponent_free(itip);
}
/* we've already tested that master does NOT contain this attendee */
static void schedule_sub_cancels(const strarray_t *schedule_addresses,
const char *organizer, const char *attendee,
icaltimetype h_cutoff,
icalcomponent *oldical, icalcomponent *newical)
{
if (!oldical) return;
/* we have to create this upfront because it walks the components too */
icalcomponent *itip = make_itip(ICAL_METHOD_CANCEL, oldical);
icalcomponent *comp = icalcomponent_get_first_real_component(oldical);
icalcomponent_kind kind = icalcomponent_isa(comp);
int do_send = 0;
for (; comp; comp = icalcomponent_get_next_component(oldical, kind)) {
icalproperty *prop =
icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
if (!prop) continue;
const char *recurid = icalproperty_get_value_as_string(prop);
/* we're not attending, there's nothing to cancel */
if (!find_attendee(comp, attendee))
continue;
/* this recurrenceid is attended by this attendee in the new data?
* there's nothing to cancel */
if (find_attended_component(newical, recurid, attendee))
continue;
icalcomponent *copy = icalcomponent_clone(comp);
clean_component(copy);
icalcomponent_add_component(itip, copy);
if (!do_send && !icalcomponent_is_historical(copy, h_cutoff))
do_send = 1;
}
if (do_send) {
struct sched_data sched =
{ 0, 0, 0, itip, oldical, newical, ICAL_SCHEDULEFORCESEND_NONE, schedule_addresses, NULL };
sched_deliver(organizer, attendee, &sched, httpd_authstate);
}
icalcomponent_free(itip);
}
icalparameter_scheduleforcesend get_forcesend(icalproperty *prop)
{
icalparameter *param = icalproperty_get_scheduleforcesend_parameter(prop);
if (!param) return ICAL_SCHEDULEFORCESEND_NONE;
return icalparameter_get_scheduleforcesend(param);
}
/* we've already tested that master does NOT contain this attendee or that
* master doesn't need to be scheduled */
static void schedule_sub_updates(const strarray_t *schedule_addresses,
const char *organizer, const char *attendee,
icaltimetype h_cutoff,
icalcomponent *oldical, icalcomponent *newical)
{
if (!newical) return;
icalcomponent *itip = make_itip(ICAL_METHOD_REQUEST, newical);
strarray_t recurids = STRARRAY_INITIALIZER;
icalparameter_scheduleforcesend force_send = ICAL_SCHEDULEFORCESEND_NONE;
int is_update = 0;
icalcomponent *oldmaster = find_attended_component(oldical, "", attendee);
icalcomponent *comp = icalcomponent_get_first_real_component(newical);
icalcomponent_kind kind = icalcomponent_isa(comp);
int do_send = 0;
for (; comp; comp = icalcomponent_get_next_component(newical, kind)) {
icalproperty *prop =
icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
if (!prop) continue;
const char *recurid = icalproperty_get_value_as_string(prop);
/* we're not attending, nothing to do */
icalproperty *att = find_attendee(comp, attendee);
if (!att) continue;
force_send = get_forcesend(att);
icalcomponent *freeme = NULL;
/* this recurrenceid was in the old data? if not we need to
* generate a synthetic one */
icalcomponent *oldcomp = find_component(oldical, recurid);
if (!oldcomp && oldmaster) {
oldcomp = freeme = master_to_recurrence(oldmaster, prop);
}
/* unchanged event - we don't need to send anything */
if (!check_changes(oldcomp, comp, attendee)) {
if (force_send == ICAL_SCHEDULEFORCESEND_NONE) {
if (freeme) icalcomponent_free(freeme);
continue;
}
}
icalcomponent *copy = icalcomponent_clone(comp);
clean_component(copy);
if (find_attendee(oldcomp, attendee))
is_update = 1;
icalcomponent_add_component(itip, copy);
strarray_add(&recurids, recurid);
if (!do_send && !icalcomponent_is_historical(copy, h_cutoff))
do_send = 1;
if (freeme) icalcomponent_free(freeme);
}
if (do_send) {
struct sched_data sched =
{ 0, 0, is_update, itip, oldical, newical, force_send, schedule_addresses, NULL };
sched_deliver(organizer, attendee, &sched, httpd_authstate);
update_attendee_status(newical, &recurids, attendee, sched.status);
}
icalcomponent_free(itip);
strarray_fini(&recurids);
}
/* we've already tested that master does contain this attendee */
static void schedule_full_update(const strarray_t *schedule_addresses,
const char *organizer, const char *attendee,
icalcomponent *mastercomp, icaltimetype h_cutoff,
icalcomponent *oldical, icalcomponent *newical)
{
/* create an itip for the complete event */
icalcomponent *itip = make_itip(ICAL_METHOD_REQUEST, newical);
icalcomponent *mastercopy = icalcomponent_clone(mastercomp);
clean_component(mastercopy);
icalcomponent_add_component(itip, mastercopy);
int do_send = 0;
int is_update = 0;
icalcomponent *oldmaster = find_attended_component(oldical, "", attendee);
if (check_changes(oldmaster, mastercopy, attendee)) {
/* we only force the send if the top level event has changed */
if (!icalcomponent_is_historical(mastercopy, h_cutoff)) do_send = 1;
if (oldmaster) {
is_update = 1;
if (!do_send && !icalcomponent_is_historical(oldmaster, h_cutoff))
do_send = 1;
}
}
icalproperty *masteratt = find_attendee(mastercomp, attendee);
icalparameter_scheduleforcesend force_send = get_forcesend(masteratt);
/* force the matter */
if (force_send != ICAL_SCHEDULEFORCESEND_NONE) do_send = 1;
icalcomponent *comp = icalcomponent_get_first_real_component(newical);
icalcomponent_kind kind = icalcomponent_isa(comp);
for (; comp; comp = icalcomponent_get_next_component(newical, kind)) {
/* this recurrenceid is attended by this attendee in the old data?
* check if changed */
icalproperty *prop =
icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
if (!prop) continue;
const char *recurid = icalproperty_get_value_as_string(prop);
/* we can't just use "find_attended_component" here, because a previous
* sub component without this attendee is an old EXDATE for us, while
* no previous sub component means it was just a regular recurrence
* of the master event */
icalcomponent *oldcomp = find_component(oldical, recurid);
int has_old = !!find_attendee(oldcomp, attendee);
if (has_old) is_update = 1;
if (!oldcomp && oldmaster)
is_update = 1;
/* non matching are exdates on the master */
if (!find_attendee(comp, attendee)) {
schedule_set_exdate(mastercopy, comp);
/* different from last time? */
if ((!oldcomp || has_old) &&
!do_send && !icalcomponent_is_historical(comp, h_cutoff)) {
do_send = 1;
}
continue;
}
icalcomponent *copy = icalcomponent_clone(comp);
/* we don't care if it's changed, just using this for the
* side effect changes to RSVP */
check_changes(oldcomp, copy, attendee);
clean_component(copy);
icalcomponent_add_component(itip, copy);
}
if (do_send) {
struct sched_data sched =
{ 0, 0, is_update, itip, oldical, newical, force_send, schedule_addresses, NULL };
sched_deliver(organizer, attendee, &sched, httpd_authstate);
update_attendee_status(newical, NULL, attendee, sched.status);
}
else {
/* just look for sub updates */
schedule_sub_updates(schedule_addresses, organizer, attendee, h_cutoff, oldical, newical);
}
icalcomponent_free(itip);
}
/* sched_request() helper
* handles scheduling for a single attendee */
static void schedule_one_attendee(const strarray_t *schedule_addresses,
const char *organizer, const char *attendee,
icaltimetype h_cutoff,
icalcomponent *oldical, icalcomponent *newical)
{
/* case: this attendee is attending the master event */
icalcomponent *mastercomp;
if ((mastercomp = find_attended_component(newical, "", attendee))) {
schedule_full_update(schedule_addresses, organizer, attendee,
mastercomp, h_cutoff, oldical, newical);
return;
}
/* otherwise we need to cancel for each sub event and then we'll still
* send the updates if any */
if ((mastercomp = find_attended_component(oldical, "", attendee))) {
schedule_full_cancel(schedule_addresses, organizer, attendee, mastercomp, h_cutoff, oldical, newical);
}
else {
schedule_sub_cancels(schedule_addresses, organizer, attendee, h_cutoff, oldical, newical);
}
schedule_sub_updates(schedule_addresses, organizer, attendee, h_cutoff, oldical, newical);
}
/* Create and deliver an organizer scheduling request */
void sched_request(const char *asuserid, const strarray_t *schedule_addresses,
const char *organizer,
icalcomponent *oldical, icalcomponent *newical)
{
/* Check ACL of auth'd user on userid's Scheduling Outbox */
int rights = 0;
mbentry_t *mbentry = NULL;
char *outboxname = caldav_mboxname(asuserid, SCHED_OUTBOX);
int r = mboxlist_lookup(outboxname, &mbentry, NULL);
if (r) {
syslog(LOG_INFO, "mboxlist_lookup(%s) failed: %s",
outboxname, error_message(r));
}
else {
rights = httpd_myrights(httpd_authstate, mbentry);
}
free(outboxname);
mboxlist_entry_free(&mbentry);
if (!(rights & DACL_INVITE)) {
/* DAV:need-privileges */
syslog(LOG_DEBUG, "No scheduling send ACL for user %s on Outbox %s",
httpd_userid, organizer);
update_attendee_status(newical, NULL, NULL, SCHEDSTAT_NOPRIVS);
return;
}
/* ok, let's figure out who the attendees are */
strarray_t attendees = STRARRAY_INITIALIZER;
add_attendees(oldical, organizer, &attendees);
add_attendees(newical, organizer, &attendees);
icaltimetype h_cutoff = get_historical_cutoff();
int i;
for (i = 0; i < strarray_size(&attendees); i++) {
const char *attendee = strarray_nth(&attendees, i);
syslog(LOG_NOTICE, "iTIP scheduling request from %s to %s",
organizer, attendee);
schedule_one_attendee(schedule_addresses, organizer, attendee, h_cutoff, oldical, newical);
}
strarray_fini(&attendees);
}
/*******************************************************************/
/* REPLIES */
struct reply_data {
icalcomponent *itip;
const char *organizer;
strarray_t *didparts;
int master_send;
int do_send;
icalparameter_scheduleforcesend force_send;
};
/*
* sched_reply() helper function
*
* Remove all attendees from 'comp' other than the one corresponding to 'match'
*/
static void trim_attendees(icalcomponent *comp, const char *match)
{
icalproperty *prop;
ptrarray_t remove = PTRARRAY_INITIALIZER;
/* Locate userid in the attendee list (stripping others) */
for (prop = icalcomponent_get_first_invitee(comp);
prop;
prop = icalcomponent_get_next_invitee(comp)) {
const char *attendee = icalproperty_get_invitee(prop);
if (!attendee) continue;
if (!strncasecmp(attendee, "mailto:", 7)) attendee += 7;
/* keep my attendee */
if (!strcasecmp(attendee, match)) continue;
/* Some other attendee, remove it */
ptrarray_append(&remove, prop);
}
int i;
for (i = 0; i < remove.count; i++) {
icalcomponent_remove_invitee(comp, ptrarray_nth(&remove, i));
}
ptrarray_fini(&remove);
}
/*
* sched_reply() helper function
*
* Attendee removed this component, mark it as declined for the organizer.
*/
static int reply_mark_declined(icalcomponent *comp)
{
icalproperty *myattendee;
icalparameter *param;
if (!comp) return 0;
/* Don't send a decline for cancelled components */
if (icalcomponent_get_status(comp) == ICAL_STATUS_CANCELLED)
return 0;
myattendee = icalcomponent_get_first_property(comp, ICAL_ATTENDEE_PROPERTY);
param = icalparameter_new_partstat(ICAL_PARTSTAT_DECLINED);
icalproperty_set_parameter(myattendee, param);
return 1;
}
/* we've already tested that master contains this attendee */
static void update_organizer_status(icalcomponent *ical, strarray_t *onrecurids,
const char *status)
{
icalcomponent *comp = icalcomponent_get_first_real_component(ical);
icalcomponent_kind kind = icalcomponent_isa(comp);
for (; comp; comp = icalcomponent_get_next_component(ical, kind)) {
if (onrecurids) {
icalproperty *prop =
icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
const char *recurid = "";
if (prop) recurid = icalproperty_get_value_as_string(prop);
if (strarray_find(onrecurids, recurid, 0) < 0) continue;
}
icalproperty *prop =
icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
/* mark the status */
icalparameter *param = icalparameter_new_schedulestatus(status);
icalproperty_set_parameter(prop, param);
}
}
static const char *get_organizer(icalcomponent *comp)
{
icalproperty *prop =
icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
const char *organizer = icalproperty_get_organizer(prop);
if (!organizer) return NULL;
if (!strncasecmp(organizer, "mailto:", 7)) organizer += 7;
icalparameter *param = icalproperty_get_scheduleagent_parameter(prop);
/* check if we're supposed to send replies to the organizer */
if (param &&
icalparameter_get_scheduleagent(param) != ICAL_SCHEDULEAGENT_SERVER)
return NULL;
return organizer;
}
static icalparameter_partstat get_partstat(icalcomponent *comp,
const char *attendee)
{
icalproperty *prop = find_attendee(comp, attendee);
if (!prop) return ICAL_PARTSTAT_NEEDSACTION;
icalparameter *param =
icalproperty_get_first_parameter(prop, ICAL_PARTSTAT_PARAMETER);
if (!param) return ICAL_PARTSTAT_NEEDSACTION;
return icalparameter_get_partstat(param);
}
static int partstat_changed(icalcomponent *oldcomp,
icalcomponent *newcomp, const char *attendee)
{
if (!attendee) return 1; // something weird is going on, treat it as a change
return (get_partstat(oldcomp, attendee) != get_partstat(newcomp, attendee));
}
static void schedule_sub_declines(const char *attendee,
icaltimetype h_cutoff,
icalcomponent *oldical, icalcomponent *newical,
struct reply_data *reply)
{
if (!oldical) return;
if (!reply->itip)
reply->itip = make_itip(ICAL_METHOD_REPLY, oldical);
icalcomponent *comp = icalcomponent_get_first_real_component(oldical);
icalcomponent_kind kind = icalcomponent_isa(comp);
for (; comp; comp = icalcomponent_get_next_component(oldical, kind)) {
icalproperty *prop =
icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
if (!prop) continue;
const char *recurid = icalproperty_get_value_as_string(prop);
/* we weren't attending, nothing to do */
if (!find_attendee(comp, attendee))
continue;
/* no organizer, can't do anything */
const char *organizer = get_organizer(comp);
if (!organizer) continue;
/* this recurrenceid is attended by this attendee in the new data?
don't decline, we've already replied if necessary */
icalcomponent *newcomp =
find_attended_component(newical, recurid, attendee);
if (newcomp) continue;
/* we need to send an update for this recurrence */
icalcomponent *copy = icalcomponent_clone(comp);
trim_attendees(copy, attendee);
if (kind == ICAL_VPOLL_COMPONENT) sched_vpoll_reply(copy);
clean_component(copy);
reply_mark_declined(copy);
icalcomponent_add_component(reply->itip, copy);
if (!reply->do_send && !icalcomponent_is_historical(comp, h_cutoff))
reply->do_send = 1;
}
}
/* we've already tested that master does NOT contain this attendee */
static void schedule_sub_replies(const char *attendee,
icaltimetype h_cutoff,
icalcomponent *oldical, icalcomponent *newical,
struct reply_data *reply)
{
if (!newical) return;
if (!reply->itip) reply->itip = make_itip(ICAL_METHOD_REPLY, newical);
icalcomponent *comp = icalcomponent_get_first_real_component(newical);
icalcomponent_kind kind = icalcomponent_isa(comp);
for (; comp; comp = icalcomponent_get_next_component(newical, kind)) {
icalproperty *prop =
icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
if (!prop) continue;
const char *recurid = icalproperty_get_value_as_string(prop);
/* we're not attending, nothing to do */
if (!find_attendee(comp, attendee))
continue;
/* no organizer, can't do anything */
const char *organizer = get_organizer(comp);
if (!organizer) continue;
icalparameter_scheduleforcesend force_send =
get_forcesend(icalcomponent_get_first_property(comp,
ICAL_ORGANIZER_PROPERTY));
/* this recurrenceid is attended by this attendee in the old data? */
icalcomponent *oldcomp =
find_attended_component(oldical, recurid, attendee);
/* unchanged partstat - we don't need to send anything */
if (!reply->master_send && !partstat_changed(oldcomp, comp, attendee)) {
if (force_send == ICAL_SCHEDULEFORCESEND_NONE)
continue;
}
/* XXX - test for changed between recurrences and error out? Any point? */
reply->force_send = force_send;
reply->organizer = organizer;
/* we need to send an update for this recurrence */
icalcomponent *copy = icalcomponent_clone(comp);
trim_attendees(copy, attendee);
if (kind == ICAL_VPOLL_COMPONENT) sched_vpoll_reply(copy);
clean_component(copy);
icalcomponent_add_component(reply->itip, copy);
if (!reply->master_send) {
if (!reply->didparts) reply->didparts = strarray_new();
strarray_add(reply->didparts, recurid);
}
if (!reply->do_send && !icalcomponent_is_historical(comp, h_cutoff))
reply->do_send = 1;
}
}
static void schedule_full_decline(const char *attendee,
icaltimetype h_cutoff,
icalcomponent *oldical, icalcomponent *newical __attribute__((unused)),
struct reply_data *reply)
{
/* we only get called if newical doesn't have an attended mastercomp */
icalcomponent *mastercomp = find_attended_component(oldical, "", attendee);
if (!mastercomp) return;
reply->organizer = get_organizer(mastercomp);
if (!reply->organizer) return;
/* we need to send a reply for this recurrence for sure, because we know that the
* that new master doesn't have this attendee */
if (!reply->itip) reply->itip = make_itip(ICAL_METHOD_REPLY, oldical);
reply->force_send =
get_forcesend(icalcomponent_get_first_property(mastercomp,
ICAL_ORGANIZER_PROPERTY));
icalcomponent *mastercopy = icalcomponent_clone(mastercomp);
trim_attendees(mastercopy, attendee);
if (icalcomponent_isa(mastercomp) == ICAL_VPOLL_COMPONENT) sched_vpoll_reply(mastercopy);
clean_component(mastercopy);
reply_mark_declined(mastercopy);
icalcomponent_add_component(reply->itip, mastercopy);
if (!reply->do_send && !icalcomponent_is_historical(mastercomp, h_cutoff))
reply->do_send = 1;
/* force ALL sub parts to be added */
reply->master_send = 1;
}
/* we've already tested that master contains this attendee */
static void schedule_full_reply(const char *attendee,
icaltimetype h_cutoff,
icalcomponent *oldical, icalcomponent *newical,
struct reply_data *reply)
{
icalcomponent *mastercomp = find_attended_component(newical, "", attendee);
icalcomponent_kind kind;
int add_master = 0;
if (!mastercomp) {
schedule_full_decline(attendee, h_cutoff, oldical, newical, reply);
return;
}
reply->organizer = get_organizer(mastercomp);
if (!reply->organizer) return;
kind = icalcomponent_isa(mastercomp);
reply->force_send =
get_forcesend(icalcomponent_get_first_property(mastercomp,
ICAL_ORGANIZER_PROPERTY));
/* calculate if we need to send a reply based on the master event */
/* it's forced */
if (reply->force_send != ICAL_SCHEDULEFORCESEND_NONE)
add_master = 1;
/* or it's a VPOLL */
else if (kind == ICAL_VPOLL_COMPONENT)
add_master = 1;
else {
/* or it's different */
icalcomponent *oldmaster = find_attended_component(oldical, "", attendee);
if (partstat_changed(oldmaster, mastercomp, attendee))
add_master = 1;
/* or it includes new EXDATEs */
else {
icalproperty *prop =
icalcomponent_get_first_property(mastercomp, ICAL_EXDATE_PROPERTY);
for (; prop; prop = icalcomponent_get_next_property(mastercomp,
ICAL_EXDATE_PROPERTY)) {
struct icaltimetype exdate = icalproperty_get_exdate(prop);
if (!has_exdate(oldmaster, exdate))
add_master = 1;
}
}
}
if (add_master) {
if (!reply->itip) reply->itip = make_itip(ICAL_METHOD_REPLY, newical);
/* add the master */
icalcomponent *mastercopy = icalcomponent_clone(mastercomp);
trim_attendees(mastercopy, attendee);
if (kind == ICAL_VPOLL_COMPONENT) sched_vpoll_reply(mastercopy);
clean_component(mastercopy);
icalcomponent_add_component(reply->itip, mastercopy);
/* force ALL sub parts to be added */
reply->master_send = 1;
/* master includes "recent" occurrence(s) - send it */
if (!icalcomponent_is_historical(mastercopy, h_cutoff))
reply->do_send = 1;
}
}
/* Create and deliver an attendee scheduling reply */
void sched_reply(const char *onuserid, const strarray_t *schedule_addresses,
icalcomponent *oldical, icalcomponent *newical)
{
/* Check ACL of auth'd user on userid's Scheduling Outbox */
int rights = 0;
mbentry_t *mbentry = NULL;
char *outboxname = caldav_mboxname(onuserid, SCHED_OUTBOX);
int r = mboxlist_lookup(outboxname, &mbentry, NULL);
if (r) {
syslog(LOG_INFO, "mboxlist_lookup(%s) failed: %s",
outboxname, error_message(r));
}
else {
rights = httpd_myrights(httpd_authstate, mbentry);
}
free(outboxname);
mboxlist_entry_free(&mbentry);
if (!(rights & DACL_REPLY)) {
/* DAV:need-privileges */
syslog(LOG_DEBUG, "No scheduling send ACL for user %s on Outbox %s",
httpd_userid, onuserid);
update_organizer_status(newical, NULL, SCHEDSTAT_NOPRIVS);
return;
}
/* otherwise we need to decline for each sub event and then we'll still
* send the accepts if any */
icaltimetype h_cutoff = get_historical_cutoff();
int i;
for (i = 0; i < strarray_size(schedule_addresses); i++) {
const char *attendee = strarray_nth(schedule_addresses, i);
struct reply_data reply = { NULL, NULL, NULL, 0, 0, ICAL_SCHEDULEFORCESEND_NONE };
if (!strncasecmp(attendee, "mailto:", 7)) attendee += 7;
schedule_full_reply(attendee, h_cutoff, oldical, newical, &reply);
schedule_sub_replies(attendee, h_cutoff, oldical, newical, &reply);
schedule_sub_declines(attendee, h_cutoff, oldical, newical, &reply);
if (reply.do_send) {
struct sched_data sched =
{ 0, 1, 0, reply.itip, oldical, newical, reply.force_send, schedule_addresses, NULL };
syslog(LOG_NOTICE, "iTIP scheduling reply from %s to %s",
attendee, reply.organizer ? reply.organizer : "<unknown>");
sched_deliver(attendee, reply.organizer, &sched, httpd_authstate);
update_organizer_status(newical, reply.didparts, sched.status);
}
if (reply.didparts) strarray_free(reply.didparts);
if (reply.itip) icalcomponent_free(reply.itip);
}
}
void sched_param_fini(struct caldav_sched_param *sparam)
{
free(sparam->userid);
free(sparam->server);
struct proplist *prop, *next;
for (prop = sparam->props; prop; prop = next) {
next = prop->next;
free(prop);
}
memset(sparam, 0, sizeof(struct caldav_sched_param));
}
void get_schedule_addresses(hdrcache_t req_hdrs, const char *mboxname,
const char *userid, strarray_t *addresses)
{
struct buf buf = BUF_INITIALIZER;
/* allow override of schedule-address per-message (FM specific) */
const char **hdr = spool_getheader(req_hdrs, "Schedule-Address");
if (hdr) {
if (!strncasecmp(hdr[0], "mailto:", 7))
strarray_append(addresses, hdr[0]+7);
else
strarray_append(addresses, hdr[0]);
}
else {
/* find schedule address based on the destination calendar's user */
/* check calendar-user-address-set for target user's mailbox */
const char *annotname =
DAV_ANNOT_NS "<" XML_NS_CALDAV ">calendar-user-address-set";
int r = annotatemore_lookupmask(mboxname, annotname,
userid, &buf);
if (r || !buf.len) {
/* check calendar-user-address-set for target user's principal */
char *calhomeset = caldav_mboxname(userid, NULL);
buf_reset(&buf);
r = annotatemore_lookupmask(calhomeset, annotname,
userid, &buf);
free(calhomeset);
}
if (!r && buf.len) {
strarray_t *values =
strarray_split(buf_cstring(&buf), ",", STRARRAY_TRIM);
int i;
for (i = 0; i < strarray_size(values); i++) {
const char *item = strarray_nth(values, i);
if (!strncasecmp(item, "mailto:", 7)) item += 7;
strarray_append(addresses, item);
}
strarray_free(values);
}
else if (strchr(userid, '@')) {
/* userid corresponding to target */
strarray_append(addresses, userid);
}
else {
/* append fully qualified userids */
struct strlist *domains;
for (domains = cua_domains; domains; domains = domains->next) {
buf_reset(&buf);
buf_printf(&buf, "%s@%s", userid, domains->s);
strarray_appendm(addresses, buf_release(&buf));
}
}
}
buf_free(&buf);
}

File Metadata

Mime Type
text/x-c
Expires
Sat, Apr 4, 4:25 AM (20 h, 39 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18822568
Default Alt Text
http_caldav_sched.c (114 KB)

Event Timeline