/* http_dav.c -- Routines for dealing with DAV properties in httpd
 *
 * Copyright (c) 1994-2011 Carnegie Mellon University.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The name "Carnegie Mellon University" must not be used to
 *    endorse or promote products derived from this software without
 *    prior written permission. For permission or any legal
 *    details, please contact
 *      Carnegie Mellon University
 *      Center for Technology Transfer and Enterprise Creation
 *      4615 Forbes Avenue
 *      Suite 302
 *      Pittsburgh, PA  15213
 *      (412) 268-7393, fax: (412) 268-7395
 *      innovation@andrew.cmu.edu
 *
 * 4. Redistributions of any form whatsoever must retain the following
 *    acknowledgment:
 *    "This product includes software developed by Computing Services
 *     at Carnegie Mellon University (http://www.cmu.edu/computing/)."
 *
 * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
 * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
 * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
 * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 */

#include <sysexits.h>

#include "httpd.h"
#include "http_dav.h"
#include "http_dav_sharing.h"
#include "http_proxy.h"
#include "proxy.h"
#include "strhash.h"
#include "syslog.h"
#include "times.h"
#include "user.h"
#include "webdav_db.h"
#include "xstrlcpy.h"

/* generated headers are not necessarily in current directory */
#include "imap/http_err.h"
#include "imap/imap_err.h"

#define DAVNOTIFICATION_CONTENT_TYPE \
    "application/davnotification+xml; charset=utf-8"

static struct webdav_db *auth_webdavdb = NULL;

static void my_dav_init(struct buf *serverinfo);
static int my_dav_auth(const char *userid);
static void my_dav_reset(void);
static void my_dav_shutdown(void);

static unsigned long notify_allow_cb(struct request_target_t *tgt);

static int notify_parse_path(const char *path, struct request_target_t *tgt,
                             const char **resultstr);

static int notify_get(struct transaction_t *txn, struct mailbox *mailbox,
                      struct index_record *record, void *data, void **obj);
static int notify_put(struct transaction_t *txn, void *obj,
                      struct mailbox *mailbox, const char *resource,
                      void *davdb, unsigned flags);

static int propfind_restype(const xmlChar *name, xmlNsPtr ns,
                            struct propfind_ctx *fctx,
                            xmlNodePtr prop, xmlNodePtr resp,
                            struct propstat propstat[], void *rock);

static int propfind_notifytype(const xmlChar *name, xmlNsPtr ns,
                               struct propfind_ctx *fctx,
                               xmlNodePtr prop, xmlNodePtr resp,
                               struct propstat propstat[], void *rock);

static struct buf *from_xml(xmlDocPtr doc)
{
    struct buf *buf = buf_new();
    xmlChar *xml = NULL;
    int len = 0;

    /* Dump XML response tree into a text buffer */
    xmlDocDumpFormatMemoryEnc(doc, &xml, &len, "utf-8",
                              config_httpprettytelemetry);
    if (xml) buf_initm(buf, (char *) xml, len);

    return buf;
}

static xmlDocPtr to_xml(const struct buf *buf)
{
    xmlParserCtxtPtr ctxt;
    xmlDocPtr doc = NULL;

    ctxt = xmlNewParserCtxt();
    if (ctxt) {
        doc = xmlCtxtReadMemory(ctxt, buf_base(buf), buf_len(buf), NULL, NULL,
                                XML_PARSE_NOWARNING);
        xmlFreeParserCtxt(ctxt);
    }

    return doc;
}

static struct mime_type_t notify_mime_types[] = {
    /* First item MUST be the default type and storage format */
    { DAVNOTIFICATION_CONTENT_TYPE, NULL, "xml",
      (struct buf* (*)(void *)) &from_xml,
      (void * (*)(const struct buf*)) &to_xml,
      (void (*)(void *)) &xmlFreeDoc, NULL, NULL
    },
    { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }
};

/* Array of supported REPORTs */
static const struct report_type_t notify_reports[] = {

    /* WebDAV Versioning (RFC 3253) REPORTs */
    { "expand-property", NS_DAV, "multistatus", &report_expand_prop,
      DACL_READ, 0 },

    /* WebDAV ACL (RFC 3744) REPORTs */
    { "acl-principal-prop-set", NS_DAV, "multistatus", &report_acl_prin_prop,
      DACL_ADMIN, REPORT_NEED_MBOX | REPORT_NEED_PROPS | REPORT_DEPTH_ZERO },

    /* WebDAV Sync (RFC 6578) REPORTs */
    { "sync-collection", NS_DAV, "multistatus", &report_sync_col,
      DACL_READ, REPORT_NEED_MBOX | REPORT_NEED_PROPS },

    { NULL, 0, NULL, NULL, 0, 0 }
};

/* Array of known "live" properties */
static const struct prop_entry notify_props[] = {

    /* WebDAV (RFC 4918) properties */
    { "creationdate", NS_DAV,
      PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
      propfind_creationdate, NULL, NULL },
    { "displayname", NS_DAV,
      PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE | PROP_PERUSER,
      propfind_fromdb, proppatch_todb, NULL },
    { "getcontentlanguage", NS_DAV,
      PROP_ALLPROP | PROP_RESOURCE,
      propfind_fromhdr, NULL, "Content-Language" },
    { "getcontentlength", NS_DAV,
      PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
      propfind_getlength, NULL, NULL },
    { "getcontenttype", NS_DAV,
      PROP_ALLPROP | PROP_RESOURCE,
      propfind_fromhdr, NULL, "Content-Type" },
    { "getetag", NS_DAV,
      PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
      propfind_getetag, NULL, NULL },
    { "getlastmodified", NS_DAV,
      PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
      propfind_getlastmod, NULL, NULL },
    { "resourcetype", NS_DAV,
      PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
      propfind_restype, NULL, NULL },

    /* WebDAV Versioning (RFC 3253) properties */
    { "supported-report-set", NS_DAV,
      PROP_COLLECTION,
      propfind_reportset, NULL, (void *) notify_reports },
    { "supported-method-set", NS_DAV,
      PROP_COLLECTION | PROP_RESOURCE,
      propfind_methodset, NULL, (void *) &notify_allow_cb },

    /* WebDAV ACL (RFC 3744) properties */
    { "owner", NS_DAV,
      PROP_COLLECTION | PROP_RESOURCE,
      propfind_owner, NULL, NULL },
    { "group", NS_DAV,
      PROP_COLLECTION | PROP_RESOURCE,
      NULL, NULL, NULL },
    { "supported-privilege-set", NS_DAV,
      PROP_COLLECTION | PROP_RESOURCE,
      propfind_supprivset, NULL, NULL },
    { "current-user-privilege-set", NS_DAV,
      PROP_COLLECTION | PROP_RESOURCE,
      propfind_curprivset, NULL, NULL },
    { "acl", NS_DAV,
      PROP_COLLECTION | PROP_RESOURCE,
      propfind_acl, NULL, NULL },
    { "acl-restrictions", NS_DAV,
      PROP_COLLECTION | PROP_RESOURCE,
      propfind_aclrestrict, NULL, NULL },
    { "inherited-acl-set", NS_DAV,
      PROP_COLLECTION | PROP_RESOURCE,
      NULL, NULL, NULL },
    { "principal-collection-set", NS_DAV,
      PROP_COLLECTION | PROP_RESOURCE,
      propfind_princolset, NULL, NULL },

    /* WebDAV Current Principal (RFC 5397) properties */
    { "current-user-principal", NS_DAV,
      PROP_COLLECTION | PROP_RESOURCE,
      propfind_curprin, NULL, NULL },

    /* WebDAV Sync (RFC 6578) properties */
    { "sync-token", NS_DAV,
      PROP_COLLECTION,
      propfind_sync_token, NULL, SYNC_TOKEN_URL_SCHEME },

    /* WebDAV Notifications (draft-pot-webdav-notifications) properties */
    { "notificationtype", NS_DAV,
      PROP_RESOURCE,
      propfind_notifytype, NULL, NULL },

    /* Backwards compatibility with Apple notifications clients */
    { "notificationtype", NS_CS,
      PROP_RESOURCE,
      propfind_notifytype, NULL, "calendarserver-sharing" },

    /* Apple Calendar Server properties */
    { "getctag", NS_CS,
      PROP_ALLPROP | PROP_COLLECTION,
      propfind_sync_token, NULL, "" },

    { NULL, 0, 0, NULL, NULL, NULL }
};

static struct meth_params notify_params = {
    notify_mime_types,
    &notify_parse_path,
    &dav_get_validators,
    &dav_get_modseq,
    &dav_check_precond,
    { (db_open_proc_t) &webdav_open_mailbox,
      (db_close_proc_t) &webdav_close,
      (db_proc_t) &webdav_begin,
      (db_proc_t) &webdav_commit,
      (db_proc_t) &webdav_abort,
      (db_lookup_proc_t) &webdav_lookup_resource,
      (db_imapuid_proc_t) &webdav_lookup_imapuid,
      (db_foreach_proc_t) &webdav_foreach,
      (db_updates_proc_t) &webdav_get_updates,
      (db_write_proc_t) &webdav_write,
      (db_delete_proc_t) &webdav_delete },
    NULL,                                       /* No ACL extensions */
    { 0, &notify_put },
    NULL,                                       /* No special DELETE handling */
    &notify_get,
    { 0, 0, NULL },                             /* No MKCOL handling */
    NULL,                                       /* No PATCH handling */
    { 0, &notify_post, { 0, NULL, NULL } },     /* No generic POST handling */
    { 0, &notify_put },
    { DAV_FINITE_DEPTH, notify_props},
    notify_reports
};


/* Namespace for WebDAV notification collections */
struct namespace_t namespace_notify = {
    URL_NS_NOTIFY, 0, "notify", "/dav/notifications", NULL,
    http_allow_noauth_get, /*authschemes*/0,
    MBTYPE_COLLECTION,
    (ALLOW_READ | ALLOW_POST | ALLOW_DELETE |
     ALLOW_DAV | ALLOW_PROPPATCH | ALLOW_ACL),
    &my_dav_init, &my_dav_auth, &my_dav_reset, &my_dav_shutdown,
    &dav_premethod, /*bearer*/NULL,
    {
        { &meth_acl,            &notify_params },      /* ACL          */
        { NULL,                 NULL },                /* BIND         */
        { NULL,                 NULL },                /* CONNECT      */
        { NULL,                 NULL },                /* COPY         */
        { &meth_delete,         &notify_params },      /* DELETE       */
        { &meth_get_head,       &notify_params },      /* GET          */
        { &meth_get_head,       &notify_params },      /* HEAD         */
        { NULL,                 NULL },                /* LOCK         */
        { NULL,                 NULL },                /* MKCALENDAR   */
        { NULL,                 NULL },                /* MKCOL        */
        { NULL,                 NULL },                /* MOVE         */
        { &meth_options,        &notify_parse_path },  /* OPTIONS      */
        { NULL,                 NULL },                /* PATCH        */
        { &meth_post,           &notify_params },      /* POST         */
        { &meth_propfind,       &notify_params },      /* PROPFIND     */
        { NULL,                 NULL },                /* PROPPATCH    */
        { NULL,                 NULL },                /* PUT          */
        { &meth_report,         &notify_params },      /* REPORT       */
        { &meth_trace,          &notify_parse_path },  /* TRACE        */
        { NULL,                 NULL },                /* UNBIND       */
        { NULL,                 NULL },                /* UNLOCK       */
    }
};


static void my_dav_init(struct buf *serverinfo __attribute__((unused)))
{
    if (!namespace_principal.enabled) return;

    if (!config_getstring(IMAPOPT_DAVNOTIFICATIONSPREFIX)) {
        fatal("Required 'davnotificationsprefix' option is not set", EX_CONFIG);
    }

    namespace_notify.enabled = 1;

    webdav_init();
}


static int lookup_notify_collection(const char *userid, mbentry_t **mbentry)
{
    mbname_t *mbname;
    const char *notifyname;
    int r;

    /* Create notification mailbox name from the parsed path */
    mbname = mbname_from_userid(userid);
    mbname_push_boxes(mbname, config_getstring(IMAPOPT_DAVNOTIFICATIONSPREFIX));

    /* XXX - hack to allow @domain parts for non-domain-split users */
    if (httpd_extradomain) {
        /* not allowed to be cross domain */
        if (mbname_localpart(mbname) &&
            strcmpsafe(mbname_domain(mbname), httpd_extradomain)) {
            r = HTTP_NOT_FOUND;
            goto done;
        }
        mbname_set_domain(mbname, NULL);
    }

    /* Locate the mailbox */
    notifyname = mbname_intname(mbname);
    r = proxy_mlookup(notifyname, mbentry, NULL, NULL);
    if (r == IMAP_MAILBOX_NONEXISTENT) {
        /* Find location of INBOX */
        char *inboxname = mboxname_user_mbox(userid, NULL);

        int r1 = proxy_mlookup(inboxname, mbentry, NULL, NULL);
        free(inboxname);
        if (r1 == IMAP_MAILBOX_NONEXISTENT) {
            r = IMAP_INVALID_USER;
            goto done;
        }

        if (*mbentry) free((*mbentry)->name);
        else *mbentry = mboxlist_entry_create();
        (*mbentry)->name = xstrdup(notifyname);
    }

  done:
    mbname_free(&mbname);

    return r;
}

static int _create_notify_collection(const char *userid, struct mailbox **mailbox)
{
    /* lock the namespace lock and try again */
    struct mboxlock *namespacelock = user_namespacelock(userid);

    mbentry_t *mbentry = NULL;
    int r = lookup_notify_collection(userid, &mbentry);

    if (r == IMAP_MAILBOX_NONEXISTENT) {
        if (!mbentry) goto done;
        else if (mbentry->server) {
            proxy_findserver(mbentry->server, &http_protocol, httpd_userid,
                             &backend_cached, NULL, NULL, httpd_in);
            goto done;
        }

        r = mboxlist_createmailbox(mbentry->name, MBTYPE_COLLECTION,
                                   NULL, 1 /* admin */, userid, NULL,
                                   0, 0, 0, 0, mailbox);
        /* we lost the race, that's OK */
        if (r == IMAP_MAILBOX_LOCKED) r = 0;
        if (r) syslog(LOG_ERR, "IOERROR: failed to create %s (%s)",
                      mbentry->name, error_message(r));
    }
    else if (!r && mailbox) {
        /* Open mailbox for writing */
        r = mailbox_open_iwl(mbentry->name, mailbox);
        if (r) {
            syslog(LOG_ERR, "mailbox_open_iwl(%s) failed: %s",
                   mbentry->name, error_message(r));
        }
    }

 done:
    mboxname_release(&namespacelock);
    mboxlist_entry_free(&mbentry);
    return r;
}

static int create_notify_collection(const char *userid, struct mailbox **mailbox)
{
    /* notifications collection */
    mbentry_t *mbentry = NULL;
    int r = lookup_notify_collection(userid, &mbentry);
    if (r) {
        mboxlist_entry_free(&mbentry);
        return _create_notify_collection(userid, mailbox);
    }

    if (mailbox) {
        /* Open mailbox for writing */
        r = mailbox_open_iwl(mbentry->name, mailbox);
        if (r) {
            syslog(LOG_ERR, "mailbox_open_iwl(%s) failed: %s",
                   mbentry->name, error_message(r));
        }
    }

    mboxlist_entry_free(&mbentry);
    return r;
}

static int my_dav_auth(const char *userid)
{
    if (httpd_userisadmin || httpd_userisanonymous ||
        global_authisa(httpd_authstate, IMAPOPT_PROXYSERVERS)) {
        /* admin, anonymous, or proxy from frontend - won't have DAV database */
        return 0;
    }
    else if (config_mupdate_server && !config_getstring(IMAPOPT_PROXYSERVERS)) {
        /* proxy-only server - won't have DAV databases */
        return 0;
    }
    else {
        /* Open WebDAV DB for 'userid' */
        my_dav_reset();
        auth_webdavdb = webdav_open_userid(userid);
        if (!auth_webdavdb) {
            syslog(LOG_ERR, "Unable to open WebDAV DB for userid: %s", userid);
            return HTTP_UNAVAILABLE;
        }
    }

    /* Auto-provision a notifications collection for 'userid' */
    create_notify_collection(userid, NULL);

    return 0;
}


static void my_dav_reset(void)
{
    if (auth_webdavdb) webdav_close(auth_webdavdb);
    auth_webdavdb = NULL;
}


static void my_dav_shutdown(void)
{
    my_dav_reset();
    webdav_done();
}


/* Perform a GET/HEAD request on a WebDAV notification resource */
static int notify_get(struct transaction_t *txn, struct mailbox *mailbox,
                      struct index_record *record, void *data,
                      void **obj __attribute__((unused)))
{
    const char **hdr;
    struct webdav_data *wdata = (struct webdav_data *) data;
    struct dlist *dl = NULL, *al;
    const char *type_str;
    struct buf msg_buf = BUF_INITIALIZER;
    struct buf inbuf, *outbuf = NULL;
    xmlDocPtr indoc = NULL, outdoc;
    xmlNodePtr notify = NULL, root, node, type;
    xmlNodePtr resp = NULL, sharedurl = NULL, node2;
    xmlNsPtr ns[NUM_NAMESPACE];
    xmlChar *dtstamp = NULL, *comment = NULL;
    char datestr[RFC3339_DATETIME_MAX];
    time_t t;
    enum {
        SYSTEM_STATUS,
        SHARE_INVITE,
        SHARE_REPLY
    } notify_type;
    int r, ret = 0;

    if (!record || !record->uid) return HTTP_NO_CONTENT;

    if ((hdr = spool_getheader(txn->req_hdrs, "Accept")) &&
        is_mediatype(DAVSHARING_CONTENT_TYPE, hdr[0])) {
        return HTTP_CONTINUE;
    }

    /* If no Accept header is given or its not application/davsharing+xml,
       assume its a legacy notification client and do a mime type translation
       from application/davnotification+xml to application/xml */

    /* Parse dlist representing notification type, and data */
    dlist_parsemap(&dl, 1, 0, wdata->filename, strlen(wdata->filename));
    dlist_getatom(dl, "T", &type_str);
    dlist_getlist(dl, "D", &al);

    if (!strcmp(type_str, SYSTEM_STATUS_NOTIFICATION)) {
        notify_type = SYSTEM_STATUS;
    }
    else if (!strcmp(type_str, SHARE_INVITE_NOTIFICATION)) {
        notify_type = SHARE_INVITE;
    }
    else if (!strcmp(type_str, SHARE_REPLY_NOTIFICATION)) {
        notify_type = SHARE_REPLY;
    }
    else {
        ret = HTTP_NOT_ACCEPTABLE;
        goto done;
    }

    txn->resp_body.type = "application/xml";

    if (txn->meth == METH_HEAD) {
        ret = HTTP_CONTINUE;
        goto done;
    }


    /* Load message containing the resource */
    r = mailbox_map_record(mailbox, record, &msg_buf);
    if (r) {
        txn->error.desc = error_message(r);
        return HTTP_SERVER_ERROR;
    }

    /* Parse message body into XML tree */
    buf_init_ro(&inbuf, buf_base(&msg_buf) + record->header_size,
                record->size - record->header_size);
    indoc = to_xml(&inbuf);
    buf_free(&inbuf);
    buf_free(&msg_buf);

    root = xmlDocGetRootElement(indoc);
    node = xmlFirstElementChild(root);
    dtstamp = xmlNodeGetContent(node);
    type = xmlNextElementSibling(node);

    /* Translate DAV notification into CS notification */
    notify = init_xml_response("notification", NS_CS, NULL, ns);
    outdoc = notify->doc;

    /* Calendar.app doesn't like separators in date-time */
    time_from_iso8601((const char *) dtstamp, &t);
    time_to_iso8601(t, datestr, RFC3339_DATETIME_MAX, 0);
    xmlNewChild(notify, NULL, BAD_CAST "dtstamp", BAD_CAST datestr);

    if (notify_type == SYSTEM_STATUS) {
    }
    else if (notify_type == SHARE_INVITE) {
        xmlNodePtr invite, sharer = NULL, access = NULL, calcompset = NULL;
        xmlChar *name = NULL;
        struct buf buf = BUF_INITIALIZER;

        /* Grab DAV elements that we need to construct CS notification */
        for (node = xmlFirstElementChild(type); node;
             node = xmlNextElementSibling(node)) {
            if (!xmlStrncmp(node->name, BAD_CAST "invite-", 7)) {
                resp = node;
            }
            else if (!xmlStrcmp(node->name, BAD_CAST "sharer-resource-uri")) {
                sharedurl = xmlFirstElementChild(node);
            }
            else if (!xmlStrcmp(node->name, BAD_CAST "principal")) {
                sharer = xmlFirstElementChild(node);
                node2 = xmlNextElementSibling(sharer);
                if (!xmlStrcmp(node2->name, BAD_CAST "prop")) {
                    for (node2 = xmlFirstElementChild(node2); node2;
                         node2 = xmlNextElementSibling(node2)) {
                        if (!xmlStrcmp(node2->name, BAD_CAST "displayname")) {
                            name = xmlNodeGetContent(node2);
                        }
                    }
                }
            }
            else if (!xmlStrcmp(node->name, BAD_CAST "share-access")) {
                access = xmlFirstElementChild(node);
            }
            else if (!xmlStrcmp(node->name, BAD_CAST "comment")) {
                comment = xmlNodeGetContent(node);
            }
            else if (!xmlStrcmp(node->name, BAD_CAST "prop")) {
                for (node2 = xmlFirstElementChild(node); node2;
                     node2 = xmlNextElementSibling(node2)) {
                    if (!xmlStrcmp(node2->name,
                                   BAD_CAST "supported-calendar-component-set")) {
                        calcompset = node2;
                    }
                }
            }
        }

        invite = xmlNewChild(notify, NULL, BAD_CAST "invite-notification", NULL);

        xmlNewChild(invite, NULL, BAD_CAST "uid", BAD_CAST wdata->dav.resource);

        /* Sharee href */
        buf_reset(&buf);
        buf_printf(&buf, "%s/%s/%s/", namespace_principal.prefix,
                   USER_COLLECTION_PREFIX, txn->req_tgt.userid);
        node = xml_add_href(invite, NULL, buf_cstring(&buf));
        ensure_ns(ns, NS_DAV, node, XML_NS_DAV, "D");
        xmlSetNs(node, ns[NS_DAV]);

#if 0  /* XXX  Apple clients seem to always want "noresponse" */
        xmlNewChild(invite, NULL, resp->name, NULL);
#else
        xmlNewChild(invite, NULL, BAD_CAST "invite-noresponse", NULL);
#endif

        node = xmlNewChild(invite, NULL, BAD_CAST "access", NULL);
        xmlNewChild(node, NULL, access->name, NULL);
        node = xmlNewChild(invite, NULL, BAD_CAST "hosturl", NULL);
        xmlAddChild(node, xmlCopyNode(sharedurl, 1));
        node = xmlNewChild(invite, NULL, BAD_CAST "organizer", NULL);
        xmlAddChild(node, xmlCopyNode(sharer, 1));
        if (name) {
            xmlNewChild(node, NULL, BAD_CAST "common-name", name);
            xmlFree(name);
        }

        if (comment) {
            xmlNewChild(invite, NULL, BAD_CAST "summary", comment);
            xmlFree(comment);
        }
        if (calcompset) {
            xmlAddChild(invite, xmlCopyNode(calcompset, 1));
        }

        buf_free(&buf);
    }
    else if (notify_type == SHARE_REPLY) {
        xmlNodePtr reply, sharee = NULL;

        /* Grab DAV elements that we need to construct CS notification */
        for (node = xmlFirstElementChild(type); node;
             node = xmlNextElementSibling(node)) {
            if (!xmlStrcmp(node->name, BAD_CAST "sharee")) {
                for (node2 = xmlFirstElementChild(node); node2;
                     node2 = xmlNextElementSibling(node2)) {
                    if (!xmlStrcmp(node2->name, BAD_CAST "href")) {
                        sharee = node2;
                    }
                    else if (!xmlStrncmp(node2->name, BAD_CAST "invite-", 7)) {
                        resp = node2;
                    }
                }
            }
            else if (!xmlStrcmp(node->name, BAD_CAST "href")) {
                sharedurl = node;
            }
            else if (!xmlStrcmp(node->name, BAD_CAST "comment")) {
                comment = xmlNodeGetContent(node);
            }
        }

        reply = xmlNewChild(notify, NULL, BAD_CAST "invite-reply", NULL);

        xmlAddChild(reply, xmlCopyNode(sharee, 1));
        xmlNewChild(reply, NULL, resp->name, NULL);
        node = xmlNewChild(reply, NULL, BAD_CAST "hosturl", NULL);
        xmlAddChild(node, xmlCopyNode(sharedurl, 1));

        xmlNewChild(reply, NULL,
                    BAD_CAST "in-reply-to", BAD_CAST wdata->dav.resource);

        if (comment) {
            xmlNewChild(reply, NULL, BAD_CAST "summary", comment);
            xmlFree(comment);
        }
    }
    else {
        /* Unknown type - return as-is */
        xmlFreeDoc(notify->doc);
        notify = NULL;
        outdoc = indoc;
    }

    /* Dump XML tree into a text buffer */
    outbuf = from_xml(outdoc);

    write_body(HTTP_OK, txn, buf_cstring(outbuf), buf_len(outbuf));

  done:
    if (dtstamp) xmlFree(dtstamp);
    if (notify) xmlFreeDoc(notify->doc);
    if (indoc) xmlFreeDoc(indoc);
    buf_destroy(outbuf);
    dlist_free(&dl);

    return ret;
}


HIDDEN int dav_send_notification(xmlDocPtr doc,
                                 const char *userid, const char *resource)
{
    struct mailbox *mailbox = NULL;
    struct webdav_db *webdavdb = NULL;
    struct transaction_t txn;
    int r;

    /* XXX  Need to find location of user.
       If remote need to do a PUT or possibly email */

    /* Open notifications collection for writing */
    r = create_notify_collection(userid, &mailbox);
    if (r == IMAP_INVALID_USER) {
        syslog(LOG_NOTICE,
               "dav_send_notification(%s) failed: %s", userid, error_message(r));
        return 0;
    }
    else if (r) {
        syslog(LOG_ERR,
               "dav_send_notification: create_notify_collection(%s) failed: %s",
               userid, error_message(r));
        return r;
    }

    /* Open the WebDAV DB corresponding to collection */
    webdavdb = webdav_open_mailbox(mailbox);
    if (!webdavdb) {
        syslog(LOG_ERR, "dav_send_notification: unable to open WebDAV DB (%s)",
               mailbox->name);
        r = HTTP_SERVER_ERROR;
        goto done;
    }

    /* Start with an empty (clean) transaction */
    memset(&txn, 0, sizeof(struct transaction_t));

    /* Create header cache */
    if (!(txn.req_hdrs = spool_new_hdrcache())) {
        syslog(LOG_ERR, "dav_send_notification: unable to create header cache");
        r = HTTP_SERVER_ERROR;
        goto done;
    }

    spool_cache_header(xstrdup("Content-Type"),
                       xstrdup(DAVNOTIFICATION_CONTENT_TYPE), txn.req_hdrs);

    r = notify_put(&txn, doc, mailbox, resource, webdavdb, 0);
    if (r != HTTP_CREATED && r != HTTP_NO_CONTENT) {
        syslog(LOG_ERR,
               "dav_send_notification: notify_put(%s, %s) failed: %s",
               mailbox->name, resource, error_message(r));
    }

  done:
    spool_free_hdrcache(txn.req_hdrs);
    buf_free(&txn.buf);
    webdav_close(webdavdb);
    mailbox_close(&mailbox);

    return r;
}


/* Perform a POST request on a WebDAV notification resource */
HIDDEN int notify_post(struct transaction_t *txn)
{
    xmlNodePtr root = NULL, node, resp = NULL;
    int rights, ret, r, legacy = 0, add = 0;
    struct mailbox *shared = NULL;
    struct webdav_db *webdavdb = NULL;
    struct webdav_data *wdata;
    struct dlist *dl = NULL, *data;
    const char *type_str, *mboxname, *url_prefix;
    char dtstamp[RFC3339_DATETIME_MAX], *resource = NULL;
    xmlNodePtr notify = NULL, type, sharee;
    xmlNsPtr ns[NUM_NAMESPACE];
    xmlChar *comment = NULL, *freeme = NULL;
    mbname_t *mbname = NULL;

    /* Check ACL for current user */
    rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry);
    if (!(rights & DACL_ADMIN)) {
        /* DAV:need-privileges */
        txn->error.precond = DAV_NEED_PRIVS;
        txn->error.resource = txn->req_tgt.path;
        txn->error.rights = DACL_ADMIN;
        return HTTP_NO_PRIVS;
    }

    if (txn->req_tgt.mbentry->server) {
        /* Remote mailbox */
        struct backend *be;

        be = proxy_findserver(txn->req_tgt.mbentry->server,
                              &http_protocol, httpd_userid,
                              &backend_cached, NULL, NULL, httpd_in);
        if (!be) return HTTP_UNAVAILABLE;

        return http_pipe_req_resp(be, txn);
    }

    /* Local Mailbox */

    /* Read body */
    ret = parse_xml_body(txn, &root, DAVSHARING_CONTENT_TYPE);
    if (!ret && !root) {
        txn->error.desc = "Missing request body";
        ret = HTTP_BAD_REQUEST;
    }
    if (ret) goto done;

    /* Make sure its a invite-reply element */
    if (xmlStrcmp(root->name, BAD_CAST "invite-reply")) {
        txn->error.desc =
            "Missing invite-reply element in POST request";
        ret = HTTP_BAD_REQUEST;
        goto done;
    }

    resource = txn->req_tgt.resource;
    if (!resource) legacy = 1;

    /* Fetch needed elements */
    for (node = xmlFirstElementChild(root); node;
         node = xmlNextElementSibling(node)) {
        if (!xmlStrncmp(node->name, BAD_CAST "invite-", 7)) {
            if (!xmlStrcmp(node->name, BAD_CAST "invite-accepted")) add = 1;
            resp = node;
        }
        else if (legacy) {
            if (!xmlStrcmp(node->name, BAD_CAST "in-reply-to")) {
                freeme = xmlNodeGetContent(node);
                resource = (char *) freeme;
            }
            else if (!xmlStrcmp(node->name, BAD_CAST "summary")) {
                comment = xmlNodeGetContent(node);
            }
        }
        else if (!xmlStrcmp(node->name, BAD_CAST "comment")) {
            comment = xmlNodeGetContent(node);
        }
    }

    if (!resp) {
        txn->error.desc = "Missing invite response element in POST request";
        ret = HTTP_BAD_REQUEST;
        goto done;
    }
    if (!resource) {
        txn->error.desc = "Missing in-reply-to element in POST request";
        ret = HTTP_BAD_REQUEST;
        goto done;
    }

    if (legacy) {
        /* Locate notification mailbox for target user */
        mboxlist_entry_free(&txn->req_tgt.mbentry);
        txn->req_tgt.mbentry = NULL;
        r = lookup_notify_collection(txn->req_tgt.userid, &txn->req_tgt.mbentry);
        if (r) {
            syslog(LOG_ERR, "lookup_notify_collection(%s) failed: %s",
                   txn->req_tgt.userid, error_message(r));
            txn->error.desc = error_message(r);
            ret = HTTP_SERVER_ERROR;
            goto done;
        }
    }

    /* Open the WebDAV DB corresponding to the mailbox */
    webdavdb = webdav_open_userid(txn->req_tgt.userid);

    /* Find message UID for the resource */
    webdav_lookup_resource(webdavdb, txn->req_tgt.mbentry->name,
                           resource, &wdata, 0);
    if (!wdata->dav.imap_uid) {
        ret = HTTP_NOT_FOUND;
        goto done;
    }

    /* Parse dlist representing notification type, and data */
    dlist_parsemap(&dl, 1, 0, wdata->filename, strlen(wdata->filename));
    dlist_getatom(dl, "T", &type_str);
    if (strcmp(type_str, SHARE_INVITE_NOTIFICATION)) {
        ret = HTTP_NOT_ALLOWED;
        goto done;
    }

    dlist_getlist(dl, "D", &data);
    dlist_getatom(data, "M", &mboxname);

    /* [Un]subscribe */
    r = mboxlist_changesub(mboxname, txn->req_tgt.userid,
                           httpd_authstate, add, 0, 0);
    if (r) {
        syslog(LOG_ERR, "mboxlist_changesub(%s, %s) failed: %s",
               mboxname, txn->req_tgt.userid, error_message(r));
        txn->error.desc = error_message(r);
        ret = HTTP_SERVER_ERROR;
        goto done;
    }

    /* Set invite status */
    r = mailbox_open_iwl(mboxname, &shared);
    if (r) {
        syslog(LOG_ERR, "IOERROR: failed to open mailbox %s for share reply",
               mboxname);
    }
    else {
        annotate_state_t *astate = NULL;

        r = mailbox_get_annotate_state(shared, 0, &astate);
        if (!r) {
            const char *annot = DAV_ANNOT_NS "<" XML_NS_DAV ">invite-status";
            struct buf value = BUF_INITIALIZER;

            buf_init_ro_cstr(&value, (char *) resp->name);
            r = annotate_state_writemask(astate, annot,
                                         txn->req_tgt.userid, &value);

            if (shared->mbtype == MBTYPE_CALENDAR) {
                /* Sharee's copy of calendar SHOULD default to transparent */
                annot =
                    DAV_ANNOT_NS "<" XML_NS_CALDAV ">schedule-calendar-transp";
                buf_init_ro_cstr(&value, "transparent");
                r = annotate_state_writemask(astate, annot,
                                             txn->req_tgt.userid, &value);
            }
        }

        mailbox_close(&shared);
    }

    /* Create share-reply-notification */
    notify = init_xml_response("notification", NS_DAV, NULL, ns);

    time_to_rfc3339(time(0), dtstamp, RFC3339_DATETIME_MAX);
    xmlNewChild(notify, NULL, BAD_CAST "dtstamp", BAD_CAST dtstamp);

    type = xmlNewChild(notify, NULL, BAD_CAST SHARE_REPLY_NOTIFICATION, NULL);

    sharee = xmlNewChild(type, NULL, BAD_CAST "sharee", NULL);
    buf_reset(&txn->buf);
    buf_printf(&txn->buf, "%s/%s/%s/", namespace_principal.prefix,
               USER_COLLECTION_PREFIX, txn->req_tgt.userid);
    xml_add_href(sharee, NULL, buf_cstring(&txn->buf));

    node = xmlNewChild(sharee, NULL, resp->name, NULL);

    /* shared-url */
    url_prefix = strstr(mboxname, config_getstring(IMAPOPT_CALENDARPREFIX)) ?
        namespace_calendar.prefix : namespace_addressbook.prefix;

    mbname = mbname_from_intname(mboxname);
    if (!mbname_domain(mbname)) mbname_set_domain(mbname, httpd_extradomain);

    make_collection_url(&txn->buf, url_prefix, /*haszzzz*/0, mbname, "");

    xml_add_href(type, NULL, buf_cstring(&txn->buf));

    if (comment) {
        xmlNewChild(type, NULL, BAD_CAST "comment", comment);
        xmlFree(comment);
    }

    /* Create a resource name for the notifications -
       We use a consistent naming scheme so that multiple notifications
       of the same type for the same resource are coalesced (overwritten) */
    buf_reset(&txn->buf);
    buf_printf(&txn->buf, "%x-%x-%x-%x.xml",
               strhash(XML_NS_DAV), strhash(SHARE_REPLY_NOTIFICATION),
               strhash(mboxname), strhash(txn->req_tgt.userid));

    r = dav_send_notification(notify->doc,
                              mbname_userid(mbname), buf_cstring(&txn->buf));


    if (add) {
        /* Accepted - create URL of sharee's new collection */
        make_collection_url(&txn->buf, url_prefix, /*haszzzz*/0,
                            mbname, txn->req_tgt.userid);

        if (legacy) {
            /* Create CS:shared-as XML body */
            xmlNodePtr shared_as = init_xml_response("shared-as", NS_CS, NULL, ns);
            if (!shared_as) {
                ret = HTTP_SERVER_ERROR;
                goto done;
            }

            node = xml_add_href(shared_as, NULL, buf_cstring(&txn->buf));
            ensure_ns(ns, NS_DAV, node, XML_NS_DAV, "D");
            xmlSetNs(node, ns[NS_DAV]);
            xml_response(HTTP_OK, txn, shared_as->doc);
            xmlFreeDoc(shared_as->doc);
            ret = 0;
        }
        else {
            /* Add Location header */
            txn->location = buf_cstring(&txn->buf);
            ret = HTTP_CREATED;
        }
    }
    else {
        /* Declined */
        ret = HTTP_NO_CONTENT;
    }

  done:
    if (freeme) xmlFree(freeme);
    if (root) xmlFreeDoc(root->doc);
    if (notify) xmlFreeDoc(notify->doc);
    webdav_close(webdavdb);
    mbname_free(&mbname);
    dlist_free(&dl);

    return ret;
}


/* Perform a PUT request on a WebDAV notification resource */
static int notify_put(struct transaction_t *txn, void *obj,
                      struct mailbox *mailbox, const char *resource,
                      void *destdb, unsigned flags __attribute__((unused)))
{
    struct webdav_db *db = (struct webdav_db *)destdb;
    xmlDocPtr doc = (xmlDocPtr) obj;
    xmlNodePtr root, dtstamp, type = NULL, node;
    struct webdav_data *wdata;
    struct index_record *oldrecord = NULL, record;
    struct buf *xmlbuf;
    int r;

    /* Validate the data */
    if (!doc) return HTTP_FORBIDDEN;

    /* Find message UID for the resource */
    webdav_lookup_resource(db, mailbox->name, resource, &wdata, 0);

    if (wdata->dav.imap_uid) {
        /* Fetch index record for the resource */
        oldrecord = &record;
        mailbox_find_index_record(mailbox, wdata->dav.imap_uid, oldrecord);
    }

    /* Get type of notification */
    if ((root = xmlDocGetRootElement(doc)) &&
        (dtstamp = xmlFirstElementChild(root))) {
        type = xmlNextElementSibling(dtstamp);
    }

    /* Create and cache RFC 5322 header fields for resource */
    if (type) {
        struct buf buf = BUF_INITIALIZER;
        xmlChar *value;
        time_t t;
        struct dlist *dl, *al;
        xmlAttrPtr attr;

        spool_replace_header(xstrdup("Subject"),
                             xstrdup((char *) type->name), txn->req_hdrs);

        /* Create a dlist representing type, namespace, and attribute(s) */
        value = xmlNodeGetContent(dtstamp);
        time_from_iso8601((const char *) value, &t);
        xmlFree(value);

        dl = dlist_newkvlist(NULL, "N");
        dlist_setdate(dl, "S", t);
        dlist_setatom(dl, "NS", (char *) type->ns->href);
        dlist_setatom(dl, "T", (char *) type->name);

        /* Add any attributes */
        al = dlist_newkvlist(dl, "A");
        for (attr = type->properties; attr; attr = attr->next) {
            value = xmlNodeGetContent((xmlNodePtr) attr);
            dlist_setmap(al, (char *) attr->name,
                         (char *) value, xmlStrlen(value));
            xmlFree(value);
        }

        /* Add any additional data */
        al = dlist_newkvlist(dl, "D");
        if (!xmlStrcmp(type->name, BAD_CAST SHARE_INVITE_NOTIFICATION)) {
            for (node = xmlFirstElementChild(type); node;
                 node = xmlNextElementSibling(node)) {
                if (!xmlStrcmp(node->name, BAD_CAST "sharer-resource-uri")) {
                    struct request_target_t tgt;
                    struct meth_params *pparams;
                    const char *path, *errstr;
                    int i;

                    value = xmlNodeGetContent(xmlFirstElementChild(node));
                    path = (const char *) value;

                    /* Find the namespace of the requested resource */
                    for (i = 0; http_namespaces[i]; i++) {
                        size_t len;

                        /* Skip disabled namespaces */
                        if (!http_namespaces[i]->enabled) continue;

                        /* See if the prefix matches - terminated with NUL or '/' */
                        len = strlen(http_namespaces[i]->prefix);
                        if (!strncmp(path, http_namespaces[i]->prefix, len) &&
                            (!path[len] || (path[len] == '/') || !strcmp(path, "*"))) {
                            break;
                        }
                    }

                    memset(&tgt, 0, sizeof(struct request_target_t));
                    tgt.namespace = http_namespaces[i];
                    pparams =
                        (struct meth_params *) tgt.namespace->methods[METH_PUT].params;
                    tgt.flags = TGT_DAV_SHARED;  // prevent old-style sharing redirect
                    pparams->parse_path(path, &tgt, &errstr);
                    xmlFree(value);
                    free(tgt.userid);

                    dlist_setatom(al, "M", tgt.mbentry->name);

                    mboxlist_entry_free(&tgt.mbentry);
                    break;
                }
            }
        }

        dlist_printbuf(dl, 1, &buf);
        dlist_free(&dl);
        spool_replace_header(xstrdup("Content-Description"),
                             buf_release(&buf), txn->req_hdrs);
    }

    buf_reset(&txn->buf);
    buf_printf(&txn->buf, "<%s-" TIME_T_FMT "@%s>", resource, time(0), config_servername);
    spool_replace_header(xstrdup("Message-ID"),
                         buf_release(&txn->buf), txn->req_hdrs);

    buf_printf(&txn->buf, "attachment;\r\n\tfilename=\"%s\"", resource);
    spool_replace_header(xstrdup("Content-Disposition"),
                         buf_release(&txn->buf), txn->req_hdrs);

    /* Dump XML response tree into a text buffer */
    xmlbuf = from_xml(doc);
    if (!buf_len(xmlbuf)) r = HTTP_SERVER_ERROR;
    else {
        /* Store the resource */
        r = dav_store_resource(txn, buf_cstring(xmlbuf), buf_len(xmlbuf),
                               mailbox, oldrecord, wdata->dav.createdmodseq, NULL);
    }

    buf_destroy(xmlbuf);

    return r;
}


/* Determine allowed methods in notify namespace */
static unsigned long notify_allow_cb(struct request_target_t *tgt)
{
    unsigned long allow = tgt->namespace->allow;

    if (!tgt->resource) {
        allow &= ~(ALLOW_DELETE | ALLOW_POST);
    }

    return allow;
}


/* Parse request-target path in DAV notifications namespace */
static int notify_parse_path(const char *path, struct request_target_t *tgt,
                             const char **resultstr)
{
    char *p;
    size_t len;
    mbname_t *mbname = NULL;

    /* Make a working copy of target path */
    strlcpy(tgt->path, path, sizeof(tgt->path));
    tgt->tail = tgt->path + strlen(tgt->path);

    p = tgt->path;

    /* Sanity check namespace */
    len = strlen(namespace_notify.prefix);
    if (strlen(p) < len ||
        strncmp(namespace_notify.prefix, p, len) ||
        (path[len] && path[len] != '/')) {
        *resultstr = "Namespace mismatch request target path";
        return HTTP_FORBIDDEN;
    }

    tgt->mboxprefix = config_getstring(IMAPOPT_DAVNOTIFICATIONSPREFIX);

    /* Default to bare-bones Allow bits */
    tgt->allow &= ALLOW_READ_MASK;

    /* Skip namespace */
    p += len;
    if (!*p || !*++p) return 0;

    /* Check if we're in user space */
    len = strcspn(p, "/");
    if (!strncmp(p, USER_COLLECTION_PREFIX, len)) {
        p += len;
        if (!*p || !*++p) return 0;

        /* Get user id */
        len = strcspn(p, "/");
        tgt->userid = xstrndup(p, len);

        if (httpd_extradomain) {
            char *at = strchr(tgt->userid, '@');
            if (at && !strcmp(at+1, httpd_extradomain))
                *at = 0;
        }

        p += len;
        if (!*p || !*++p) goto done;
    }
    else return HTTP_NOT_FOUND;  /* need to specify a userid */


    /* Get resource */
    len = strcspn(p, "/");
    tgt->resource = p;
    tgt->reslen = len;

    p += len;

    if (*p) {
//      *resultstr = "Too many segments in request target path";
        return HTTP_NOT_FOUND;
    }

  done:
    /* Create mailbox name from the parsed path */

    mbname = mbname_from_userid(tgt->userid);
    mbname_push_boxes(mbname, config_getstring(IMAPOPT_DAVNOTIFICATIONSPREFIX));

    /* XXX - hack to allow @domain parts for non-domain-split users */
    if (httpd_extradomain) {
        /* not allowed to be cross domain */
        if (mbname_localpart(mbname) &&
            strcmpsafe(mbname_domain(mbname), httpd_extradomain))
        {
            mbname_free(&mbname);
            return HTTP_NOT_FOUND;
        }
        mbname_set_domain(mbname, NULL);
    }

    const char *mboxname = mbname_intname(mbname);

    if (*mboxname) {
        /* Locate the mailbox */
        int r = proxy_mlookup(mboxname, &tgt->mbentry, NULL, NULL);
        if (r) {
            syslog(LOG_ERR, "mlookup(%s) failed: %s",
                   mboxname, error_message(r));
            *resultstr = error_message(r);
            mbname_free(&mbname);

            switch (r) {
            case IMAP_PERMISSION_DENIED: return HTTP_FORBIDDEN;
            case IMAP_MAILBOX_NONEXISTENT: return HTTP_NOT_FOUND;
            default: return HTTP_SERVER_ERROR;
            }
        }
    }

    mbname_free(&mbname);

    /* Set proper Allow bits based on path components */
    tgt->allow |= ALLOW_ACL;

    if (tgt->resource) tgt->allow |= ALLOW_POST | ALLOW_DELETE;

    return 0;
}


struct userid_rights {
    long positive;
    long negative;
};

static void parse_acl(hash_table *table, const char *origacl)
{
    char *acl = xstrdupsafe(origacl);
    char *thisid, *rights, *nextid;

    for (thisid = acl; *thisid; thisid = nextid) {
        struct userid_rights *id_rights;
        int is_negative = 0;
        int mask;

        rights = strchr(thisid, '\t');
        if (!rights) {
            break;
        }
        *rights++ = '\0';

        nextid = strchr(rights, '\t');
        if (!nextid) {
            rights[-1] = '\t';
            break;
        }
        *nextid++ = '\0';

        if (*thisid == '-') {
            is_negative = 1;
            thisid++;
        }

        id_rights = hash_lookup(thisid, table);
        if (!id_rights) {
            id_rights = xzmalloc(sizeof(struct userid_rights));
            hash_insert(thisid, id_rights, table);
        }

        cyrus_acl_strtomask(rights, &mask);
        /* XXX and if strtomask fails? */
        if (is_negative) id_rights->negative |= mask;
        else id_rights->positive |= mask;
    }

    free(acl);
}

struct invite_rock {
    const char *owner;
    int is_shared;
    struct propfind_ctx *fctx;
    xmlNodePtr node;
    int legacy;
};

static void xml_add_sharee(const char *userid, void *data, void *rock)
{
    struct userid_rights *id_rights = (struct userid_rights *) data;
    struct invite_rock *irock = (struct invite_rock *) rock;
    int rights = id_rights->positive & ~id_rights->negative;
    struct auth_state *authstate;
    int isadmin;

    if ((rights & DACL_SHARE) != DACL_SHARE) return;  /* not shared */
    if (!strcmp(userid, irock->owner)) return;  /* user is owner */

    authstate = auth_newstate(userid);
    isadmin = global_authisa(authstate, IMAPOPT_ADMINS);
    auth_freestate(authstate);
    if (isadmin) return;  /* user is an admin */


    irock->is_shared = 1;

    if (irock->node) {
        xmlNodePtr sharee, access;
        const char *annot = DAV_ANNOT_NS "<" XML_NS_DAV ">invite-status";
        const char *resp = "invite-noresponse";
        struct buf value = BUF_INITIALIZER;
        int r;

        sharee = xmlNewChild(irock->node, NULL,
                             BAD_CAST (irock->legacy ? "user" : "sharee"), NULL);

        buf_reset(&irock->fctx->buf);
        if (strchr(userid, '@')) {
            buf_printf(&irock->fctx->buf, "mailto:%s", userid);
        }
        else {
            const char *domain = httpd_extradomain;
            if (!domain) domain = config_defdomain;
            if (!domain) domain = config_servername;

            buf_printf(&irock->fctx->buf, "mailto:%s@%s", userid, domain);
        }
        xml_add_href(sharee, irock->fctx->ns[NS_DAV],
                     buf_cstring(&irock->fctx->buf));

        /* Lookup invite status */
        r = annotatemore_lookupmask(irock->fctx->mbentry->name,
                                    annot, userid, &value);
        if (!r && buf_len(&value)) resp = buf_cstring(&value);
        xmlNewChild(sharee, NULL, BAD_CAST resp, NULL);
        buf_free(&value);

        access = xmlNewChild(sharee, NULL,
                             BAD_CAST (irock->legacy ? "access" :
                                       "share-access"), NULL);
        if ((rights & DACL_SHARERW) == DACL_SHARERW)
            xmlNewChild(access, NULL, BAD_CAST "read-write", NULL);
        else xmlNewChild(access, NULL, BAD_CAST "read", NULL);
    }
}


HIDDEN void xml_add_shareaccess(struct propfind_ctx *fctx,
                                xmlNodePtr resp, xmlNodePtr node, int legacy)
{
    if (mboxname_userownsmailbox(fctx->req_tgt->userid, fctx->mbentry->name)) {
        hash_table table;
        struct invite_rock irock = { fctx->req_tgt->userid, 0, NULL, NULL, 0 };

        construct_hash_table(&table, 10, 1);
        parse_acl(&table, fctx->mbentry->acl);
        hash_enumerate(&table, &xml_add_sharee, &irock);

        if (irock.is_shared) {
            xmlNsPtr ns = fctx->ns[NS_DAV];

            if (legacy) {
                ensure_ns(fctx->ns, NS_CS, resp->parent, XML_NS_CS, "CS");
                ns = fctx->ns[NS_CS];
            }
            xmlNewChild(node, ns, BAD_CAST "shared-owner", NULL);
        }
        else if (!legacy)
            xmlNewChild(node, NULL, BAD_CAST "not-shared", NULL);

        free_hash_table(&table, &free);
    }
    else if (legacy) {
        ensure_ns(fctx->ns, NS_CS, resp->parent, XML_NS_CS, "CS");
        xmlNewChild(node, fctx->ns[NS_CS], BAD_CAST "shared", NULL);
    }
    else {
        int rights = httpd_myrights(httpd_authstate, fctx->mbentry);

        if ((rights & DACL_SHARERW) == DACL_SHARERW)
            xmlNewChild(node, NULL, BAD_CAST "read-write", NULL);
        else
            xmlNewChild(node, NULL, BAD_CAST "read", NULL);
    }
}


/* Callback to fetch DAV:share-access */
HIDDEN int propfind_shareaccess(const xmlChar *name, xmlNsPtr ns,
                                struct propfind_ctx *fctx,
                                xmlNodePtr prop __attribute__((unused)),
                                xmlNodePtr resp,
                                struct propstat propstat[],
                                void *rock __attribute__((unused)))
{
    xmlNodePtr node;

    if (!fctx->mbentry) return HTTP_NOT_FOUND;

    node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV],
                        &propstat[PROPSTAT_OK], name, ns, NULL, 0);

    xml_add_shareaccess(fctx, resp, node, 0 /* legacy */);

    return 0;
}


/* Callback to fetch DAV:invite and CS:invite */
HIDDEN int propfind_invite(const xmlChar *name, xmlNsPtr ns,
                           struct propfind_ctx *fctx,
                           xmlNodePtr prop __attribute__((unused)),
                           xmlNodePtr resp __attribute__((unused)),
                           struct propstat propstat[], void *rock)
{
    struct invite_rock irock = { fctx->req_tgt->userid, 0 /* is_shared */,
                                 fctx, NULL, rock != 0 /* legacy */ };
    xmlNodePtr node;

    fctx->flags.cs_sharing = (rock != 0);

    if (!fctx->mbentry) return HTTP_NOT_FOUND;

    node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV],
                        &propstat[PROPSTAT_OK], name, ns, NULL, 0);
    irock.node = node;

    if (mboxname_userownsmailbox(fctx->req_tgt->userid, fctx->mbentry->name)) {
        hash_table table;

        construct_hash_table(&table, 10, 1);

        parse_acl(&table, fctx->mbentry->acl);
        hash_enumerate(&table, &xml_add_sharee, &irock);

        free_hash_table(&table, &free);
    }
    else {
        struct userid_rights id_rights = 
            { httpd_myrights(httpd_authstate, fctx->mbentry), 0 /* neg */};

        irock.owner = "";
        xml_add_sharee(fctx->req_tgt->userid, &id_rights, &irock);
    }

    return 0;
}


/* Callback to fetch DAV:sharer-resource-uri and CS:shared-url */
HIDDEN int propfind_sharedurl(const xmlChar *name, xmlNsPtr ns,
                              struct propfind_ctx *fctx,
                              xmlNodePtr prop __attribute__((unused)),
                              xmlNodePtr resp __attribute__((unused)),
                              struct propstat propstat[], void *rock)
{
    mbname_t *mbname;
    const strarray_t *boxes;
    int n, size;
    xmlNodePtr node;

    fctx->flags.cs_sharing = (rock != 0);

    mbname = mbname_from_intname(fctx->mailbox->name);

    if (!strcmpsafe(mbname_userid(mbname), fctx->req_tgt->userid)) {
        mbname_free(&mbname);
        return HTTP_NOT_FOUND;
    }

    buf_setcstr(&fctx->buf, fctx->req_tgt->namespace->prefix);

    if (mbname_localpart(mbname)) {
        const char *domain =
            mbname_domain(mbname) ? mbname_domain(mbname) : httpd_extradomain;

        buf_printf(&fctx->buf, "/%s/%s",
                   USER_COLLECTION_PREFIX, mbname_localpart(mbname));
        if (domain) buf_printf(&fctx->buf, "@%s", domain);
    }
    buf_putc(&fctx->buf, '/');

    boxes = mbname_boxes(mbname);
    size = strarray_size(boxes);
    for (n = 1; n < size; n++) {
        buf_appendcstr(&fctx->buf, strarray_nth(boxes, n));
        buf_putc(&fctx->buf, '/');
    }

    node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV],
                        &propstat[PROPSTAT_OK], name, ns, NULL, 0);

    xml_add_href(node, fctx->ns[NS_DAV], buf_cstring(&fctx->buf));

    mbname_free(&mbname);

    return 0;
}


/* Callback to fetch DAV:notification-URL and CS:notification-URL */
HIDDEN int propfind_notifyurl(const xmlChar *name, xmlNsPtr ns,
                              struct propfind_ctx *fctx,
                              xmlNodePtr prop,
                              xmlNodePtr resp __attribute__((unused)),
                              struct propstat propstat[],
                              void *rock __attribute__((unused)))
{
    xmlNodePtr node;

    if (!(namespace_principal.enabled && fctx->req_tgt->userid))
        return HTTP_NOT_FOUND;

    node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
                        name, ns, NULL, 0);

    buf_reset(&fctx->buf);
    buf_printf(&fctx->buf, "%s/%s/%s/", namespace_notify.prefix,
               USER_COLLECTION_PREFIX, fctx->req_tgt->userid);

    if ((fctx->mode == PROPFIND_EXPAND) && xmlFirstElementChild(prop)) {
        /* Return properties for this URL */
        expand_property(prop, fctx, &namespace_notify, buf_cstring(&fctx->buf),
                        &notify_parse_path, notify_props, node, 0);

    }
    else {
        /* Return just the URL */
        xml_add_href(node, fctx->ns[NS_DAV], buf_cstring(&fctx->buf));
    }

    return 0;
}


/* Callback to fetch DAV:resourcetype */
static int propfind_restype(const xmlChar *name, xmlNsPtr ns,
                            struct propfind_ctx *fctx,
                            xmlNodePtr prop __attribute__((unused)),
                            xmlNodePtr resp __attribute__((unused)),
                            struct propstat propstat[],
                            void *rock __attribute__((unused)))
{
    xmlNodePtr node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV],
                                   &propstat[PROPSTAT_OK], name, ns, NULL, 0);

    if (!fctx->record) {
        xmlNewChild(node, NULL, BAD_CAST "collection", NULL);

        if (fctx->req_tgt->userid) {
            xmlNewChild(node, NULL, BAD_CAST "notifications", NULL);

            ensure_ns(fctx->ns, NS_CS, resp->parent, XML_NS_CS, "CS");
            xmlNewChild(node, fctx->ns[NS_CS], BAD_CAST "notification", NULL);
        }
    }

    return 0;
}


/* Callback to fetch DAV:notificationtype */
static int propfind_notifytype(const xmlChar *name, xmlNsPtr ns,
                               struct propfind_ctx *fctx,
                               xmlNodePtr prop __attribute__((unused)),
                               xmlNodePtr resp __attribute__((unused)),
                               struct propstat propstat[], void *rock)
{
    struct webdav_data *wdata = (struct webdav_data *) fctx->data;
    xmlNodePtr node;
    xmlNsPtr type_ns = NULL;
    struct dlist *dl = NULL, *al, *item;
    const char *ns_href, *type;
    int i;

    if (!wdata->filename) return HTTP_NOT_FOUND;

    node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
                        name, ns, NULL, 0);

    /* Parse dlist representing notification type, namespace, and attributes */
    dlist_parsemap(&dl, 1, 0, wdata->filename, strlen(wdata->filename));
    dlist_getatom(dl, "T", &type);
    dlist_getatom(dl, "NS", &ns_href);
    dlist_getlist(dl, "A", &al);

    if (rock /* calendarserver-sharing */) {
        if (!strcmp(type, SHARE_INVITE_NOTIFICATION))
            type = "invite-notification";
        else if (!strcmp(type, SHARE_REPLY_NOTIFICATION))
            type = "invite-reply";
    }
    else {
        /* Check if we already have this ns-href, otherwise create a new one */
        type_ns = xmlSearchNsByHref(node->doc, node, BAD_CAST ns_href);
        if (!type_ns) {
            char prefix[20];
            snprintf(prefix, sizeof(prefix), "X%X", strhash(ns_href) & 0xffff);
            type_ns = xmlNewNs(node, BAD_CAST ns_href, BAD_CAST prefix);
        }
    }

    /* Create node for type */
    node = xmlNewChild(node, type_ns, BAD_CAST type, NULL);

    /* Add attributes */
    for (i = 0; (item = dlist_getchildn(al, i)); i++) {
        xmlNewProp(node, BAD_CAST item->name, BAD_CAST dlist_cstring(item));
    }

    dlist_free(&dl);

    return 0;
}


HIDDEN int propfind_csnotify_collection(struct propfind_ctx *fctx,
                                        xmlNodePtr props)
{
    struct propfind_ctx my_fctx;
    struct request_target_t tgt;
    const char *err = NULL;

    /* Populate our propfind context for notification collection */
    memset(&my_fctx, 0, sizeof(struct propfind_ctx));

    buf_printf(&my_fctx.buf, "%s/%s/%s/", namespace_notify.prefix,
               USER_COLLECTION_PREFIX, fctx->req_tgt->userid);

    memset(&tgt, 0, sizeof(struct request_target_t));
    tgt.namespace = &namespace_notify;
    notify_parse_path(buf_cstring(&my_fctx.buf), &tgt, &err);

    my_fctx.txn = fctx->txn;
    my_fctx.req_tgt = &tgt;
    my_fctx.mode = fctx->mode;
    my_fctx.depth = fctx->depth;
    my_fctx.prefer = fctx->prefer & PREFER_MIN;
    my_fctx.userid = httpd_userid;
    my_fctx.userisadmin = httpd_userisadmin;
    my_fctx.authstate = httpd_authstate;
    my_fctx.mbentry = NULL;
    my_fctx.mailbox = NULL;
    my_fctx.record = NULL;
    my_fctx.reqd_privs = DACL_READ;
    my_fctx.filter = NULL;
    my_fctx.filter_crit = NULL;
    my_fctx.open_db = notify_params.davdb.open_db;
    my_fctx.close_db = notify_params.davdb.close_db;
    my_fctx.lookup_resource = notify_params.davdb.lookup_resource;
    my_fctx.foreach_resource = notify_params.davdb.foreach_resource;
    my_fctx.proc_by_resource = &propfind_by_resource;
    my_fctx.elist = NULL;
    my_fctx.lprops = notify_params.propfind.lprops;
    my_fctx.root = fctx->root;
    my_fctx.ns = fctx->ns;
    my_fctx.ns_table = fctx->ns_table;
    my_fctx.ret = fctx->ret;
    my_fctx.xmlbuf = fctx->xmlbuf;

    /* Parse the list of properties and build a list of callbacks */
    preload_proplist(props, &my_fctx);

    /* Add response for target collection */
    propfind_by_collection(tgt.mbentry, &my_fctx);

    free(tgt.userid);
    mboxlist_entry_free(&tgt.mbentry);

    /* Free the entry list */
    free_entry_list(my_fctx.elist);

    buf_free(&my_fctx.buf);

    return 0;
}


static const char *access_types[] = { "no-access", "read", "read-write" };

static int set_share_access(const char *mboxname,
                            const char *userid, int access)
{
    char r, rightstr[100];

    /* Set access rights */
    rightstr[0] = (access == SHARE_READWRITE) ? '+' : '-';

    cyrus_acl_masktostr(DACL_SHARERW, rightstr+1);
    r = mboxlist_setacl(&httpd_namespace, mboxname, userid, rightstr,
                        httpd_userisadmin || httpd_userisproxyadmin,
                        httpd_userid, httpd_authstate);
    if (!r && access == SHARE_READONLY) {
        rightstr[0] = '+';
        cyrus_acl_masktostr(DACL_SHARE, rightstr+1);
        r = mboxlist_setacl(&httpd_namespace, mboxname, userid, rightstr,
                            httpd_userisadmin || httpd_userisproxyadmin,
                            httpd_userid, httpd_authstate);
    }

    return r;
}


static xmlNodePtr get_props(struct request_target_t *req_tgt,
                            const char *prop_names[],
                            xmlNodePtr root, xmlNsPtr ns[],
                            const struct prop_entry prop_list[])
{
    struct propstat propstat = { NULL, 0, 0 };
    struct propfind_ctx fctx;
    const struct prop_entry *prop;
    const char **name;
    xmlNodePtr node;

    memset(&fctx, 0, sizeof(struct propfind_ctx));
    fctx.req_tgt = req_tgt;
    fctx.mbentry = req_tgt->mbentry;
    fctx.root = root;
    fctx.ns = ns;

    for (prop = prop_list; prop->name; prop++) {
        for (name = prop_names; *name; name++) {
            if (!strcmp(*name, prop->name)) {
                prop->get(BAD_CAST prop->name, ns[NS_DAV],
                          &fctx, NULL, NULL, &propstat, NULL);
            }
        }
    }

    buf_free(&fctx.buf);

    node = propstat.root->children;
    xmlUnlinkNode(node);
    xmlFreeNode(propstat.root);

    return node;
}


HIDDEN int dav_create_invite(xmlNodePtr *notify, xmlNsPtr *ns,
                             struct request_target_t *tgt,
                             const struct prop_entry *live_props,
                             const char *sharee, int access,
                             xmlChar *content)
{
    static xmlNodePtr resp, share, comment, node;
    struct buf buf = BUF_INITIALIZER;
    const char *invite_principal_props[] = { "displayname", NULL };
    const char *invite_collection_props[] = { "displayname", "resourcetype",
                                              "supported-calendar-component-set",
                                              NULL };
    const char *annot = DAV_ANNOT_NS "<" XML_NS_DAV ">invite-status";
    const char *response = "invite-noresponse";
    int r;

    if (!*notify) {
        /* Create share-invite-notification -
           response and share-access will be replaced for each sharee */
        char dtstamp[RFC3339_DATETIME_MAX];
        xmlNodePtr type, princ;

        *notify = init_xml_response("notification", NS_DAV, NULL, ns);
        if (!*notify) return HTTP_SERVER_ERROR;

        time_to_rfc3339(time(0), dtstamp, RFC3339_DATETIME_MAX);
        xmlNewChild(*notify, NULL, BAD_CAST "dtstamp", BAD_CAST dtstamp);

        type = xmlNewChild(*notify, NULL,
                           BAD_CAST SHARE_INVITE_NOTIFICATION, NULL);

        princ = xmlNewChild(type, NULL, BAD_CAST "principal", NULL);
        buf_printf(&buf, "%s/%s/%s/", namespace_principal.prefix,
                   USER_COLLECTION_PREFIX, tgt->userid);
        xml_add_href(princ, NULL, buf_cstring(&buf));
        buf_free(&buf);

        node = get_props(tgt, invite_principal_props,
                         *notify, ns, princ_params.propfind.lprops);
        xmlAddChild(princ, node);

        resp = xmlNewChild(type, NULL, BAD_CAST "invite-noresponse", NULL);

        node = xmlNewChild(type, NULL, BAD_CAST "sharer-resource-uri", NULL);
        xml_add_href(node, NULL, tgt->path);

        node = xmlNewChild(type, NULL, BAD_CAST "share-access", NULL);
        share = xmlNewChild(node, NULL, BAD_CAST "no-access", NULL);

        node = get_props(tgt, invite_collection_props,
                         *notify, ns, live_props);
        xmlAddChild(type, node);

        comment = xmlNewChild(type, NULL, BAD_CAST "comment", NULL);
    }

    /* Lookup invite status */
    r = annotatemore_lookupmask(tgt->mbentry->name,
                                annot, sharee, &buf);
    if (!r && buf_len(&buf)) response = buf_cstring(&buf);

    /* Patch in response and share-access */
    node = xmlNewNode(ns[NS_DAV], BAD_CAST response);
    buf_free(&buf);
    xmlReplaceNode(resp, node);
    xmlFreeNode(resp);
    resp = node;

    node = xmlNewNode(ns[NS_DAV], BAD_CAST access_types[access]);
    xmlReplaceNode(share, node);
    xmlFreeNode(share);
    share = node;

    xmlNodeSetContent(comment, content ? content : BAD_CAST "");

    return 0;
}


HIDDEN int dav_post_share(struct transaction_t *txn, struct meth_params *pparams)
{
    xmlNodePtr root = NULL, node, sharee;
    int rights, ret, legacy = 0;
    struct buf resource = BUF_INITIALIZER;
    xmlNodePtr notify = NULL;
    xmlNsPtr ns[NUM_NAMESPACE];

    /* Check ACL for current user */
    rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry);
    if (!(rights & DACL_ADMIN)) {
        /* DAV:need-privileges */
        txn->error.precond = DAV_NEED_PRIVS;
        txn->error.resource = txn->req_tgt.path;
        txn->error.rights = DACL_ADMIN;
        return HTTP_NO_PRIVS;
    }

    if (txn->req_tgt.mbentry->server) {
        /* Remote mailbox */
        struct backend *be;

        be = proxy_findserver(txn->req_tgt.mbentry->server,
                              &http_protocol, httpd_userid,
                              &backend_cached, NULL, NULL, httpd_in);
        if (!be) return HTTP_UNAVAILABLE;

        return http_pipe_req_resp(be, txn);
    }

    /* Local mailbox */

    /* Read body */
    ret = parse_xml_body(txn, &root, DAVSHARING_CONTENT_TYPE);
    if (!ret && !root) {
        txn->error.desc = "Missing request body";
        ret = HTTP_BAD_REQUEST;
    }
    if (ret) goto done;

    /* Make sure its a share-resource element */
    if (!xmlStrcmp(root->name, BAD_CAST "share")) legacy = 1;
    else if (xmlStrcmp(root->name, BAD_CAST "share-resource")) {
        txn->error.desc =
            "Missing share-resource element in POST request";
        ret = HTTP_BAD_REQUEST;
        goto done;
    }

    /* Process each sharee */
    for (sharee = xmlFirstElementChild(root); sharee;
         sharee = xmlNextElementSibling(sharee)) {
        xmlChar *href = NULL, *content = NULL;
        int access = SHARE_READONLY;

        if (legacy) {
            if (!xmlStrcmp(sharee->name, BAD_CAST "remove")) {
                access = SHARE_NONE;
            }
            else if (xmlStrcmp(sharee->name, BAD_CAST "set")) continue;
        }
        else if (xmlStrcmp(sharee->name, BAD_CAST "sharee")) continue;

        for (node = xmlFirstElementChild(sharee); node;
             node = xmlNextElementSibling(node)) {
            if (!xmlStrcmp(node->name, BAD_CAST "href")) {
                href = xmlNodeGetContent(node);
                if (access == SHARE_NONE) break;
            }

            if (legacy) {
                if (!xmlStrcmp(node->name, BAD_CAST "read-write")) {
                    access = SHARE_READWRITE;
                }
                else if (!xmlStrcmp(node->name, BAD_CAST "summary")) {
                    content = xmlNodeGetContent(node);
                }
            }
            else if (!xmlStrcmp(node->name, BAD_CAST "share-access")) {
                xmlNodePtr share = xmlFirstElementChild(node);

                if (!xmlStrcmp(share->name, BAD_CAST "no-access")) {
                    access = SHARE_NONE;
                }
                else if (!xmlStrcmp(share->name, BAD_CAST "read-write")) {
                    access = SHARE_READWRITE;
                }
            }
            else if (!xmlStrcmp(node->name, BAD_CAST "comment")) {
                content = xmlNodeGetContent(node);
            }
        }

        if (href) {
            char *userid = NULL, *at;
            int r;

            if (!xmlStrncasecmp(href, BAD_CAST "mailto:", 7)) {
                userid = xstrdup((char *) href + 7);
                if ((at = strchr(userid, '@'))) {
                    if (!config_virtdomains || !strcmp(at+1, config_defdomain)){
                        *at = '\0';
                    }
                }
            }
            else if (!xmlStrncmp(href, BAD_CAST "DAV:", 4)) {
                if (!xmlStrcmp(href + 4, BAD_CAST "all")) {
                    userid = xstrdup("anyone");
                }
                else if (!xmlStrcmp(href + 4, BAD_CAST "unauthenticated")) {
                    userid = xstrdup("anonymous");
                }
                else if (!xmlStrcmp(href + 4, BAD_CAST "authenticated")) {
                    /* This needs to be done as anyone - anonymous */
                    r = set_share_access(txn->req_tgt.mbentry->name,
                                         "anyone", access);
                    if (r) {
                        syslog(LOG_NOTICE,
                               "failed to set share access for"
                               " 'anyone' on '%s': %s",
                               txn->req_tgt.mbentry->name, error_message(r));
                    }
                    else userid = xstrdup("-anonymous");
                }
            }
            else {
                const char *errstr = NULL;
                xmlURIPtr uri = parse_uri(METH_UNKNOWN,
                                          (const char *) href, 1, &errstr);

                if (uri) {
                    struct request_target_t principal;

                    memset(&principal, 0, sizeof(struct request_target_t));
                    r = princ_params.parse_path((const char *) uri->path,
                                                &principal, &errstr);
                    if (!r && principal.userid) userid = principal.userid;
                    else if (principal.userid) free(principal.userid);

                    xmlFreeURI(uri);
                }
            }

            if (!userid) {
                /* XXX  set invite-invalid ? */
                syslog(LOG_NOTICE, "could not parse userid from sharing href");
            }
            else {
                /* Set access rights */
                r = set_share_access(txn->req_tgt.mbentry->name,
                                     userid, access);
                if (r) {
                    syslog(LOG_NOTICE,
                           "failed to set share access for '%s' on '%s': %s",
                           userid, txn->req_tgt.mbentry->name,
                           error_message(r));
                }
                else {
                    /* Notify sharee */
                    r = dav_create_invite(&notify, ns, &txn->req_tgt,
                                          pparams->propfind.lprops,
                                          userid, access, content);

                    /* Create a resource name for the notifications -
                       We use a consistent naming scheme so that multiple
                       notifications of the same type for the same resource
                       are coalesced (overwritten) */
                    buf_reset(&resource);
                    buf_printf(&resource, "%x-%x-%x-%x.xml",
                               strhash(XML_NS_DAV),
                               strhash(SHARE_INVITE_NOTIFICATION),
                               strhash(txn->req_tgt.mbentry->name),
                               strhash(userid));

                    r = dav_send_notification(notify->doc,
                                              userid, buf_cstring(&resource));
                }

                free(userid);
            }

            xmlFree(href);
        }
        if (content) xmlFree(content);

        ret = HTTP_NO_CONTENT;
    }

  done:
    if (root) xmlFreeDoc(root->doc);
    if (notify) xmlFreeDoc(notify->doc);
    buf_free(&resource);

    return ret;
}
