/* 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 = proxy_mlookup(mbname_intname(mbname), &mbentry, NULL, 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-" TIME_T_FMT "-%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);
    }

    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

    /* Don't send a bogus message - check late to not allocate our own copy */
    const char *ical_str = icalcomponent_as_ical_string(sched_data->itip);
    if (!ical_str) return 0;

    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, *master = NULL;
    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 {
            master = comp;
            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. */
            if (icalcomponent_get_status(master) == ICAL_STATUS_CANCELLED) {
                /* The master event has been cancelled - ignore this override. */
                continue;
            }

            /* create a new recurrence from master component. */
            comp = master_to_recurrence(master, 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);
        }
        else if (icalcomponent_get_status(comp) == ICAL_STATUS_CANCELLED) {
            /* This component has been cancelled - ignore the reply */
            continue;
        }

        /* 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);
    mailboxname = NULL;

    /* 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 */
        char *scheddefault = caldav_scheddefault(userid);
        mailboxname = caldav_mboxname(userid, scheddefault);
        free(scheddefault);
        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 (icalcomponent_get_status(comp) == ICAL_STATUS_CANCELLED) {
        /* Can't attend a cancelled event */
        return NULL;
    }
    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_set_status(mastercopy, ICAL_STATUS_CANCELLED);
    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_set_status(copy, ICAL_STATUS_CANCELLED);
        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_set_status(copy, ICAL_STATUS_CANCELLED);
        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_add(addresses, hdr[0]+7);
        else
            strarray_add(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_add(addresses, item);
            }
            strarray_free(values);
        }
        else if (strchr(userid, '@')) {
            /* userid corresponding to target */
            strarray_add(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_add(addresses, buf_cstring(&buf));
            }
        }
    }

    buf_free(&buf);
}
