static void sched_reply(const char *userid,
			icalcomponent *oldical, icalcomponent *newical)
{
    int r, rights = 0;
    mbentry_t *mbentry = NULL;
    char *outboxname;
    icalcomponent *ical;
    struct sched_data *sched_data;
    struct auth_state *authstate;
    icalcomponent *comp;
    icalproperty *prop;
    icalparameter *param;
    icalcomponent_kind kind;
    icalparameter_scheduleforcesend force_send = ICAL_SCHEDULEFORCESEND_NONE;
    const char *organizer, *recurid;
    struct hash_table comp_table;
    struct comp_data *old_data;

    /* Check what kind of action we are dealing with */
    if (!newical) {
	/* Remove */
	ical = oldical;
    }
    else {
	/* Create / Modify */
	ical = newical;
    }

    /* Check CalDAV Scheduling parameters on the organizer */
    comp = icalcomponent_get_first_real_component(ical);
    kind = icalcomponent_isa(comp);
    prop = icalcomponent_get_first_property(comp, ICAL_ORGANIZER_PROPERTY);
    organizer = icalproperty_get_organizer(prop);

    param = icalproperty_get_scheduleagent_parameter(prop);
    if (param &&
	icalparameter_get_scheduleagent(param) != ICAL_SCHEDULEAGENT_SERVER) {
	/* We are not supposed to send replies to the organizer */
	return;
    }

    param = icalproperty_get_scheduleforcesend_parameter(prop);
    if (param) force_send = icalparameter_get_scheduleforcesend(param);

    sched_data = xzmalloc(sizeof(struct sched_data));
    sched_data->is_reply = 1;
    sched_data->force_send = force_send;

    /* Check ACL of auth'd user on userid's Scheduling Outbox */
    outboxname = caldav_mboxname(userid, SCHED_OUTBOX);

    r = mboxlist_lookup(outboxname, &mbentry, NULL);
    if (r) {
	syslog(LOG_INFO, "mboxlist_lookup(%s) failed: %s",
	       outboxname, error_message(r));
    }
    else {
	rights = cyrus_acl_myrights(httpd_authstate, mbentry->acl);
	mboxlist_entry_free(&mbentry);
    }
    free(outboxname);

    if (!(rights & DACL_REPLY)) {
	/* DAV:need-privileges */
	if (newical) sched_data->status = SCHEDSTAT_NOPRIVS;
	syslog(LOG_DEBUG, "No scheduling send ACL for user %s on Outbox %s",
	       httpd_userid, userid);

	goto done;
    }

    /* Create our reply iCal object */
    sched_data->itip =
	icalcomponent_vanew(ICAL_VCALENDAR_COMPONENT,
			    icalproperty_new_version("2.0"),
			    icalproperty_new_prodid(ical_prodid),
			    icalproperty_new_method(ICAL_METHOD_REPLY),
			    0);

    /* XXX  Make sure SEQUENCE is incremented */

    /* Copy over any CALSCALE property */
    prop = icalcomponent_get_first_property(ical, ICAL_CALSCALE_PROPERTY);
    if (prop) {
	icalcomponent_add_property(sched_data->itip,
				   icalproperty_new_clone(prop));
    }

    /* Copy over any VTIMEZONE components */
    for (comp = icalcomponent_get_first_component(ical,
						  ICAL_VTIMEZONE_COMPONENT);
	 comp;
	 comp = icalcomponent_get_next_component(ical,
						 ICAL_VTIMEZONE_COMPONENT)) {
	 icalcomponent_add_component(sched_data->itip,
				     icalcomponent_new_clone(comp));
    }

    /* Add each component of old object to hash table for comparison */
    construct_hash_table(&comp_table, 10, 1);

    if (oldical) {
	comp = icalcomponent_get_first_real_component(oldical);
	do {
	    old_data = xzmalloc(sizeof(struct comp_data));

	    old_data->comp = trim_attendees(comp, userid, NULL,
					    &old_data->partstat, &recurid);

	    hash_insert(recurid, old_data, &comp_table);

	} while ((comp = icalcomponent_get_next_component(oldical, kind)));
    }

    /* Process each component of new object */
    if (newical) {
	unsigned ncomp = 0;

	comp = icalcomponent_get_first_real_component(newical);
	do {
	    icalcomponent *copy;
	    icalproperty *myattendee;
	    icalparameter_partstat partstat;
	    int changed = 1;

	    copy = trim_attendees(comp, userid,
				  &myattendee, &partstat, &recurid);
	    if (myattendee) {
		/* Found our userid */
		old_data = hash_del(recurid, &comp_table);

		if (old_data) {
		    if (kind == ICAL_VPOLL_COMPONENT) {
			/* VPOLL replies always override existing votes */
			sched_vpoll_reply(copy,
					  icalproperty_get_voter(myattendee));
		    }
		    else {
			/* XXX  Need to check EXDATE */

			/* Compare PARTSTAT in the two components */
			if (old_data->partstat == partstat) {
			    changed = 0;
			}
		    }

		    free_comp_data(old_data);
		}
	    }
	    else {
		/* Our user isn't in this component */
		/* XXX  Can this actually happen? */
		changed = 0;
	    }

	    if (changed) {
		clean_component(copy, 1);

		icalcomponent_add_component(sched_data->itip, copy);
		sched_data->comp_mask |= (1 << ncomp);
	    }
	    else icalcomponent_free(copy);

	    ncomp++;
	} while ((comp = icalcomponent_get_next_component(newical, kind)));
    }

    /* Decline any components that have been left behind in the old obj */
    hash_enumerate(&comp_table, sched_decline, sched_data->itip);
    free_hash_table(&comp_table, free_comp_data);

  done:
    if (sched_data->itip &&
	icalcomponent_get_first_real_component(sched_data->itip)) {
	/* We built a reply object */

	if (!sched_data->status) {
	    /* Attempt to deliver reply to organizer */
	    authstate = auth_newstate(userid);
	    sched_deliver(organizer, sched_data, authstate);
	    auth_freestate(authstate);
	}

	if (newical) {
	    unsigned ncomp = 0;

	    /* Set SCHEDULE-STATUS for organizer in attendee object */
	    comp = icalcomponent_get_first_real_component(newical);
	    do {
		if (sched_data->comp_mask & (1 << ncomp)) {
		    prop =
			icalcomponent_get_first_property(comp,
							 ICAL_ORGANIZER_PROPERTY);
		    param =
			icalparameter_new_schedulestatus(sched_data->status);
		    icalproperty_add_parameter(prop, param);
		}

		ncomp++;
	    } while ((comp = icalcomponent_get_next_component(newical, kind)));
	}
    }

    /* Cleanup */
    free_sched_data(sched_data);
}