/* 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.
 *
 */
/*
 * TODO:
 *
 *   - CALDAV:supported-calendar-component-set should be a bitmask in
 *     cyrus.index header Mailbox Options field
 *
 *   - CALDAV:schedule-calendar-transp should be a flag in
 *     cyrus.index header (Mailbox Options)
 *
 *   - DAV:creationdate should be added to cyrus.header since it only
 *     gets set at creation time
 *
 *   - Should add a last_metadata_update field to cyrus.index header
 *     for use in PROPFIND, PROPPATCH, and possibly REPORT.
 *     This would get updated any time a mailbox annotation, mailbox
 *     acl, or quota root limit is changed
 *
 *   - Should we use cyrus.index header Format field to indicate
 *     CalDAV mailbox?
 *
 */

#include <sysexits.h>

#include "annotate.h"
#include "acl.h"
#include "append.h"
#include "caldav_db.h"
#include "dlist.h"
#include "global.h"
#include "http_dav.h"
#include "http_dav_sharing.h"
#include "http_proxy.h"
#include "index.h"
#include "proxy.h"
#include "times.h"
#include "syslog.h"
#include "strhash.h"
#include "tok.h"
#include "user.h"
#include "util.h"
#include "version.h"
#include "xmalloc.h"
#include "xml_support.h"
#include "xstrlcat.h"
#include "xstrlcpy.h"
#include "xstrnchr.h"

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

#include <errno.h>
#include <libxml/uri.h>

static const struct dav_namespace_t {
    const char *href;
    const char *prefix;
} known_namespaces[] = {
    { XML_NS_DAV, "D" },
    { XML_NS_CALDAV, "C" },
    { XML_NS_CARDDAV, "C" },
    { XML_NS_ISCHED, NULL },
    { XML_NS_CS, "CS" },
    { XML_NS_MECOM, "MC" },
    { XML_NS_MOBME, "MM" },
    { XML_NS_CYRUS, "CY" },
    { XML_NS_USERFLAG, "UF" },
    { XML_NS_SYSFLAG, "SF" },
};

const struct match_type_t dav_match_types[] = {
    { "contains", MATCH_TYPE_CONTAINS },
    { "equals", MATCH_TYPE_EQUALS },
    { "starts-with", MATCH_TYPE_PREFIX },
    { "ends-with", MATCH_TYPE_SUFFIX },
    { NULL, 0 }
};

const struct collation_t dav_collations[] = {
    { "i;unicode-casemap", COLLATION_UNICODE },
    { "i;ascii-casemap", COLLATION_ASCII },
    { "i;octet", COLLATION_OCTET },
    { NULL, 0 }
};

static xmlChar *server_info = NULL;
static int server_info_size = 0;
static time_t server_info_lastmod = 0;
static struct buf server_info_token = BUF_INITIALIZER;
static struct buf server_info_link = BUF_INITIALIZER;

static void my_dav_init(struct buf *serverinfo);
static void my_dav_shutdown(void);

static int get_server_info(struct transaction_t *txn);

static unsigned long principal_allow_cb(struct request_target_t *tgt);
static int principal_parse_path(const char *path, struct request_target_t *tgt,
                                const char **resultstr);
static int propfind_principalname(const xmlChar *name, xmlNsPtr ns,
                                  struct propfind_ctx *fctx,
                                  xmlNodePtr prop, xmlNodePtr resp,
                                  struct propstat propstat[], void *rock);
static int proppatch_principalname(xmlNodePtr prop, unsigned set,
                                   struct proppatch_ctx *pctx,
                                   struct propstat propstat[],
                                   void *rock);
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_alturiset(const xmlChar *name, xmlNsPtr ns,
                              struct propfind_ctx *fctx,
                              xmlNodePtr prop, xmlNodePtr resp,
                              struct propstat propstat[], void *rock);

static int principal_search(const char *userid, void *rock);
static int report_prin_prop_search(struct transaction_t *txn,
                                   struct meth_params *rparams,
                                   xmlNodePtr inroot,
                                   struct propfind_ctx *fctx);
static int report_prin_search_prop_set(struct transaction_t *txn,
                                       struct meth_params *rparams,
                                       xmlNodePtr inroot,
                                       struct propfind_ctx *fctx);

static int allprop_cb(const char *mailbox __attribute__((unused)),
                      uint32_t uid __attribute__((unused)),
                      const char *entry,
                      const char *userid, const struct buf *attrib,
                      const struct annotate_metadata *mdata __attribute__((unused)),
                      void *rock);

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

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

    /* WebDAV ACL (RFC 3744) REPORTs */
    { "principal-property-search", NS_DAV, "multistatus",
      &report_prin_prop_search, 0, REPORT_ALLOW_PROPS | REPORT_DEPTH_ZERO },
    { "principal-search-property-set", NS_DAV, "principal-search-property-set",
      &report_prin_search_prop_set, 0, REPORT_DEPTH_ZERO },

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

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

    /* WebDAV (RFC 4918) properties */
    { "creationdate", NS_DAV,
      PROP_ALLPROP, NULL, NULL, NULL },
    { "displayname", NS_DAV,
      PROP_ALLPROP | PROP_COLLECTION,
      propfind_principalname, proppatch_principalname, NULL },
    { "getcontentlanguage", NS_DAV,
      PROP_ALLPROP, NULL, NULL, NULL },
    { "getcontentlength", NS_DAV,
      PROP_ALLPROP | PROP_COLLECTION,
      propfind_getlength, NULL, NULL },
    { "getcontenttype", NS_DAV,
      PROP_ALLPROP, NULL, NULL, NULL },
    { "getetag", NS_DAV,
      PROP_ALLPROP, NULL, NULL, NULL },
    { "getlastmodified", NS_DAV,
      PROP_ALLPROP, NULL, NULL, NULL },
    { "lockdiscovery", NS_DAV,
      PROP_ALLPROP | PROP_COLLECTION,
      propfind_lockdisc, NULL, NULL },
    { "resourcetype", NS_DAV,
      PROP_ALLPROP | PROP_COLLECTION,
      propfind_restype, NULL, NULL },
    { "supportedlock", NS_DAV,
      PROP_ALLPROP | PROP_COLLECTION,
      propfind_suplock, NULL, NULL },

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

    /* WebDAV ACL (RFC 3744) properties */
    { "alternate-URI-set", NS_DAV,
      PROP_COLLECTION,
      propfind_alturiset, NULL, NULL },
    { "principal-URL", NS_DAV,
      PROP_COLLECTION,
      propfind_principalurl, NULL, NULL },
    { "group-member-set", NS_DAV,
      PROP_COLLECTION,
      NULL, NULL, NULL },
    { "group-membership", NS_DAV,
      PROP_COLLECTION,
      NULL, NULL, NULL },
    { "principal-collection-set", NS_DAV,
      PROP_COLLECTION,
      propfind_princolset, NULL, NULL },

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

    /* CalDAV (RFC 4791) properties */
    { "calendar-home-set", NS_CALDAV,
      PROP_COLLECTION,
      propfind_calurl, NULL, NULL },

    /* CalDAV Scheduling (RFC 6638) properties */
    { "schedule-inbox-URL", NS_CALDAV,
      PROP_COLLECTION,
      propfind_calurl, NULL, SCHED_INBOX },
    { "schedule-outbox-URL", NS_CALDAV,
      PROP_COLLECTION,
      propfind_calurl, NULL, SCHED_OUTBOX },
    { "calendar-user-address-set", NS_CALDAV,
      PROP_COLLECTION,
      propfind_caluseraddr, proppatch_caluseraddr, NULL },
    { "calendar-user-type", NS_CALDAV,
      PROP_COLLECTION,
      propfind_calusertype, NULL, NULL },

    /* CardDAV (RFC 6352) properties */
    { "addressbook-home-set", NS_CARDDAV,
      PROP_COLLECTION,
      propfind_abookhome, NULL, NULL },

    /* WebDAV Notifications (draft-pot-webdav-notifications) properties */
    { "notification-URL", NS_DAV,
      PROP_COLLECTION,
      propfind_notifyurl, NULL, NULL },

    /* Backwards compatibility with Apple notifications clients */
    { "notification-URL", NS_CS,
      PROP_COLLECTION,
      propfind_notifyurl, NULL, NULL },
    { "email-address-set", NS_CS,
      PROP_COLLECTION,
      propfind_caluseremail, NULL, NULL },

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


struct meth_params princ_params = {
    .parse_path = &principal_parse_path,
    .propfind = { 0, principal_props },
    .reports = principal_reports
};

/* Namespace for WebDAV principals */
struct namespace_t namespace_principal = {
    URL_NS_PRINCIPAL, 0, "principal", "/dav/principals", NULL,
    http_allow_noauth_get, /*authschemes*/0,
    /*mbtype */ 0,
    ALLOW_READ | ALLOW_DAV | ALLOW_PROPPATCH,
    &my_dav_init, NULL, NULL, &my_dav_shutdown, &dav_premethod,
    /*bearer*/NULL,
    {
        { NULL,                 NULL },                 /* ACL          */
        { NULL,                 NULL },                 /* BIND         */
        { NULL,                 NULL },                 /* CONNECT      */
        { NULL,                 NULL },                 /* COPY         */
        { NULL,                 NULL },                 /* DELETE       */
        { &meth_get_head,       &princ_params },        /* GET          */
        { &meth_get_head,       &princ_params },        /* HEAD         */
        { NULL,                 NULL },                 /* LOCK         */
        { NULL,                 NULL },                 /* MKCALENDAR   */
        { NULL,                 NULL },                 /* MKCOL        */
        { NULL,                 NULL },                 /* MOVE         */
        { &meth_options,        NULL },                 /* OPTIONS      */
        { NULL,                 NULL },                 /* PATCH        */
        { NULL,                 NULL },                 /* POST         */
        { &meth_propfind,       &princ_params },        /* PROPFIND     */
        { &meth_proppatch,      &princ_params },        /* PROPPATCH    */
        { NULL,                 NULL },                 /* PUT          */
        { &meth_report,         &princ_params },        /* REPORT       */
        { &meth_trace,          NULL },                 /* TRACE        */
        { NULL,                 NULL },                 /* UNBIND       */
        { NULL,                 NULL }                  /* UNLOCK       */
    }
};


/* Linked-list of properties for fetching */
struct propfind_entry_list {
    xmlChar *name;                      /* Property name (needs to be freed) */
    xmlNsPtr ns;                        /* Property namespace */
    unsigned char flags;                /* Flags for how/where prop apply */
    int (*get)(const xmlChar *name,     /* Callback to fetch property */
               xmlNsPtr ns, struct propfind_ctx *fctx, xmlNodePtr prop,
               xmlNodePtr resp, struct propstat propstat[], void *rock);
    xmlNodePtr prop;                    /* Property node from request */
    void *rock;                         /* Add'l data to pass to callback */
    struct propfind_entry_list *next;
};


/* Bitmask of privilege flags */
enum {
    PRIV_IMPLICIT =             (1<<0),
    PRIV_INBOX =                (1<<1),
    PRIV_OUTBOX =               (1<<2),
    PRIV_CONTAINED =            (1<<3)
};


/* Array of precondition/postcondition errors */
static const struct precond_t {
    const char *name;                   /* Property name */
    unsigned ns;                        /* Index into known namespace array */
} preconds[] = {
    /* Placeholder for zero (no) precondition code */
    { NULL, 0 },

    /* WebDAV (RFC 4918) preconditions */
    { "cannot-modify-protected-property", NS_DAV },
    { "lock-token-matches-request-uri", NS_DAV },
    { "lock-token-submitted", NS_DAV },
    { "no-conflicting-lock", NS_DAV },
    { "propfind-finite-depth", NS_DAV },

    /* WebDAV Versioning (RFC 3253) preconditions */
    { "supported-report", NS_DAV },
    { "resource-must-be-null", NS_DAV },

    /* WebDAV ACL (RFC 3744) preconditions */
    { "need-privileges", NS_DAV },
    { "no-invert", NS_DAV },
    { "no-abstract", NS_DAV },
    { "not-supported-privilege", NS_DAV },
    { "recognized-principal", NS_DAV },
    { "allowed-principal", NS_DAV },
    { "grant-only", NS_DAV },

    /* WebDAV Quota (RFC 4331) preconditions */
    { "quota-not-exceeded", NS_DAV },
    { "sufficient-disk-space", NS_DAV },

    /* WebDAV Extended MKCOL (RFC 5689) preconditions */
    { "valid-resourcetype", NS_DAV },

    /* WebDAV Sync (RFC 6578) preconditions */
    { "valid-sync-token", NS_DAV },
    { "number-of-matches-within-limits", NS_DAV },

    /* CalDAV (RFC 4791) preconditions */
    { "supported-calendar-data", NS_CALDAV },
    { "valid-calendar-data", NS_CALDAV },
    { "valid-calendar-object-resource", NS_CALDAV },
    { "supported-calendar-component", NS_CALDAV },
    { "calendar-collection-location-ok", NS_CALDAV },
    { "no-uid-conflict", NS_CALDAV },
    { "supported-filter", NS_CALDAV },
    { "valid-filter", NS_CALDAV },
    { "supported-collation", NS_CALDAV },

    /* RSCALE (RFC 7529) preconditions */
    { "supported-rscale", NS_CALDAV },

    /* Time Zones by Reference (RFC 7809) preconditions */
    { "valid-timezone", NS_CALDAV },

    /* Managed Attachments (draft-ietf-calext-caldav-attachments) preconditions */
    { "valid-managed-id", NS_CALDAV },

    /* Bulk Change (draft-daboo-calendarserver-bulk-change) preconditions */
    { "ctag-ok", NS_MECOM },

    /* CalDAV Scheduling (RFC 6638) preconditions */
    { "valid-scheduling-message", NS_CALDAV },
    { "valid-organizer", NS_CALDAV },
    { "unique-scheduling-object-resource", NS_CALDAV },
    { "same-organizer-in-all-components", NS_CALDAV },
    { "allowed-organizer-scheduling-object-change", NS_CALDAV },
    { "allowed-attendee-scheduling-object-change", NS_CALDAV },

    /* iSchedule (draft-desruisseaux-ischedule) preconditions */
    { "version-not-supported", NS_ISCHED },
    { "invalid-calendar-data-type", NS_ISCHED },
    { "invalid-calendar-data", NS_ISCHED },
    { "invalid-scheduling-message", NS_ISCHED },
    { "originator-missing", NS_ISCHED },
    { "too-many-originators", NS_ISCHED },
    { "originator-invalid", NS_ISCHED },
    { "originator-denied", NS_ISCHED },
    { "recipient-missing", NS_ISCHED },
    { "recipient-mismatch", NS_ISCHED },
    { "verification-failed", NS_ISCHED },

    /* CardDAV (RFC 6352) preconditions */
    { "supported-address-data", NS_CARDDAV },
    { "valid-address-data", NS_CARDDAV },
    { "no-uid-conflict", NS_CARDDAV },
    { "addressbook-collection-location-ok", NS_CARDDAV },
    { "supported-filter", NS_CARDDAV },
    { "supported-collation", NS_CARDDAV }
};


/* Check ACL on userid's principal (Inbox): ACL_LOOKUP right gives access */
static int principal_acl_check(const char *userid, struct auth_state *authstate)
{
    int r = 0;

    if (!httpd_userisadmin) {
        char *mboxname = caldav_mboxname(userid, NULL);
        mbentry_t *mbentry = NULL;

        r = http_mlookup(mboxname, &mbentry, NULL);
        if (r) {
            syslog(LOG_ERR, "mlookup(%s) failed: %s",
                   mboxname, error_message(r));
            r = HTTP_NOT_FOUND;
        }
        else if (!(httpd_myrights(authstate, mbentry) & ACL_LOOKUP)) {
            // allow READ (for owner) or USER6 (to grant access generally without anything else)
            r = HTTP_NOT_FOUND;
        }

        mboxlist_entry_free(&mbentry);
        free(mboxname);
    }

    return r;
}


/* Determine allowed methods in DAV principals namespace */
static unsigned long principal_allow_cb(struct request_target_t *tgt)
{
    return tgt->namespace->allow;
}


/* Parse request-target path in DAV principals namespace */
static int principal_parse_path(const char *path, struct request_target_t *tgt,
                                const char **resultstr)
{
    char *p;
    size_t len;

    /* 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_principal.prefix);
    if (strlen(p) < len ||
        strncmp(namespace_principal.prefix, p, len) ||
        (path[len] && path[len] != '/')) {
        *resultstr = "Namespace mismatch request target path";
        return HTTP_FORBIDDEN;
    }

    /* Skip past namespace (and any extra '/') */
    for (p += len; p[1] == '/'; p++);
    if (!*p || !*++p) {
        /* Make sure collection is terminated with '/' */
        if (p[-1] != '/') *p++ = '/';
        return 0;
    }

    /* Check if we're in user space */
    len = strcspn(p, "/");
    if (!strncmp(p, USER_COLLECTION_PREFIX, len)) {
        /* Skip past user prefix (and any extra '/') */
        for (p += len; p[1] == '/'; p++);
        if (!*p || !*++p) {
            /* Make sure collection is terminated with '/' */
            if (p[-1] != '/') *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;
        }

        /* Skip past userid (and any extra '/') */
        for (p += len; p[1] == '/'; p++);
        if (!*p || !*++p) goto mailbox;
    }
    else if (!strncmp(p, SERVER_INFO, len)) {
        p += len;
        if (!*p || !*++p) {
            tgt->flags = TGT_SERVER_INFO;
            return 0;
        }
    }
    else return HTTP_NOT_FOUND;  /* need to specify a userid */

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

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

    if (tgt->userid) {
        /* Locate the mailbox */
        char *mboxname = caldav_mboxname(tgt->userid, NULL);
        int r = http_mlookup(mboxname, &tgt->mbentry, NULL);

        if (r) {
            *resultstr = error_message(r);
            syslog(LOG_ERR, "mlookup(%s) failed: %s", mboxname, *resultstr);
        }
        free(mboxname);

        switch (r) {
        case 0:
            break;

        case IMAP_PERMISSION_DENIED:
            return HTTP_FORBIDDEN;

        case IMAP_MAILBOX_NONEXISTENT:
            return HTTP_NOT_FOUND;

        default:
            return HTTP_SERVER_ERROR;
        }
    }

    return 0;
}


/* Determine allowed methods in Cal/CardDAV namespace */
EXPORTED unsigned long calcarddav_allow_cb(struct request_target_t *tgt)
{
    unsigned long allow = tgt->namespace->allow;

    if (!tgt->userid) {
        allow &= ALLOW_READ_MASK;
    }
    else if (!tgt->collection) {
        allow &= ~(ALLOW_DELETE | ALLOW_PATCH | ALLOW_POST | ALLOW_WRITE);
    }
    else if (!tgt->resource) {
        allow &= ~(ALLOW_MKCOL | ALLOW_PATCH);
    }
    else {
        allow &= ~(ALLOW_MKCOL | ALLOW_POST);
    }

    return allow;
}


/* Parse request-target path in *DAV namespace */
EXPORTED int dav_parse_req_target(struct transaction_t *txn,
                                  struct meth_params *params)
{
    const char *resultstr = NULL;
    int r;

    r = params->parse_path(txn->req_uri->path, &txn->req_tgt, &resultstr);
    if (r) {
        if (r == HTTP_MOVED) txn->location = resultstr;
        else txn->error.desc = resultstr;
    }

    return r;
}


/* Parse a path in Cal/CardDAV namespace */
EXPORTED int calcarddav_parse_path(const char *path,
                                   struct request_target_t *tgt,
                                   const char *mboxprefix,
                                   const char **resultstr)
{
    char *p, *owner = NULL, *collection = NULL, *freeme = NULL;
    size_t len;
    const char *mboxname;
    mbname_t *mbname = NULL;
    int ret = 0;
    static struct buf redirect_buf = BUF_INITIALIZER;

    if (*tgt->path) return 0;  /* Already parsed */

    /* 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(tgt->namespace->prefix);
    if (strlen(p) < len ||
        strncmp(tgt->namespace->prefix, p, len) || (path[len] && path[len] != '/')) {
        *resultstr = "Namespace mismatch request target path";
        return HTTP_FORBIDDEN;
    }

    tgt->mboxprefix = mboxprefix;

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

    /* Skip past namespace (and any extra '/') */
    for (p += len; p[1] == '/'; p++);
    if (!*p || !*++p) return 0;

    /* Check if we're in user space */
    len = strcspn(p, "/");
    /* zzzz is part of the FastMail sorting hack to make shared collections
     * always appear later */
    if (!strncmp(p, USER_COLLECTION_PREFIX, len) || !strncmp(p, "zzzz", len)) {
        if (!strncmp(p, "zzzz", len))
            tgt->flags |= TGT_USER_ZZZZ;

        /* Skip past user prefix (and any extra '/') */
        for (p += len; p[1] == '/'; p++);
        if (!*p || !*++p) return 0;

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

        /* Skip past userid (and any extra '/') */
        for (p += len; p[1] == '/'; p++);
        if (!*p || !*++p) {
            /* Make sure home-set is terminated with '/' */
            if (p[-1] != '/') *p++ = '/';
            goto mailbox;
        }

        len = strcspn(p, "/");
    }

    /* Get collection */
    tgt->collection = p;
    tgt->collen = len;

    /* Skip past collection (and any extra '/') */
    for (p += len; p[1] == '/'; p++);
    if (!*p || !*++p) {
        /* Make sure collection is terminated with '/' */
        if (p[-1] != '/') *p++ = '/';
        goto mailbox;
    }

    /* 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;
    }

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

    owner = tgt->userid;
    if (tgt->collen) {
        collection = freeme = xstrndup(tgt->collection, tgt->collen);

        p = strrchr(collection, SHARED_COLLECTION_DELIM);
        if (p) {
            if (tgt->mbentry) { /* MKCOL or COPY/MOVE destination */
                *resultstr = "Invalid characters in collection name";
                ret = HTTP_FORBIDDEN;
                goto done;
            }
            else {
                /* Shared collection encoded as: <owner> "." <mboxname> */
                owner = collection;
                *p++ = '\0';
                collection = p;

                tgt->flags = TGT_DAV_SHARED;
                tgt->allow |= ALLOW_DELETE;
            }
        }
    }

    mbname = mbname_from_userid(owner);

    mbname_push_boxes(mbname, mboxprefix);
    if (collection) {
        mbname_push_boxes(mbname, collection);
    }

    /* 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)) {
            ret = HTTP_NOT_FOUND;
            goto done;
        }
        mbname_set_domain(mbname, NULL);
    }

    mboxname = mbname_intname(mbname);

    /* Check for FastMail legacy sharing URLs and redirect */
    if (httpd_userid && !config_getswitch(IMAPOPT_FASTMAILSHARING) &&
        tgt->flags != TGT_DAV_SHARED &&
        !mboxname_userownsmailbox(httpd_userid, mboxname)) {
        buf_reset(&redirect_buf);
        buf_printf(&redirect_buf, "%s/%s/%s/%s%c%s",
                 tgt->namespace->prefix, USER_COLLECTION_PREFIX,
                 httpd_userid, tgt->userid, SHARED_COLLECTION_DELIM,
                 tgt->collection);
        *resultstr = buf_cstring(&redirect_buf);

        ret = HTTP_MOVED;
        goto done;
    }

    if (tgt->mbentry) {
        /* Just return the mboxname (MKCOL or COPY/MOVE destination) */
        tgt->mbentry->name = xstrdup(mboxname);

        ret = mboxlist_createmailboxcheck(mboxname, 0, NULL, httpd_userisadmin,
                                          httpd_userid, httpd_authstate,
                                          NULL, NULL, 0 /* force */);
        if (ret) goto done;

        tgt->allow |= ALLOW_MKCOL;
    }
    else if (*mboxname) {
        /* Locate the mailbox */
        int r = http_mlookup(mboxname, &tgt->mbentry, NULL);

        if (r) {
            *resultstr = error_message(r);
            syslog(LOG_ERR, "mlookup(%s) failed: %s", mboxname, *resultstr);

            switch (r) {
            case IMAP_PERMISSION_DENIED:
                ret = HTTP_FORBIDDEN;
                break;

            case IMAP_MAILBOX_NONEXISTENT:
                ret = HTTP_NOT_FOUND;
                break;

            default:
                ret = HTTP_SERVER_ERROR;
                break;
            }

            goto done;
        }
    }

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

    if (tgt->collection) {
        tgt->allow |= ALLOW_WRITE | ALLOW_DELETE;

        if (!tgt->resource) tgt->allow |= ALLOW_POST;
    }
    else if (tgt->userid) tgt->allow |= ALLOW_MKCOL;

  done:
    mbname_free(&mbname);
    free(freeme);

    return ret;
}


EXPORTED int dav_get_validators(struct mailbox *mailbox, void *data,
                                const char *userid __attribute__((unused)),
                                struct index_record *record,
                                const char **etag, time_t *lastmod)
{
    const struct dav_data *ddata = (const struct dav_data *) data;

    memset(record, 0, sizeof(struct index_record));

    if (!ddata->alive) {
        /* New resource */
        if (etag) *etag = NULL;
        if (lastmod) *lastmod = 0;
    }
    else if (ddata->imap_uid) {
        /* Mapped URL */
        int r;

        /* Fetch index record for the resource */
        r = mailbox_find_index_record(mailbox, ddata->imap_uid, record);
        if (r) {
            syslog(LOG_ERR, "mailbox_find_index_record(%s, %u) failed: %s",
                   mailbox->name, ddata->imap_uid, error_message(r));
            return r;
        }

        if (etag) *etag = message_guid_encode(&record->guid);
        if (lastmod) *lastmod = record->internaldate;
    }
    else {
        /* Unmapped URL (empty resource) */
        if (etag) *etag = NULL;
        if (lastmod) *lastmod = ddata->creationdate;
    }

    return 0;
}


EXPORTED modseq_t dav_get_modseq(struct mailbox *mailbox __attribute__((unused)),
                                 void *data,
                                 const char *userid __attribute__((unused)))
{
    return ((struct dav_data *) data)->modseq;
}


/* Evaluate If header.  Note that we can't short-circuit any of the tests
   because we need to check for a lock-token anywhere in the header */
static int eval_list(char *list, struct mailbox *mailbox, const char *etag,
                     const char *lock_token, unsigned *locked)
{
    unsigned ret = 1;
    tok_t tok;
    char *cond;

    /* Process each condition, ANDing the results */
    tok_initm(&tok, list+1, "]>", TOK_TRIMLEFT|TOK_TRIMRIGHT);
    while ((cond = tok_next(&tok))) {
        unsigned r = 0, not = 0;

        if (!strncmp(cond, "Not", 3)) {
            not = 1;
            cond += 3;
            while (*cond == ' ') cond++;
        }
        if (*cond++ == '[') {
            /* ETag */
            r = !etagcmp(cond, etag);
        }
        else {
            /* State Token */
            r = !strcmpnull(cond, lock_token);
            if (r) {
                /* Correct lock-token has been provided */
                (*locked)--;
            }
            else if (mailbox) {
                struct buf buf = BUF_INITIALIZER;

                dav_get_synctoken(mailbox, &buf, SYNC_TOKEN_URL_SCHEME);
                r = !strcmp(cond, buf_cstring(&buf));
                if (!r) {
                    dav_get_synctoken(mailbox, &buf, XML_NS_MECOM "ctag/");
                    r = !strcmp(cond, buf_cstring(&buf));
                }
                buf_free(&buf);
            }
        }

        ret &= (not ? !r : r);
    }

    tok_fini(&tok);

    return ret;
}

static int eval_if(const char *hdr, struct meth_params *params,
                   const struct namespace_t *namespace,
                   struct mailbox *tgt_mailbox, const char *tgt_resource,
                   const char *tgt_etag, const char *tgt_lock_token,
                   unsigned *locked)
{
    unsigned ret = 0;
    tok_t tok;
    char *list;

    /* Process each list, ORing the results */
    tok_init(&tok, hdr, ")", TOK_TRIMLEFT|TOK_TRIMRIGHT);
    while ((list = tok_next(&tok))) {
        struct mailbox *mailbox, *my_mailbox = NULL;
        const char *etag, *lock_token;
        struct buf buf = BUF_INITIALIZER;
        struct index_record record;
        struct dav_data *ddata;
        void *davdb = NULL;

        if (*list == '<') {
            /* Tagged-list */
            const char *tag, *err;
            xmlURIPtr uri;

            tag = ++list;
            list = strchr(tag, '>');
            *list++ = '\0';

            mailbox = NULL;
            etag = lock_token = NULL;

            /* Parse the URL and assign mailbox, etag, and lock_token */
            if (params && (uri = parse_uri(METH_UNKNOWN, tag, 1, &err))) {
                struct request_target_t tag_tgt;
                int r;

                memset(&tag_tgt, 0, sizeof(struct request_target_t));
                tag_tgt.namespace = namespace;

                if (!params->parse_path(uri->path, &tag_tgt, &err)) {
                    if (tag_tgt.mbentry && !tag_tgt.mbentry->server) {
                        if (tgt_mailbox &&
                            !strcmp(tgt_mailbox->name, tag_tgt.mbentry->name)) {
                            /* Use target mailbox */
                            mailbox = tgt_mailbox;
                        }
                        else {
                            /* Open new mailbox */
                            r = mailbox_open_irl(tag_tgt.mbentry->name,
                                                 &my_mailbox);
                            if (r) {
                                syslog(LOG_NOTICE,
                                       "failed to open mailbox '%s'"
                                       " in tagged If header: %s",
                                       tag_tgt.mbentry->name, error_message(r));
                            }
                            mailbox = my_mailbox;
                        }
                        if (mailbox) {
                            if (!strcmpnull(tgt_resource, tag_tgt.resource)) {
                                /* Tag IS target resource */
                                etag = tgt_etag;
                                lock_token = tgt_lock_token;
                            }
                            else if (tag_tgt.resource) {
                                /* Open DAV DB corresponding to the mailbox */
                                davdb = params->davdb.open_db(mailbox);

                                /* Find message UID for the resource */
                                params->davdb.lookup_resource(davdb,
                                                              mailbox->name,
                                                              tag_tgt.resource,
                                                              (void **) &ddata,
                                                              0);
                                if (ddata->rowid) {
                                    if (ddata->lock_expire > time(NULL)) {
                                        lock_token = ddata->lock_token;
                                        (*locked)++;
                                    }

                                    memset(&record, 0,
                                           sizeof(struct index_record));
                                    if (ddata->imap_uid) {
                                        /* Mapped URL - Fetch index record */
                                        r = mailbox_find_index_record(mailbox,
                                                                      ddata->imap_uid,
                                                                      &record);
                                        if (r) {
                                            syslog(LOG_NOTICE,
                                                   "failed to fetch record for"
                                                   " '%s':%u in tagged"
                                                   " If header: %s",
                                                   mailbox->name,
                                                   ddata->imap_uid,
                                                   error_message(r));
                                        }
                                        else {
                                            etag =
                                                message_guid_encode(&record.guid);
                                        }
                                    }
                                    else {
                                        /* Unmapped URL (empty resource) */
                                        etag = NULL;
                                    }
                                }
                            }
                            else {
                                /* Collection */
                                buf_printf(&buf, "%u-%u-%u",
                                           mailbox->i.uidvalidity,
                                           mailbox->i.last_uid,
                                           mailbox->i.exists);
                                etag = buf_cstring(&buf);
                            }
                        }
                    }

                    mboxlist_entry_free(&tag_tgt.mbentry);
                    free(tag_tgt.userid);
                }

                xmlFreeURI(uri);
            }
        }
        else {
            /* No-tag-list */
            mailbox = tgt_mailbox;
            etag = tgt_etag;
            lock_token = tgt_lock_token;
        }

        list = strchr(list, '(');

        ret |= eval_list(list, mailbox, etag, lock_token, locked);

        if (davdb) params->davdb.close_db(davdb);
        mailbox_close(&my_mailbox);
        buf_free(&buf);
    }

    tok_fini(&tok);

    return (ret || *locked);
}


/* Check headers for any preconditions */
EXPORTED int dav_check_precond(struct transaction_t *txn,
                               struct meth_params *params,
                               struct mailbox *mailbox, const void *data,
                               const char *etag, time_t lastmod)
{
    const struct dav_data *ddata = (const struct dav_data *) data;
    hdrcache_t hdrcache = txn->req_hdrs;
    const char **hdr;
    const char *lock_token = NULL;
    unsigned locked = 0;

    /* Check for a write-lock on the source */
    if (ddata && ddata->lock_expire > time(NULL)) {
        lock_token = ddata->lock_token;

        switch (txn->meth) {
        case METH_DELETE:
        case METH_LOCK:
        case METH_MOVE:
        case METH_PATCH:
        case METH_POST:
        case METH_PUT:
            /* State-changing method: Only the lock owner can execute
               and MUST provide the correct lock-token in an If header */
            if (strcmp(ddata->lock_ownerid, httpd_userid)) return HTTP_LOCKED;

            locked = 1;
            break;

        case METH_UNLOCK:
            /* State-changing method: Authorized in meth_unlock() */
            break;

        case METH_ACL:
        case METH_MKCALENDAR:
        case METH_MKCOL:
        case METH_PROPPATCH:
            /* State-changing method: Locks on collections unsupported */
            break;

        default:
            /* Non-state-changing method: Always allowed */
            break;
        }
    }

    /* Per RFC 4918, If is similar to If-Match, but with lock-token submission.
       Per RFC 7232, LOCK errors supercede preconditions */
    if ((hdr = spool_getheader(hdrcache, "If"))) {
        /* State tokens (sync-token, lock-token) and Etags */
        if (!eval_if(hdr[0], params, txn->req_tgt.namespace,
                     mailbox, txn->req_tgt.resource,
                     etag, lock_token, &locked))
            return HTTP_PRECOND_FAILED;
    }

    if (locked) {
        /* Correct lock-token was not provided in If header */
        return HTTP_LOCKED;
    }


    /* Do normal HTTP checks */
    return check_precond(txn, etag, lastmod);
}


EXPORTED int dav_premethod(struct transaction_t *txn)
{
    if (buf_len(&server_info_link)) {
        /* Check for Server-Info-Token header */
        const char **hdr = spool_getheader(txn->req_hdrs, "Server-Info-Token");

        if ((hdr && strcmp(hdr[0], buf_cstring(&server_info_token))) ||
            (!hdr && txn->meth == METH_OPTIONS)) {
            strarray_append(&txn->resp_body.links,
                            buf_cstring(&server_info_link));
        }
    }

    return 0;
}


EXPORTED unsigned get_preferences(struct transaction_t *txn)
{
    unsigned mask = 0, prefs = 0;
    const char **hdr;

    /* Create a mask for preferences honored by method */
    switch (txn->meth) {
    case METH_COPY:
    case METH_MOVE:
    case METH_PATCH:
    case METH_POST:
    case METH_PUT:
        mask = PREFER_REP;
        break;

    case METH_GET:
    case METH_MKCALENDAR:
    case METH_MKCOL:
    case METH_PROPPATCH:
        mask = PREFER_MIN;
        break;

    case METH_PROPFIND:
    case METH_REPORT:
        mask = (PREFER_MIN | PREFER_NOROOT);
        break;
    }

    if (!mask) return 0;
    else {
        txn->flags.vary |= VARY_PREFER;
        if (mask & PREFER_MIN) txn->flags.vary |= VARY_BRIEF;
    }

    /* Check for Prefer header(s) */
    if ((hdr = spool_getheader(txn->req_hdrs, "Prefer"))) {
        int i;
        for (i = 0; hdr[i]; i++) {
            tok_t tok;
            char *token;

            tok_init(&tok, hdr[i], ",\r\n", TOK_TRIMLEFT|TOK_TRIMRIGHT);
            while ((token = tok_next(&tok))) {
                if ((mask & PREFER_MIN) &&
                    !strcmp(token, "return=minimal"))
                    prefs |= PREFER_MIN;
                else if ((mask & PREFER_REP) &&
                         !strcmp(token, "return=representation"))
                    prefs |= PREFER_REP;
                else if ((mask & PREFER_NOROOT) &&
                         !strcmp(token, "depth-noroot"))
                    prefs |= PREFER_NOROOT;
            }
            tok_fini(&tok);
        }

        txn->resp_body.prefs = prefs;
    }

    /* Check for Brief header */
    if ((mask & PREFER_MIN) &&
        (hdr = spool_getheader(txn->req_hdrs, "Brief")) &&
        !strcasecmp(hdr[0], "t")) {
        prefs |= PREFER_MIN;
    }

    /* Check for X-MobileMe-DAV-Options header */
    if ((mask & PREFER_REP) &&
        (hdr = spool_getheader(txn->req_hdrs, "X-MobileMe-DAV-Options")) &&
        !strcasecmp(hdr[0], "return-changed-data")) {
        prefs |= PREFER_REP;
    }

    return prefs;
}


/* Check requested MIME type */
struct mime_type_t *get_accept_type(const char **hdr, struct mime_type_t *types)
{
    struct mime_type_t *ret = NULL;
    struct accept *e, *enc = parse_accept(hdr);

    for (e = enc; e && e->token; e++) {
        if (!ret && e->qual > 0.0) {
            struct mime_type_t *m;

            for (m = types; !ret && m->content_type; m++) {
                if (is_mediatype(e->token, m->content_type)) ret = m;
            }
        }

        free(e->token);
    }
    if (enc) free(enc);

    return ret;
}


static void add_privs(int rights, unsigned flags,
                      xmlNodePtr parent, xmlNodePtr root, xmlNsPtr *ns);


/* Ensure that we have a given namespace.  If it doesn't exist in what we
 * parsed in the request, create it and attach to 'node'.
 */
int ensure_ns(xmlNsPtr *respNs, int ns, xmlNodePtr node,
              const char *url, const char *prefix)
{
    if (!respNs[ns]) {
        xmlNsPtr nsDef;
        char myprefix[20];

        /* Search for existing namespace using our prefix */
        for (nsDef = node->nsDef; nsDef; nsDef = nsDef->next) {
            if ((!nsDef->prefix && !prefix) ||
                (nsDef->prefix && prefix &&
                 !strcmp((const char *) nsDef->prefix, prefix))) break;
        }

        if (nsDef) {
            /* Prefix is already used - generate a new one */
            snprintf(myprefix, sizeof(myprefix), "X%X", strhash(url) & 0xffff);
            prefix = myprefix;
        }

        respNs[ns] = xmlNewNs(node, BAD_CAST url, BAD_CAST prefix);
    }

    /* XXX  check for errors */
    return 0;
}


/* Add namespaces declared in the request to our root node and Ns array */
static int xml_add_ns(xmlNodePtr req, xmlNsPtr *respNs, xmlNodePtr root)
{
    for (; req; req = req->next) {
        if (req->type == XML_ELEMENT_NODE) {
            xmlNsPtr nsDef;

            for (nsDef = req->nsDef; nsDef; nsDef = nsDef->next) {
                if (!xmlStrcmp(nsDef->href, BAD_CAST XML_NS_DAV))
                    ensure_ns(respNs, NS_DAV, root,
                              (const char *) nsDef->href,
                              (const char *) nsDef->prefix);
                else if (!xmlStrcmp(nsDef->href, BAD_CAST XML_NS_CALDAV))
                    ensure_ns(respNs, NS_CALDAV, root,
                              (const char *) nsDef->href,
                              (const char *) nsDef->prefix);
                else if (!xmlStrcmp(nsDef->href, BAD_CAST XML_NS_CARDDAV))
                    ensure_ns(respNs, NS_CARDDAV, root,
                              (const char *) nsDef->href,
                              (const char *) nsDef->prefix);
                else if (!xmlStrcmp(nsDef->href, BAD_CAST XML_NS_CS))
                    ensure_ns(respNs, NS_CS, root,
                              (const char *) nsDef->href,
                              (const char *) nsDef->prefix);
                else if (!xmlStrcmp(nsDef->href, BAD_CAST XML_NS_MECOM))
                    ensure_ns(respNs, NS_MECOM, root,
                              (const char *) nsDef->href,
                              (const char *) nsDef->prefix);
                else if (!xmlStrcmp(nsDef->href, BAD_CAST XML_NS_MOBME))
                    ensure_ns(respNs, NS_MOBME, root,
                              (const char *) nsDef->href,
                              (const char *) nsDef->prefix);
                else if (!xmlStrcmp(nsDef->href, BAD_CAST XML_NS_CYRUS))
                    ensure_ns(respNs, NS_CYRUS, root,
                              (const char *) nsDef->href,
                              (const char *) nsDef->prefix);
                else
                    xmlNewNs(root, nsDef->href, nsDef->prefix);
            }
        }

        xml_add_ns(req->children, respNs, root);
    }

    /* XXX  check for errors */
    return 0;
}


/* Initialize an XML tree for a property response */
xmlNodePtr init_xml_response(const char *resp, int ns,
                             xmlNodePtr req, xmlNsPtr *respNs)
{
    /* Start construction of our XML response tree */
    xmlDocPtr doc = xmlNewDoc(BAD_CAST "1.0");
    xmlNodePtr root = NULL;

    if (!doc) return NULL;
    if (!(root = xmlNewNode(NULL, BAD_CAST resp))) {
        xmlFreeDoc(doc);
        return NULL;
    }

    xmlDocSetRootElement(doc, root);

    /* Add namespaces from request to our response,
     * creating array of known namespaces that we can reference later.
     */
    memset(respNs, 0, NUM_NAMESPACE * sizeof(xmlNsPtr));
    xml_add_ns(req, respNs, root);

    /* Set namespace of root node */
    if (ns == NS_REQ_ROOT) xmlSetNs(root, req->ns);
    else {
        ensure_ns(respNs, ns, root,
                  known_namespaces[ns].href, known_namespaces[ns].prefix);
        xmlSetNs(root, respNs[ns]);
    }

    return root;
}

xmlNodePtr xml_add_href(xmlNodePtr parent, xmlNsPtr ns, const char *href)
{
    xmlChar *uri = xmlURIEscapeStr(BAD_CAST href, BAD_CAST ":/?=");
    xmlNodePtr node = xmlNewChild(parent, ns, BAD_CAST "href", uri);

    free(uri);
    return node;
}

xmlNodePtr xml_add_error(xmlNodePtr root, struct error_t *err,
                         xmlNsPtr *avail_ns)
{
    xmlNsPtr ns[NUM_NAMESPACE];
    xmlNodePtr error, node;
    const struct precond_t *precond = &preconds[err->precond];
    unsigned err_ns = NS_DAV;
    const char *resp_desc = "responsedescription";

    if (precond->ns == NS_ISCHED) {
        err_ns = NS_ISCHED;
        resp_desc = "response-description";
    }

    if (!root) {
        error = root = init_xml_response("error", err_ns, NULL, ns);
        avail_ns = ns;
    }
    else error = xmlNewChild(root, NULL, BAD_CAST "error", NULL);

    ensure_ns(avail_ns, precond->ns, root, known_namespaces[precond->ns].href,
              known_namespaces[precond->ns].prefix);
    node = xmlNewChild(error, avail_ns[precond->ns],
                       BAD_CAST precond->name, NULL);

    switch (err->precond) {
    case DAV_NEED_PRIVS:
        if (err->resource && err->rights) {
            unsigned flags = 0;
            size_t rlen = strlen(err->resource);
            const char *p = err->resource + rlen;

            node = xmlNewChild(node, NULL, BAD_CAST "resource", NULL);
            xml_add_href(node, NULL, err->resource);

            if (rlen > 6 && !strcmp(p-6, SCHED_INBOX))
                flags = PRIV_INBOX;
            else if (rlen > 7 && !strcmp(p-7, SCHED_OUTBOX))
                flags = PRIV_OUTBOX;

            add_privs(err->rights, flags, node, root, avail_ns);
        }
        break;

    default:
        if (err->node) xmlAddChild(node, err->node);
        if (err->resource) xml_add_href(node, avail_ns[NS_DAV], err->resource);
        break;
    }

    if (err->desc) {
        xmlNewTextChild(error, NULL, BAD_CAST resp_desc, BAD_CAST err->desc);
    }

    return error;
}


void xml_add_lockdisc(xmlNodePtr node, const char *root, struct dav_data *data)
{
    time_t now = time(NULL);

    if (data->lock_expire > now) {
        xmlNodePtr active, node1;
        char tbuf[30]; /* "Second-" + long int + NUL */

        active = xmlNewChild(node, NULL, BAD_CAST "activelock", NULL);
        node1 = xmlNewChild(active, NULL, BAD_CAST "lockscope", NULL);
        xmlNewChild(node1, NULL, BAD_CAST "exclusive", NULL);

        node1 = xmlNewChild(active, NULL, BAD_CAST "locktype", NULL);
        xmlNewChild(node1, NULL, BAD_CAST "write", NULL);

        xmlNewChild(active, NULL, BAD_CAST "depth", BAD_CAST "0");

        if (data->lock_owner) {
            if (!strncmp(data->lock_owner, "<DAV:href>", 10)) {
                node1 = xmlNewChild(active, NULL, BAD_CAST "owner", NULL);
                xml_add_href(node1, NULL, data->lock_owner + 10);
            }
            else {
                xmlNewTextChild(active, NULL, BAD_CAST "owner",
                                BAD_CAST data->lock_owner);
            }
        }

        snprintf(tbuf, sizeof(tbuf), "Second-%lu", data->lock_expire - now);
        xmlNewChild(active, NULL, BAD_CAST "timeout", BAD_CAST tbuf);

        node1 = xmlNewChild(active, NULL, BAD_CAST "locktoken", NULL);
        xml_add_href(node1, NULL, data->lock_token);

        node1 = xmlNewChild(active, NULL, BAD_CAST "lockroot", NULL);
        xml_add_href(node1, NULL, root);
    }
}


/* Add a property 'name', of namespace 'ns', with content 'content',
 * and status code/string 'status' to propstat element 'stat'.
 * 'stat' will be created as necessary.
 */
xmlNodePtr xml_add_prop(long status, xmlNsPtr davns,
                        struct propstat *propstat,
                        const xmlChar *name, xmlNsPtr ns,
                        xmlChar *content,
                        unsigned precond)
{
    xmlNodePtr newprop = NULL;

    if (!propstat->root) {
        propstat->root = xmlNewNode(davns, BAD_CAST "propstat");
        xmlNewChild(propstat->root, NULL, BAD_CAST "prop", NULL);
    }

    if (name) newprop = xmlNewTextChild(propstat->root->children,
                                        ns, name, content);
    propstat->status = status;
    propstat->precond = precond;

    return newprop;
}


struct allprop_rock {
    struct propfind_ctx *fctx;
    struct propstat *propstat;
};

/* Add a response tree to 'root' for the specified href and
   either error code or property list */
int xml_add_response(struct propfind_ctx *fctx, long code, unsigned precond,
                     const char *desc, const char *location)
{
    xmlNodePtr resp;

    resp = xmlNewChild(fctx->root, fctx->ns[NS_DAV], BAD_CAST "response", NULL);
    if (!resp) {
        fctx->txn->error.desc = "Unable to add response XML element";
        *fctx->ret = HTTP_SERVER_ERROR;
        return HTTP_SERVER_ERROR;
    }
    xml_add_href(resp, NULL, fctx->req_tgt->path);

    if (code) {
        xmlNewChild(resp, NULL, BAD_CAST "status",
                    BAD_CAST http_statusline(VER_1_1, code));

        if (precond) {
            xmlNodePtr error = xmlNewChild(resp, NULL, BAD_CAST "error", NULL);

            xmlNewChild(error, NULL, BAD_CAST preconds[precond].name, NULL);
        }
        if (desc) {
            xmlNewTextChild(resp, NULL,
                            BAD_CAST "errordescription", BAD_CAST desc);
        }
        if (location) {
            xmlNodePtr node = xmlNewChild(resp, NULL, BAD_CAST "location", NULL);

            xml_add_href(node, NULL, location);
        }
    }
    else {
        struct propstat propstat[NUM_PROPSTAT], *stat;
        struct propfind_entry_list *e;
        int i;

        memset(propstat, 0, NUM_PROPSTAT * sizeof(struct propstat));

        /* Process each property in the linked list */
        for (e = fctx->elist; e; e = e->next) {
            int r = HTTP_NOT_FOUND;

            if (e->get) {
                r = 0;

                /* Pre-screen request based on prop flags */
                if (fctx->req_tgt->resource) {
                    if (!(e->flags & PROP_RESOURCE)) r = HTTP_NOT_FOUND;
                }
                else if (!(e->flags & PROP_COLLECTION)) r = HTTP_NOT_FOUND;

                if (!r) {
                    if (fctx->mode == PROPFIND_NAME) {
                        xml_add_prop(HTTP_OK, fctx->ns[NS_DAV],
                                     &propstat[PROPSTAT_OK],
                                     e->name, e->ns, NULL, 0);
                    }
                    else {
                        r = e->get(e->name, e->ns, fctx,
                                   e->prop, resp, propstat, e->rock);
                    }
                }
            }

            switch (r) {
            case 0:
            case HTTP_OK:
                /* Nothing to do - property handled in callback */
                break;

            case HTTP_UNAUTHORIZED:
                xml_add_prop(HTTP_UNAUTHORIZED, fctx->ns[NS_DAV],
                             &propstat[PROPSTAT_UNAUTH],
                             e->name, e->ns, NULL, 0);
                break;

            case HTTP_FORBIDDEN:
                xml_add_prop(HTTP_FORBIDDEN, fctx->ns[NS_DAV],
                             &propstat[PROPSTAT_FORBID],
                             e->name, e->ns, NULL, 0);
                break;

            case HTTP_NOT_FOUND:
                if (!(fctx->prefer & PREFER_MIN)) {
                    xml_add_prop(HTTP_NOT_FOUND, fctx->ns[NS_DAV],
                                 &propstat[PROPSTAT_NOTFOUND],
                                 e->name, e->ns, NULL, 0);
                }
                break;

            case HTTP_BAD_MEDIATYPE:
                /* CALDAV:calendar-data/timezone/availability and
                   CARDDAV:address-data ONLY.
                   'e->rock' contains supported data precondition code.
                */
                xml_add_prop(HTTP_FORBIDDEN, fctx->ns[NS_DAV],
                             &propstat[PROPSTAT_FORBID],
                             e->name, e->ns, NULL, (uintptr_t) e->rock);
                break;

            default:
                xml_add_prop(r, fctx->ns[NS_DAV], &propstat[PROPSTAT_ERROR],
                             e->name, e->ns, NULL, 0);
                break;

            }
        }

        /* Process dead properties for allprop/propname */
        if (fctx->mailbox && !fctx->req_tgt->resource &&
            (fctx->mode == PROPFIND_ALL || fctx->mode == PROPFIND_NAME)) {
            struct allprop_rock arock = { fctx, propstat };

            annotatemore_findall(fctx->mailbox->name, 0, "*", /*modseq*/0,
                                 allprop_cb, &arock, /*flags*/0);
        }

        /* Check if we have any propstat elements */
        for (i = 0; i < NUM_PROPSTAT && !propstat[i].root; i++);
        if (i == NUM_PROPSTAT) {
            /* Add an empty propstat 200 */
            xml_add_prop(HTTP_OK, fctx->ns[NS_DAV],
                         &propstat[PROPSTAT_OK], NULL, NULL, NULL, 0);
        }

        /* Add status and optional error to the propstat elements
           and then add them to response element */
        for (i = 0; i < NUM_PROPSTAT; i++) {
            stat = &propstat[i];

            if (stat->root) {
                xmlNewChild(stat->root, NULL, BAD_CAST "status",
                            BAD_CAST http_statusline(VER_1_1, stat->status));
                if (stat->precond) {
                    struct error_t error = { NULL, stat->precond, NULL, NULL, 0 };
                    xml_add_error(stat->root, &error, fctx->ns);
                }

                xmlAddChild(resp, stat->root);
            }
        }
    }

    fctx->record = NULL;

    if (fctx->txn->flags.te & TE_CHUNKED) {
        /* Add <response> element for this resource to output buffer.
           Only output the xmlBuffer every PROT_BUFSIZE bytes */
        xml_partial_response((xmlBufferLength(fctx->xmlbuf) > PROT_BUFSIZE) ?
                             fctx->txn : NULL,
                             fctx->root->doc, resp, 1, &fctx->xmlbuf);

        /* Remove <response> element from root (no need to keep in memory) */
        xmlReplaceNode(resp, NULL);
        xmlFreeNode(resp);
    }

    return 0;
}


/* Helper function to prescreen/fetch resource data */
int propfind_getdata(const xmlChar *name, xmlNsPtr ns,
                     struct propfind_ctx *fctx,
                     xmlNodePtr prop, struct propstat propstat[],
                     struct mime_type_t *mime_types,
                     struct mime_type_t **out_type,
                     const char *data, unsigned long datalen)
{
    int ret = 0;

    if (!propstat) {
        /* Prescreen "property" request */
        xmlChar *type, *ver = NULL;
        struct mime_type_t *mime;

        type = xmlGetProp(prop, BAD_CAST "content-type");
        if (type) ver = xmlGetProp(prop, BAD_CAST "version");

        /* Check/find requested MIME type */
        for (mime = mime_types; type && mime->content_type; mime++) {
            if (is_mediatype((const char *) type, mime->content_type)) {
                if (ver &&
                    (!mime->version || xmlStrcmp(ver, BAD_CAST mime->version))) {
                    continue;
                }
                break;
            }
        }

        if (type) xmlFree(type);
        if (ver) xmlFree(ver);

        *out_type = mime;
    }
    else {
        /* Add "property" */
        struct mime_type_t *mime = *out_type;
        char *freeme = NULL;

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

        if (mime != mime_types) {
            /* Not the storage format - convert into requested MIME type */
            struct buf inbuf = BUF_INITIALIZER;

            if (!fctx->obj) {
                buf_init_ro(&inbuf, data, datalen);
                fctx->obj = mime_types->to_object(&inbuf);
                buf_free(&inbuf);
            }

            struct buf *outbuf = mime->from_object(fctx->obj);
            datalen = buf_len(outbuf);
            data = freeme = buf_release(outbuf);
            buf_destroy(outbuf);

            xmlSetProp(prop,
                       BAD_CAST "content-type", BAD_CAST mime->content_type);
            if (mime->version)
                xmlSetProp(prop, BAD_CAST "version", BAD_CAST mime->version);
        }

        xmlAddChild(prop,
                    xmlNewCDataBlock(fctx->root->doc, BAD_CAST data, datalen));

        fctx->flags.fetcheddata = 1;

        if (freeme) free(freeme);
    }

    return ret;
}


/* Callback to fetch DAV:creationdate */
int propfind_creationdate(const xmlChar *name, xmlNsPtr ns,
                          struct propfind_ctx *fctx,
                          xmlNodePtr prop __attribute__((unused)),
                          xmlNodePtr resp __attribute__((unused)),
                          struct propstat propstat[],
                          void *rock __attribute__((unused)))
{
    time_t t = 0;
    char datestr[RFC3339_DATETIME_MAX];

    if (fctx->data) {
        struct dav_data *ddata = (struct dav_data *) fctx->data;

        t = ddata->creationdate;
    }
    else if (fctx->mailbox) {
        struct stat sbuf;

        fstat(fctx->mailbox->header_fd, &sbuf);

        t = sbuf.st_ctime;
    }

    if (!t) return HTTP_NOT_FOUND;

    time_to_rfc3339(t, datestr, RFC3339_DATETIME_MAX);

    xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
                 name, ns, BAD_CAST datestr, 0);

    return 0;
}

/* Callback to write CALDAV:displayname for the principal */
int proppatch_principalname(xmlNodePtr prop, unsigned set,
                          struct proppatch_ctx *pctx,
                          struct propstat propstat[],
                          void *rock __attribute__((unused)))
{
    struct mailbox *mailbox = pctx->mailbox;
    struct mailbox *calhomeset = NULL;

    if (pctx->txn->req_tgt.namespace->id == URL_NS_PRINCIPAL) {
        /* We have been storing CUAS on cal-home-set, NOT INBOX */
        char *mboxname = caldav_mboxname(pctx->txn->req_tgt.userid, NULL);
        int r = 0;

        if (!mailbox || strcmp(mboxname, mailbox->name)) {
            r = mailbox_open_iwl(mboxname, &calhomeset);
            if (!r) pctx->mailbox = calhomeset;
        }
        free(mboxname);

        if (r) {
            xml_add_prop(HTTP_SERVER_ERROR, pctx->ns[NS_DAV],
                         &propstat[PROPSTAT_ERROR],
                         prop->name, prop->ns, NULL, 0);
            *pctx->ret = HTTP_SERVER_ERROR;
            return 0;
        }
    }
    else {
        /* shouldn't happen!  Internal server error 'r' us */
        *pctx->ret = HTTP_SERVER_ERROR;
        return 0;
    }

    /* Make sure this is on a collection and the user has admin rights */
    if (pctx->txn->req_tgt.resource ||
        !(cyrus_acl_myrights(httpd_authstate, pctx->mailbox->acl) & DACL_ADMIN)) {
        xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV],
                     &propstat[PROPSTAT_FORBID],
                     prop->name, prop->ns, NULL, 0);

        *pctx->ret = HTTP_FORBIDDEN;
    }
    else {
        char *value = NULL;

        if (set) {
            value = (char *) xmlNodeGetContent(prop);
        }

        proppatch_todb(prop, set, pctx, propstat, (void *) value);
        free(value);
    }

    if (calhomeset) {
        mailbox_close(&calhomeset);
        pctx->mailbox = mailbox;
    }

    return 0;
}

/* Callback to fetch DAV:displayname for principals */
static int propfind_principalname(const xmlChar *name, xmlNsPtr ns,
                                  struct propfind_ctx *fctx,
                                  xmlNodePtr prop __attribute__((unused)),
                                  xmlNodePtr resp __attribute__((unused)),
                                  struct propstat propstat[],
                                  void *rock __attribute__((unused)))
{
    /* XXX  Do LDAP/SQL lookup here */
    buf_reset(&fctx->buf);

    if (fctx->req_tgt->userid) {
        const char *annotname = DAV_ANNOT_NS "<" XML_NS_DAV ">displayname";
        char *mailboxname = caldav_mboxname(fctx->req_tgt->userid, NULL);
        int r = annotatemore_lookupmask(mailboxname, annotname,
                                        fctx->req_tgt->userid, &fctx->buf);
        free(mailboxname);
        if (r || !buf_len(&fctx->buf)) {
            buf_printf(&fctx->buf, "%s", fctx->req_tgt->userid);
        }
    }
    else {
        buf_printf(&fctx->buf, "no userid");
    }

    xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
                 name, ns, BAD_CAST buf_cstring(&fctx->buf), 0);

    return 0;
}


/* Callback to fetch DAV:displayname for collections */
int propfind_collectionname(const xmlChar *name, xmlNsPtr ns,
                            struct propfind_ctx *fctx,
                            xmlNodePtr prop, xmlNodePtr resp,
                            struct propstat propstat[], void *rock)
{
    int r = propfind_fromdb(name, ns, fctx, prop, resp, propstat, rock);

    if (r && fctx->mbentry && !fctx->req_tgt->resource) {
        /* Special case empty displayname -- use last segment of path */
        char *p = strrchr(fctx->mbentry->name, '.');
        xmlNodePtr node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV],
                                       &propstat[PROPSTAT_OK], name, ns, NULL, 0);
        buf_setcstr(&fctx->buf, (p ? p + 1 : fctx->mbentry->name));
        xmlAddChild(node, xmlNewCDataBlock(fctx->root->doc,
                                           BAD_CAST buf_cstring(&fctx->buf),
                                           buf_len(&fctx->buf)));
        return 0;
    }

    return r;
}


/* Callback to fetch DAV:getcontentlength */
int propfind_getlength(const xmlChar *name, xmlNsPtr ns,
                       struct propfind_ctx *fctx,
                       xmlNodePtr prop __attribute__((unused)),
                       xmlNodePtr resp __attribute__((unused)),
                       struct propstat propstat[],
                       void *rock __attribute__((unused)))
{
    buf_reset(&fctx->buf);

    if (fctx->record) {
        buf_printf(&fctx->buf, "%u",
                   fctx->record->size - fctx->record->header_size);
    }

    xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
                 name, ns, BAD_CAST buf_cstring(&fctx->buf), 0);

    return 0;
}


/* Callback to fetch DAV:getetag */
int propfind_getetag(const xmlChar *name, xmlNsPtr ns,
                     struct propfind_ctx *fctx,
                     xmlNodePtr prop __attribute__((unused)),
                     xmlNodePtr resp __attribute__((unused)),
                     struct propstat propstat[],
                     void *rock __attribute__((unused)))
{
    if (fctx->req_tgt->resource && !fctx->record) return HTTP_NOT_FOUND;
    if (!fctx->mailbox) return HTTP_NOT_FOUND;

    buf_reset(&fctx->buf);

    if (fctx->record) {
        const char *etag;

        fctx->get_validators(fctx->mailbox, fctx->data, fctx->userid,
                             fctx->record, &etag, NULL);
        /* add DQUOTEs */
        buf_printf(&fctx->buf, "\"%s\"", etag);
    }
    else {
        buf_printf(&fctx->buf, "\"%u-%u-%u\"", fctx->mailbox->i.uidvalidity,
                   fctx->mailbox->i.last_uid, fctx->mailbox->i.exists);
    }

    xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
                 name, ns, BAD_CAST buf_cstring(&fctx->buf), 0);

    return 0;
}


/* Callback to fetch DAV:getlastmodified */
int propfind_getlastmod(const xmlChar *name, xmlNsPtr ns,
                        struct propfind_ctx *fctx,
                        xmlNodePtr prop __attribute__((unused)),
                        xmlNodePtr resp __attribute__((unused)),
                        struct propstat propstat[],
                        void *rock __attribute__((unused)))
{
    time_t lastmod;

    if (!fctx->mailbox ||
        (fctx->req_tgt->resource && !fctx->record)) return HTTP_NOT_FOUND;

    if (fctx->record) {
        fctx->get_validators(fctx->mailbox, fctx->data, fctx->userid,
                             fctx->record, NULL, &lastmod);
    }
    else {
        lastmod = fctx->mailbox->index_mtime;
    }

    buf_ensure(&fctx->buf, 30);
    httpdate_gen(fctx->buf.s, fctx->buf.alloc, lastmod);

    xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
                 name, ns, BAD_CAST fctx->buf.s, 0);

    return 0;
}


/* Callback to fetch DAV:lockdiscovery */
int propfind_lockdisc(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->data) {
        struct dav_data *ddata = (struct dav_data *) fctx->data;

        xml_add_lockdisc(node, fctx->req_tgt->path, ddata);
    }

    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->req_tgt->namespace->id == URL_NS_PRINCIPAL) {
        if (fctx->req_tgt->userid)
            xmlNewChild(node, NULL, BAD_CAST "principal", NULL);
        else
            xmlNewChild(node, NULL, BAD_CAST "collection", NULL);
    }
    else 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 "write" resourcetype property */
int proppatch_restype(xmlNodePtr prop, unsigned set,
                      struct proppatch_ctx *pctx,
                      struct propstat propstat[],
                      void *rock)
{
    const char *coltype = (const char *) rock;
    unsigned precond = 0;

    if (set && (pctx->txn->meth != METH_PROPPATCH)) {
        /* "Writeable" for MKCOL/MKCALENDAR only */
        xmlNodePtr cur;

        for (cur = prop->children; cur; cur = cur->next) {
            if (cur->type != XML_ELEMENT_NODE) continue;
            /* Make sure we have valid resourcetypes for the collection */
            if (xmlStrcmp(cur->name, BAD_CAST "collection") &&
                (!coltype || xmlStrcmp(cur->name, BAD_CAST coltype))) break;
        }

        if (!cur) {
            /* All resourcetypes are valid */
            xml_add_prop(HTTP_OK, pctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
                         prop->name, prop->ns, NULL, 0);

            return 0;
        }

        /* Invalid resourcetype */
        precond = DAV_VALID_RESTYPE;
    }
    else {
        /* Protected property */
        precond = DAV_PROT_PROP;
    }

    xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV], &propstat[PROPSTAT_FORBID],
                 prop->name, prop->ns, NULL, precond);

    *pctx->ret = HTTP_FORBIDDEN;

    return 0;
}


/* Callback to fetch DAV:supportedlock */
int propfind_suplock(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->mailbox && fctx->record) {
        xmlNodePtr entry = xmlNewChild(node, NULL, BAD_CAST "lockentry", NULL);
        xmlNodePtr scope = xmlNewChild(entry, NULL, BAD_CAST "lockscope", NULL);
        xmlNodePtr type = xmlNewChild(entry, NULL, BAD_CAST "locktype", NULL);

        xmlNewChild(scope, NULL, BAD_CAST "exclusive", NULL);
        xmlNewChild(type, NULL, BAD_CAST "write", NULL);
    }

    return 0;
}


/* Callback to fetch DAV:supported-report-set */
int propfind_reportset(const xmlChar *name, xmlNsPtr ns,
                       struct propfind_ctx *fctx,
                       xmlNodePtr prop __attribute__((unused)),
                       xmlNodePtr resp __attribute__((unused)),
                       struct propstat propstat[],
                       void *rock)
{
    xmlNodePtr top, node;
    const struct report_type_t *report;

    if (!propstat) {
        /* Prescreen "property" request */
        for (report = (const struct report_type_t *) rock;
             report && report->name; report++) {
            /* Add namespaces for possible reports */
            ensure_ns(fctx->ns, report->ns, fctx->root,
                      known_namespaces[report->ns].href,
                      known_namespaces[report->ns].prefix);
        }

        return 0;
    }

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

    for (report = (const struct report_type_t *) rock;
         report && report->name; report++) {
        node = xmlNewChild(top, NULL, BAD_CAST "supported-report", NULL);
        node = xmlNewChild(node, NULL, BAD_CAST "report", NULL);

        ensure_ns(fctx->ns, report->ns, resp->parent,
                  known_namespaces[report->ns].href,
                  known_namespaces[report->ns].prefix);
        xmlNewChild(node, fctx->ns[report->ns], BAD_CAST report->name, NULL);
    }

    return 0;
}


/* Callback to fetch DAV:supported-method-set */
int propfind_methodset(const xmlChar *name, xmlNsPtr ns,
                       struct propfind_ctx *fctx,
                       xmlNodePtr prop __attribute__((unused)),
                       xmlNodePtr resp __attribute__((unused)),
                       struct propstat propstat[],
                       void *rock)
{
    unsigned long (*allow_cb)(struct request_target_t *) =
        (unsigned long (*)(struct request_target_t *)) rock;
    unsigned long allow = allow_cb(fctx->req_tgt);
    xmlNodePtr top, node;

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

    if (allow & ALLOW_ACL) {
        node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL);
        xmlSetProp(node, BAD_CAST "name", BAD_CAST "ACL");
    }
    if (allow & ALLOW_READ) {
        node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL);
        xmlSetProp(node, BAD_CAST "name", BAD_CAST "COPY");
    }
    if (allow & ALLOW_DELETE) {
        node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL);
        xmlSetProp(node, BAD_CAST "name", BAD_CAST "DELETE");
    }
    if (allow & ALLOW_READ) {
        node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL);
        xmlSetProp(node, BAD_CAST "name", BAD_CAST "GET");
        node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL);
        xmlSetProp(node, BAD_CAST "name", BAD_CAST "HEAD");
    }
    if (allow & ALLOW_WRITE) {
        node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL);
        xmlSetProp(node, BAD_CAST "name", BAD_CAST "LOCK");
    }
    if (allow & ALLOW_MKCOL) {
        if (allow & ALLOW_CAL) {
            node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL);
            xmlSetProp(node, BAD_CAST "name", BAD_CAST "MKCALENDAR");
        }
        node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL);
        xmlSetProp(node, BAD_CAST "name", BAD_CAST "MKCOL");
    }
    if (allow & ALLOW_DELETE) {
        node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL);
        xmlSetProp(node, BAD_CAST "name", BAD_CAST "MOVE");
    }
    if (allow & ALLOW_READ) {
        node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL);
        xmlSetProp(node, BAD_CAST "name", BAD_CAST "OPTIONS");
    }
    if (allow & ALLOW_PATCH) {
        node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL);
        xmlSetProp(node, BAD_CAST "name", BAD_CAST "PATCH");
    }
    if (allow & ALLOW_POST) {
        node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL);
        xmlSetProp(node, BAD_CAST "name", BAD_CAST "POST");
    }
    if (allow & ALLOW_READ) {
        node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL);
        xmlSetProp(node, BAD_CAST "name", BAD_CAST "PROPFIND");
    }
    if (allow & ALLOW_PROPPATCH) {
        node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL);
        xmlSetProp(node, BAD_CAST "name", BAD_CAST "PROPPATCH");
    }
    if (allow & ALLOW_WRITE) {
        node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL);
        xmlSetProp(node, BAD_CAST "name", BAD_CAST "PUT");
    }
    if (allow & ALLOW_READ) {
        node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL);
        xmlSetProp(node, BAD_CAST "name", BAD_CAST "REPORT");
    }
    if (allow & ALLOW_TRACE) {
        node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL);
        xmlSetProp(node, BAD_CAST "name", BAD_CAST "TRACE");
    }
    if (allow & ALLOW_WRITE) {
        node = xmlNewChild(top, NULL, BAD_CAST "supported-method", NULL);
        xmlSetProp(node, BAD_CAST "name", BAD_CAST "UNLOCK");
    }

    return 0;
}


/* Callback to fetch *DAV:supported-collation-set */
int propfind_collationset(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 top;
    const struct collation_t *col;

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

    for (col = dav_collations; col->name; col++) {
        xmlNewChild(top, NULL, BAD_CAST "supported-collation", BAD_CAST col->name);
    }

    return 0;
}


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

    return 0;
}


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

    if (userid) {
        buf_reset(&fctx->buf);

        if (strchr(userid, '@') || !httpd_extradomain) {
            buf_printf(&fctx->buf, "%s/%s/%s/", namespace_principal.prefix,
                       USER_COLLECTION_PREFIX, userid);
        }
        else {
            buf_printf(&fctx->buf, "%s/%s/%s@%s/", namespace_principal.prefix,
                       USER_COLLECTION_PREFIX, userid, httpd_extradomain);
        }

        if ((fctx->mode == PROPFIND_EXPAND) && xmlFirstElementChild(prop)) {
            /* Return properties for this URL */
            expand_property(prop, fctx, &namespace_principal, buf_cstring(&fctx->buf),
                            &principal_parse_path, principal_props, node, 0);
        }
        else {
            /* Return just the URL */
            xml_add_href(node, NULL, buf_cstring(&fctx->buf));
        }
    }

    return 0;
}


/* Callback to fetch DAV:owner */
int propfind_owner(const xmlChar *name, xmlNsPtr ns,
                   struct propfind_ctx *fctx,
                   xmlNodePtr prop,
                   xmlNodePtr resp __attribute__((unused)),
                   struct propstat propstat[],
                   void *rock __attribute__((unused)))
{
    mbname_t *mbname;
    const char *owner;
    int r;

    if (!fctx->mbentry) return HTTP_NOT_FOUND;

    mbname = mbname_from_intname(fctx->mbentry->name);
    owner = mbname_userid(mbname);
    if (!owner) {
        static strarray_t *admins = NULL;

        if (!admins) admins = strarray_split(config_getstring(IMAPOPT_ADMINS),
                                             NULL, STRARRAY_TRIM);

        owner = strarray_nth(admins, 0);
    }

    r = propfind_principalurl(name, ns, fctx,
                              prop, resp, propstat, (void *) owner);

    mbname_free(&mbname);

    return r;
}


/* Add possibly 'abstract' supported-privilege 'priv_name', of namespace 'ns',
 * with description 'desc_str' to node 'root'.  For now, we assume all
 * descriptions are English.
 */
static xmlNodePtr add_suppriv(xmlNodePtr root, const char *priv_name,
                              xmlNsPtr ns, int abstract, const char *desc_str)
{
    xmlNodePtr supp, priv, desc;

    supp = xmlNewChild(root, NULL, BAD_CAST "supported-privilege", NULL);
    priv = xmlNewChild(supp, NULL, BAD_CAST "privilege", NULL);
    xmlNewChild(priv, ns, BAD_CAST priv_name, NULL);
    if (abstract) xmlNewChild(supp, NULL, BAD_CAST "abstract", NULL);
    desc = xmlNewChild(supp, NULL, BAD_CAST "description", BAD_CAST desc_str);
    xmlNodeSetLang(desc, BAD_CAST "en");

    return supp;
}


/* Callback to fetch DAV:supported-privilege-set */
int propfind_supprivset(const xmlChar *name, xmlNsPtr ns,
                        struct propfind_ctx *fctx,
                        xmlNodePtr prop __attribute__((unused)),
                        xmlNodePtr resp,
                        struct propstat propstat[],
                        void *rock __attribute__((unused)))
{
    xmlNodePtr set, all, agg, write;
    unsigned tgt_flags = 0;

    if (!propstat) {
        /* Prescreen "property" request */
        if (fctx->req_tgt->collection ||
            (fctx->req_tgt->userid && fctx->depth >= 1) || fctx->depth >= 2) {
            /* Add namespaces for possible privileges */
            ensure_ns(fctx->ns, NS_CYRUS, fctx->root, XML_NS_CYRUS, "CY");
            if (fctx->req_tgt->namespace->id == URL_NS_CALENDAR) {
                ensure_ns(fctx->ns, NS_CALDAV, fctx->root, XML_NS_CALDAV, "C");
            }
        }

        return 0;
    }

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

    all = add_suppriv(set, "all", NULL, 0, "Any operation");

    agg = add_suppriv(all, "read", NULL, 0, "Read any object");
    add_suppriv(agg, "read-current-user-privilege-set", NULL, 1,
                "Read current user privilege set");

    if (fctx->req_tgt->namespace->id == URL_NS_CALENDAR) {
        if (fctx->req_tgt->collection) {
            ensure_ns(fctx->ns, NS_CALDAV, resp->parent, XML_NS_CALDAV, "C");

            if (!strcmp(fctx->req_tgt->collection, SCHED_INBOX))
                tgt_flags = TGT_SCHED_INBOX;
            else if (!strcmp(fctx->req_tgt->collection, SCHED_OUTBOX))
                tgt_flags = TGT_SCHED_OUTBOX;
            else {
                add_suppriv(agg, "read-free-busy", fctx->ns[NS_CALDAV], 0,
                            "Read free/busy time");
            }
        }
    }

    write = add_suppriv(all, "write", NULL, 0, "Write any object");
    add_suppriv(write, "write-content", NULL, 0, "Write resource content");

    agg = add_suppriv(write, "write-properties", NULL, 0, "Write properties");
    ensure_ns(fctx->ns, NS_CYRUS, resp->parent, XML_NS_CYRUS, "CY");
    add_suppriv(agg, "write-properties-collection", fctx->ns[NS_CYRUS], 0,
                "Write properties on a collection");
    add_suppriv(agg, "write-properties-resource", fctx->ns[NS_CYRUS], 0,
                "Write properties on a resource");

    agg = add_suppriv(write, "bind", NULL, 0, "Add new member to collection");
    add_suppriv(agg, "make-collection", fctx->ns[NS_CYRUS], 0,
                "Make new collection");
    add_suppriv(agg, "add-resource", fctx->ns[NS_CYRUS], 0,
                "Add new resource");

    agg = add_suppriv(write, "unbind", NULL, 0,
                         "Remove member from collection");
    add_suppriv(agg, "remove-collection", fctx->ns[NS_CYRUS], 0,
                "Remove collection");
    add_suppriv(agg, "remove-resource", fctx->ns[NS_CYRUS], 0,
                "Remove resource");

    agg = add_suppriv(all, "admin", fctx->ns[NS_CYRUS], 0,
                        "Perform administrative operations");
    add_suppriv(agg, "read-acl", NULL, 1, "Read ACL");
    add_suppriv(agg, "write-acl", NULL, 1, "Write ACL");
    add_suppriv(agg, "unlock", NULL, 1, "Unlock resource");
    add_suppriv(agg, "share", NULL, 1, "Share resource");

    if (tgt_flags == TGT_SCHED_INBOX) {
        agg = add_suppriv(all, "schedule-deliver", fctx->ns[NS_CALDAV], 0,
                          "Deliver scheduling messages");
        add_suppriv(agg, "schedule-deliver-invite", fctx->ns[NS_CALDAV], 0,
                    "Deliver scheduling messages from Organizers");
        add_suppriv(agg, "schedule-deliver-reply", fctx->ns[NS_CALDAV], 0,
                    "Deliver scheduling messages from Attendees");
        add_suppriv(agg, "schedule-query-freebusy", fctx->ns[NS_CALDAV], 0,
                    "Accept free/busy requests");
    }
    else if (tgt_flags == TGT_SCHED_OUTBOX) {
        agg = add_suppriv(all, "schedule-send", fctx->ns[NS_CALDAV], 0,
                          "Send scheduling messages");
        add_suppriv(agg, "schedule-send-invite", fctx->ns[NS_CALDAV], 0,
                    "Send scheduling messages by Organizers");
        add_suppriv(agg, "schedule-send-reply", fctx->ns[NS_CALDAV], 0,
                    "Send scheduling messages by Attendees");
        add_suppriv(agg, "schedule-send-freebusy", fctx->ns[NS_CALDAV], 0,
                    "Submit free/busy requests");
    }

    return 0;
}


static void add_privs(int rights, unsigned flags,
                      xmlNodePtr parent, xmlNodePtr root, xmlNsPtr *ns)
{
    xmlNodePtr priv;
    int do_contained;

    /* DAV:all */
    if ((rights & DACL_ALL) == DACL_ALL &&
        /* DAV:all on CALDAV:schedule-in/outbox MUST include CALDAV:schedule */
        (!(flags & (PRIV_INBOX|PRIV_OUTBOX)) ||
         (rights & DACL_SCHED) == DACL_SCHED)) {
        priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL);
        xmlNewChild(priv, NULL, BAD_CAST "all", NULL);

        if (!(flags & PRIV_CONTAINED)) return;
    }

    /* DAV:read */
    if ((rights & DACL_READ) == DACL_READ) {
        priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL);
        xmlNewChild(priv, NULL, BAD_CAST "read", NULL);
        if (flags & PRIV_IMPLICIT) rights |= DACL_READFB;

        do_contained = (flags & PRIV_CONTAINED);
    }
    else do_contained = 1;

    if (do_contained) {
        if ((rights & DACL_READFB) &&
            /* CALDAV:read-free-busy doesn't apply to CALDAV:sched-in/outbox */
            !(flags & (PRIV_INBOX|PRIV_OUTBOX))) {
            priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL);
            ensure_ns(ns, NS_CALDAV, root, XML_NS_CALDAV, "C");
            xmlNewChild(priv, ns[NS_CALDAV], BAD_CAST  "read-free-busy", NULL);
        }
    }

    /* DAV:write */
    if ((rights & DACL_WRITE) == DACL_WRITE) {
        priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL);
        xmlNewChild(priv, NULL, BAD_CAST "write", NULL);

        do_contained = (flags & PRIV_CONTAINED);
    }
    else do_contained = 1;

    if (do_contained) {
        if (rights & DACL_WRITECONT) {
            priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL);
            xmlNewChild(priv, NULL, BAD_CAST "write-content", NULL);
        }

        if (rights & (DACL_WRITEPROPS|DACL_BIND|DACL_UNBIND)) {
            ensure_ns(ns, NS_CYRUS, root, XML_NS_CYRUS, "CY");

            /* DAV:write-properties */
            if ((rights & DACL_WRITEPROPS) == DACL_WRITEPROPS) {
                priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL);
                xmlNewChild(priv, NULL, BAD_CAST "write-properties", NULL);

                do_contained = (flags & PRIV_CONTAINED);
            }
            else do_contained = 1;

            if (do_contained) {
                if (rights & DACL_PROPCOL) {
                    priv = xmlNewChild(parent, NULL,
                                       BAD_CAST "privilege", NULL);
                    xmlNewChild(priv, ns[NS_CYRUS],
                                BAD_CAST "write-properties-collection", NULL);
                }
                if (rights & DACL_PROPRSRC) {
                    priv = xmlNewChild(parent, NULL,
                                       BAD_CAST "privilege", NULL);
                    xmlNewChild(priv, ns[NS_CYRUS],
                                BAD_CAST "write-properties-resource", NULL);
                }
            }

            /* DAV:bind */
            if ((rights & DACL_BIND) == DACL_BIND) {
                priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL);
                xmlNewChild(priv, NULL, BAD_CAST "bind", NULL);

                do_contained = (flags & PRIV_CONTAINED);
            }
            else do_contained = 1;

            if (do_contained) {
                if (rights & DACL_MKCOL) {
                    priv = xmlNewChild(parent, NULL,
                                       BAD_CAST "privilege", NULL);
                    xmlNewChild(priv, ns[NS_CYRUS],
                                BAD_CAST "make-collection", NULL);
                }
                if (rights & DACL_ADDRSRC) {
                    priv = xmlNewChild(parent, NULL,
                                       BAD_CAST "privilege", NULL);
                    xmlNewChild(priv, ns[NS_CYRUS],
                                BAD_CAST "add-resource", NULL);
                }
            }

            /* DAV:unbind */
            if ((rights & DACL_UNBIND) == DACL_UNBIND) {
                priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL);
                xmlNewChild(priv, NULL, BAD_CAST "unbind", NULL);

                do_contained = (flags & PRIV_CONTAINED);
            }
            else do_contained = 1;

            if (do_contained) {
                if (rights & DACL_RMCOL) {
                    priv = xmlNewChild(parent, NULL,
                                       BAD_CAST "privilege", NULL);
                    xmlNewChild(priv, ns[NS_CYRUS],
                                BAD_CAST "remove-collection", NULL);
                }
                if ((rights & DACL_RMRSRC) == DACL_RMRSRC) {
                    priv = xmlNewChild(parent, NULL,
                                       BAD_CAST "privilege", NULL);
                    xmlNewChild(priv, ns[NS_CYRUS],
                                BAD_CAST "remove-resource", NULL);
                }
            }
        }
    }

    /* CYRUS:admin */
    if (rights & DACL_ADMIN) {
        ensure_ns(ns, NS_CYRUS, root, XML_NS_CYRUS, "CY");
        priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL);
        xmlNewChild(priv, ns[NS_CYRUS], BAD_CAST  "admin", NULL);
    }

    if ((rights & DACL_SCHED) && (flags & (PRIV_INBOX|PRIV_OUTBOX))) {
        struct buf buf = BUF_INITIALIZER;
        size_t len;

        if (flags & PRIV_INBOX) buf_setcstr(&buf, "schedule-deliver");
        else buf_setcstr(&buf, "schedule-send");
        len = buf_len(&buf);

        ensure_ns(ns, NS_CALDAV, root, XML_NS_CALDAV, "C");

        /* CALDAV:schedule */
        if ((rights & DACL_SCHED) == DACL_SCHED) {
            priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL);
            xmlNewChild(priv, ns[NS_CALDAV], BAD_CAST buf_cstring(&buf), NULL);
            
            do_contained = (flags & PRIV_CONTAINED);
        }
        else do_contained = 1;

        if (do_contained) {
            if (rights & DACL_INVITE) {
                buf_truncate(&buf, len);
                buf_appendcstr(&buf, "-invite");
                priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL);
                xmlNewChild(priv, ns[NS_CALDAV],
                            BAD_CAST buf_cstring(&buf), NULL);
            }
            if (rights & DACL_REPLY) {
                buf_truncate(&buf, len);
                buf_appendcstr(&buf, "-reply");
                priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL);
                xmlNewChild(priv, ns[NS_CALDAV],
                            BAD_CAST buf_cstring(&buf), NULL);
            }
            if (rights & DACL_SCHEDFB) {
                if (flags & PRIV_INBOX) buf_setcstr(&buf, "schedule-query");
                else buf_truncate(&buf, len);
                buf_appendcstr(&buf, "-freebusy");
                priv = xmlNewChild(parent, NULL, BAD_CAST "privilege", NULL);
                xmlNewChild(priv, ns[NS_CALDAV],
                            BAD_CAST buf_cstring(&buf), NULL);
            }
        }
        buf_free(&buf);
    }
}


/* Callback to fetch DAV:current-user-privilege-set */
int propfind_curprivset(const xmlChar *name, xmlNsPtr ns,
                        struct propfind_ctx *fctx,
                        xmlNodePtr prop __attribute__((unused)),
                        xmlNodePtr resp,
                        struct propstat propstat[],
                        void *rock __attribute__((unused)))
{
    int rights;
    unsigned flags = 0;
    xmlNodePtr set;

    if (!propstat) {
        /* Prescreen "property" request */
        if (fctx->req_tgt->collection ||
            (fctx->req_tgt->userid && fctx->depth >= 1) || fctx->depth >= 2) {
            /* Add namespaces for possible privileges */
            ensure_ns(fctx->ns, NS_CYRUS, fctx->root, XML_NS_CYRUS, "CY");
            if (fctx->req_tgt->namespace->id == URL_NS_CALENDAR) {
                ensure_ns(fctx->ns, NS_CALDAV, fctx->root, XML_NS_CALDAV, "C");
            }
        }

        return 0;
    }

    if (!fctx->mailbox) return HTTP_NOT_FOUND;
    rights = httpd_myrights(fctx->authstate, fctx->mbentry);
    if ((rights & DACL_READ) != DACL_READ) {
        return HTTP_UNAUTHORIZED;
    }

    /* Add in implicit rights */
    if (fctx->userisadmin) {
        rights |= DACL_ADMIN;
    }
    else if (mboxname_userownsmailbox(httpd_userid, fctx->mailbox->name)) {
        rights |= config_implicitrights;
        /* we always allow admin by the owner in DAV */
        rights |= DACL_ADMIN;
    }

    /* Build the rest of the XML response */
    set = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
                       name, ns, NULL, 0);

    if (!fctx->req_tgt->resource) {
        if (fctx->req_tgt->namespace->id == URL_NS_CALENDAR) {
            flags = PRIV_IMPLICIT;

            if (fctx->req_tgt->collection) {
                if (!strcmp(fctx->req_tgt->collection, SCHED_INBOX))
                    flags = PRIV_INBOX;
                else if (!strcmp(fctx->req_tgt->collection, SCHED_OUTBOX))
                    flags = PRIV_OUTBOX;
            }
        }

        flags += PRIV_CONTAINED;

        add_privs(rights, flags, set, resp->parent, fctx->ns);
    }

    return 0;
}


/* Callback to fetch DAV:acl */
int propfind_acl(const xmlChar *name, xmlNsPtr ns,
                 struct propfind_ctx *fctx,
                 xmlNodePtr prop __attribute__((unused)),
                 xmlNodePtr resp,
                 struct propstat propstat[],
                 void *rock __attribute__((unused)))
{
    xmlNodePtr acl;
    char *aclstr, *userid;
    unsigned flags = 0;

    if (!propstat) {
        /* Prescreen "property" request */
        if (fctx->req_tgt->namespace->id == URL_NS_CALENDAR &&
            (fctx->req_tgt->collection ||
             (fctx->req_tgt->userid && fctx->depth >= 1) || fctx->depth >= 2)) {
            /* Add namespaces for possible privileges */
            ensure_ns(fctx->ns, NS_CALDAV, fctx->root, XML_NS_CALDAV, "C");
        }

        return 0;
    }

    if (!fctx->mailbox) return HTTP_NOT_FOUND;

    /* owner has implicit admin rights */
    if (!mboxname_userownsmailbox(httpd_userid, fctx->mailbox->name)) {
        int rights = httpd_myrights(fctx->authstate, fctx->mbentry);
        if (!(rights & DACL_ADMIN))
            return HTTP_UNAUTHORIZED;
    }

    if (fctx->req_tgt->namespace->id == URL_NS_CALENDAR) {
        flags = PRIV_IMPLICIT;

        if (fctx->req_tgt->collection) {
            if (!strcmp(fctx->req_tgt->collection, SCHED_INBOX))
                flags = PRIV_INBOX;
            else if (!strcmp(fctx->req_tgt->collection, SCHED_OUTBOX))
                flags = PRIV_OUTBOX;
        }
    }

    /* Start the acl XML response */
    acl = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
                       name, ns, NULL, 0);

    /* Parse the ACL string (userid/rights pairs) */
    userid = aclstr = xstrdup(fctx->mailbox->acl);

    while (userid) {
        int rights;
        char *rightstr, *nextid;
        xmlNodePtr ace, node;
        int deny = 0;

        rightstr = strchr(userid, '\t');
        if (!rightstr) break;
        *rightstr++ = '\0';

        nextid = strchr(rightstr, '\t');
        if (!nextid) break;
        *nextid++ = '\0';

        /* Check for negative rights */
        if (*userid == '-') {
            deny = 1;
            userid++;
        }

        cyrus_acl_strtomask(rightstr, &rights);
        /* XXX and if strtomask fails? */

        /* Add ace XML element for this userid/right pair */
        ace = xmlNewChild(acl, NULL, BAD_CAST "ace", NULL);

        node = xmlNewChild(ace, NULL, BAD_CAST "principal", NULL);
        if (!strcmp(userid, fctx->userid))
            xmlNewChild(node, NULL, BAD_CAST "self", NULL);
        else if (mboxname_userownsmailbox(userid, fctx->mailbox->name)) {
            xmlNewChild(node, NULL, BAD_CAST "owner", NULL);
            /* we always allow admin by the owner in DAV */
            rights |= DACL_ADMIN;
        }
        else if (!strcmp(userid, "anyone"))
            xmlNewChild(node, NULL, BAD_CAST "all", NULL);
        else if (!strcmp(userid, "anonymous"))
            xmlNewChild(node, NULL, BAD_CAST "unauthenticated", NULL);
        else if (!strncmp(userid, "group:", 6)) {
            buf_reset(&fctx->buf);
            buf_printf(&fctx->buf, "%s/%s/%s/", namespace_principal.prefix,
                       GROUP_COLLECTION_PREFIX, userid+6);
            xml_add_href(node, NULL, buf_cstring(&fctx->buf));
        }
        else {
            buf_reset(&fctx->buf);
            buf_printf(&fctx->buf, "%s/%s/%s/", namespace_principal.prefix,
                       USER_COLLECTION_PREFIX, userid);
            xml_add_href(node, NULL, buf_cstring(&fctx->buf));
        }

        node = xmlNewChild(ace, NULL, BAD_CAST (deny ? "deny" : "grant"), NULL);
        add_privs(rights, flags, node, resp->parent, fctx->ns);

        if (fctx->req_tgt->resource) {
            node = xmlNewChild(ace, NULL, BAD_CAST "inherited", NULL);
            buf_reset(&fctx->buf);
            buf_printf(&fctx->buf, "%.*s",
                       (int)(fctx->req_tgt->resource - fctx->req_tgt->path),
                       fctx->req_tgt->path);
            xml_add_href(node, NULL, buf_cstring(&fctx->buf));
        }

        userid = nextid;
    }

    if (aclstr) free(aclstr);

    return 0;
}


/* Callback to fetch DAV:acl-restrictions */
int propfind_aclrestrict(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);

    xmlNewChild(node, NULL, BAD_CAST "no-invert", NULL);

    return 0;
}


/* Callback to fetch DAV:principal-collection-set */
EXPORTED int propfind_princolset(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);

    buf_reset(&fctx->buf);
    buf_printf(&fctx->buf, "%s/%s/",
               namespace_principal.prefix, USER_COLLECTION_PREFIX);
    xml_add_href(node, NULL, buf_cstring(&fctx->buf));

    return 0;
}


/* Callback to fetch DAV:quota-available-bytes and DAV:quota-used-bytes */
EXPORTED int propfind_quota(const xmlChar *name, xmlNsPtr ns,
                            struct propfind_ctx *fctx,
                            xmlNodePtr prop __attribute__((unused)),
                            xmlNodePtr resp __attribute__((unused)),
                            struct propstat propstat[],
                            void *rock __attribute__((unused)))
{
    static char prevroot[MAX_MAILBOX_BUFFER];
    char foundroot[MAX_MAILBOX_BUFFER], *qr = NULL;

    if (fctx->mailbox) {
        /* Use the quotaroot as specified in mailbox header */
        qr = fctx->mailbox->quotaroot;
    }
    else if (fctx->req_tgt->mbentry) {
        /* Find the quotaroot governing this hierarchy */
        if (quota_findroot(foundroot, sizeof(foundroot),
                           fctx->req_tgt->mbentry->name)) {
            qr = foundroot;
        }
    }

    if (!qr) return HTTP_NOT_FOUND;

    if (!fctx->quota.root ||
        strcmp(fctx->quota.root, qr)) {
        /* Different quotaroot - read it */

        syslog(LOG_DEBUG, "reading quota for '%s'", qr);

        fctx->quota.root = strcpy(prevroot, qr);

        quota_read(&fctx->quota, NULL, 0);
    }

    buf_reset(&fctx->buf);
    if (!xmlStrcmp(name, BAD_CAST "quota-available-bytes")) {
        /* Calculate limit in bytes and subtract usage */
        quota_t limit =
            fctx->quota.limits[QUOTA_STORAGE] * quota_units[QUOTA_STORAGE];

        buf_printf(&fctx->buf, QUOTA_T_FMT,
                   limit - fctx->quota.useds[QUOTA_STORAGE]);
    }
    else if (fctx->record) {
        /* Bytes used by resource */
        buf_printf(&fctx->buf, "%u", fctx->record->size);
    }
    else if (fctx->mailbox) {
        /* Bytes used by calendar collection */
        buf_printf(&fctx->buf, QUOTA_T_FMT,
                   fctx->mailbox->i.quota_mailbox_used);
    }
    else {
        /* Bytes used by entire hierarchy */
        buf_printf(&fctx->buf, QUOTA_T_FMT, fctx->quota.useds[QUOTA_STORAGE]);
    }

    xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
                 name, ns, BAD_CAST buf_cstring(&fctx->buf), 0);

    return 0;
}


/* Callback to fetch DAV:current-user-principal */
EXPORTED int propfind_curprin(const xmlChar *name, xmlNsPtr ns,
                              struct propfind_ctx *fctx,
                              xmlNodePtr prop,
                              xmlNodePtr resp,
                              struct propstat propstat[],
                              void *rock __attribute__((unused)))
{
    if (httpd_userid) {
        propfind_principalurl(name, ns, fctx,
                              prop, resp, propstat, httpd_userid);
    }
    else {
        xmlNodePtr node =
            xml_add_prop(HTTP_OK, fctx->ns[NS_DAV],
                         &propstat[PROPSTAT_OK], name, ns, NULL, 0);

        xmlNewChild(node, NULL, BAD_CAST "unauthenticated", NULL);
    }

    return 0;
}


/* Callback to fetch DAV:add-member */
int propfind_addmember(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;
    int len;

    if (!fctx->req_tgt->collection ||
        !strcmp(fctx->req_tgt->collection, SCHED_INBOX) ||
        !strcmp(fctx->req_tgt->collection, SCHED_OUTBOX) ||
        (fctx->req_tgt->namespace->id == URL_NS_ADDRESSBOOK &&
         !config_getswitch(IMAPOPT_CARDDAV_ALLOWADDMEMBER))) {
        /* Only allowed on non-scheduling collections */
        return HTTP_NOT_FOUND;
    }

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

    len = fctx->req_tgt->resource ?
        (size_t) (fctx->req_tgt->resource - fctx->req_tgt->path) :
        strlen(fctx->req_tgt->path);
    buf_reset(&fctx->buf);
    buf_printf(&fctx->buf, "%.*s?action=add-member", len, fctx->req_tgt->path);

    xml_add_href(node, NULL, buf_cstring(&fctx->buf));

    return 0;
}


void dav_get_synctoken(struct mailbox *mailbox,
                       struct buf *buf, const char *prefix)
{
    buf_reset(buf);
    buf_printf(buf, "%s%u-" MODSEQ_FMT,
               prefix, mailbox->i.uidvalidity, mailbox->i.highestmodseq);
}

/* Callback to fetch DAV:sync-token and CS:getctag */
int propfind_sync_token(const xmlChar *name, xmlNsPtr ns,
                        struct propfind_ctx *fctx,
                        xmlNodePtr prop __attribute__((unused)),
                        xmlNodePtr resp __attribute__((unused)),
                        struct propstat propstat[],
                        void *rock)
{
    const char *prefix = (const char *) rock;

    if (!fctx->req_tgt->collection || /* until we support sync on cal-home */
        !fctx->mailbox || fctx->record) return HTTP_NOT_FOUND;

    /* not defined on the top-level collection either (aka #calendars) */
    if (!fctx->req_tgt->collection) return HTTP_NOT_FOUND;

    dav_get_synctoken(fctx->mailbox, &fctx->buf, prefix);

    xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
                 name, ns, BAD_CAST buf_cstring(&fctx->buf), 0);

    return 0;
}


/* Callback to fetch MC:bulk-requests */
int propfind_bulkrequests(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->req_tgt->collection && !fctx->req_tgt->flags &&
        !fctx->req_tgt->resource) {
        xmlNodePtr type = xmlNewChild(node, NULL, BAD_CAST "simple", NULL);
        xmlNewChild(type, NULL, BAD_CAST "max-resources", NULL);
        xmlNewChild(type, NULL, BAD_CAST "max-bytes", NULL);
#if 0
        type = xmlNewChild(node, NULL, BAD_CAST "crud", NULL);
        xmlNewChild(type, NULL, BAD_CAST "max-resources", NULL);
        xmlNewChild(type, NULL, BAD_CAST "max-bytes", NULL);
#endif
    }

    return 0;
}


/* Callback to fetch properties from resource header */
int propfind_fromhdr(const xmlChar *name, xmlNsPtr ns,
                     struct propfind_ctx *fctx,
                     xmlNodePtr prop __attribute__((unused)),
                     xmlNodePtr resp __attribute__((unused)),
                     struct propstat propstat[],
                     void *rock)
{
    const char *hdrname = (const char *) rock;
    int r = HTTP_NOT_FOUND;

    if (fctx->record &&
        (mailbox_cached_header(hdrname) != BIT32_MAX) &&
        !mailbox_cacherecord(fctx->mailbox, fctx->record)) {
        unsigned size;
        struct protstream *stream;
        hdrcache_t hdrs = NULL;
        const char **hdr;

        size = cacheitem_size(fctx->record, CACHE_HEADERS);
        stream = prot_readmap(cacheitem_base(fctx->record,
                                             CACHE_HEADERS), size);
        hdrs = spool_new_hdrcache();
        spool_fill_hdrcache(stream, NULL, hdrs, NULL);
        prot_free(stream);

        if ((hdr = spool_getheader(hdrs, (const char *) hdrname))) {
            xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
                         name, ns, BAD_CAST hdr[0], 0);
            r = 0;
        }

        spool_free_hdrcache(hdrs);
    }

    return r;
}

static struct flaggedresources {
    const char *name;
    int flag;
} fres[] = {
    { "answered", FLAG_ANSWERED },
    { "flagged", FLAG_FLAGGED },
    { "seen", FLAG_SEEN },
    { NULL, 0 } /* last is always NULL */
};

/* Callback to write a property to annotation DB */
static int proppatch_toresource(xmlNodePtr prop, unsigned set,
                         struct proppatch_ctx *pctx,
                         struct propstat propstat[],
                         void *rock __attribute__((unused)))
{
    xmlChar *freeme = NULL;
    annotate_state_t *astate = NULL;
    struct buf value = BUF_INITIALIZER;
    int r = 1; /* default to error */

    /* flags only store "exists" */

    if (!strcmp((const char *)prop->ns->href, XML_NS_SYSFLAG)) {
        struct flaggedresources *frp;
        int isset;
        for (frp = fres; frp->name; frp++) {
            if (strcasecmp((const char *)prop->name, frp->name)) continue;
            r = 0; /* ok to do nothing */
            isset = pctx->record->system_flags & frp->flag;
            if (set) {
                if (isset) goto done;
                pctx->record->system_flags |= frp->flag;
            }
            else {
                if (!isset) goto done;
                pctx->record->system_flags &= ~frp->flag;
            }
            r = mailbox_rewrite_index_record(pctx->mailbox, pctx->record);
            goto done;
        }
        goto done;
    }

    if (!strcmp((const char *)prop->ns->href, XML_NS_USERFLAG)) {
        int userflag = 0;
        int isset;
        r = mailbox_user_flag(pctx->mailbox, (const char *)prop->name, &userflag, 1);
        if (r) goto done;
        isset = pctx->record->user_flags[userflag/32] & (1<<userflag%31);
        if (set) {
            if (isset) goto done;
            pctx->record->user_flags[userflag/32] |= (1<<userflag%31);
        }
        else {
            if (!isset) goto done;
            pctx->record->user_flags[userflag/32] &= ~(1<<userflag%31);
        }
        r = mailbox_rewrite_index_record(pctx->mailbox, pctx->record);
        goto done;
    }

    /* otherwise it's a database annotation */

    buf_reset(&pctx->buf);
    buf_printf(&pctx->buf, DAV_ANNOT_NS "<%s>%s",
               (const char *) prop->ns->href, prop->name);

    if (set) {
        freeme = xmlNodeGetContent(prop);
        buf_init_ro_cstr(&value, (const char *)freeme);
    }

    r = mailbox_get_annotate_state(pctx->mailbox, pctx->record->uid, &astate);
    if (!r) r = annotate_state_writemask(astate, buf_cstring(&pctx->buf),
                                         httpd_userid, &value);
    /* we need to rewrite the record to update the modseq because the layering
     * of annotations and mailboxes is broken */
    if (!r) r = mailbox_rewrite_index_record(pctx->mailbox, pctx->record);

 done:

    if (!r) {
        xml_add_prop(HTTP_OK, pctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
                     prop->name, prop->ns, NULL, 0);
    }
    else {
        xml_add_prop(HTTP_SERVER_ERROR, pctx->ns[NS_DAV],
                     &propstat[PROPSTAT_ERROR], prop->name, prop->ns, NULL, 0);
    }

    buf_free(&value);
    if (freeme) xmlFree(freeme);

    return 0;
}


/* Callback to read a property from annotation DB */
static int propfind_fromresource(const xmlChar *name, xmlNsPtr ns,
                                 struct propfind_ctx *fctx,
                                 xmlNodePtr prop __attribute__((unused)),
                                 xmlNodePtr resp __attribute__((unused)),
                                 struct propstat propstat[],
                                 void *rock __attribute__((unused)))
{
    struct buf attrib = BUF_INITIALIZER;
    xmlNodePtr node;
    int r = 0; /* default no error */

    if (!strcmp((const char *)ns->href, XML_NS_SYSFLAG)) {
        struct flaggedresources *frp;
        int isset;
        for (frp = fres; frp->name; frp++) {
            if (strcasecmp((const char *)name, frp->name)) continue;
            isset = fctx->record->system_flags & frp->flag;
            if (isset)
                buf_setcstr(&attrib, "1");
            goto done;
        }
        goto done;
    }

    if (!strcmp((const char *)ns->href, XML_NS_USERFLAG)) {
        int userflag = 0;
        int isset;
        r = mailbox_user_flag(fctx->mailbox, (const char *)name, &userflag, 0);
        if (r) goto done;
        isset = fctx->record->user_flags[userflag/32] & (1<<userflag%31);
        if (isset)
            buf_setcstr(&attrib, "1");
        goto done;
    }

    /* otherwise it's a DB annotation */

    buf_reset(&fctx->buf);
    buf_printf(&fctx->buf, DAV_ANNOT_NS "<%s>%s",
               (const char *) ns->href, name);

    r = annotatemore_msg_lookup(fctx->mailbox->name, fctx->record->uid,
                                buf_cstring(&fctx->buf), NULL, &attrib);

done:
    if (r) r = HTTP_SERVER_ERROR;
    else if (!buf_len(&attrib)) r = HTTP_NOT_FOUND;

    if (!r) {
        node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
                            name, ns, NULL, 0);
        xmlAddChild(node, xmlNewCDataBlock(fctx->root->doc,
                                           BAD_CAST buf_cstring(&attrib),
                                           buf_len(&attrib)));
    }

    buf_free(&attrib);

    return r;
}


/* Callback to read a property from annotation DB */
int propfind_fromdb(const xmlChar *name, xmlNsPtr ns,
                    struct propfind_ctx *fctx,
                    xmlNodePtr prop, xmlNodePtr resp,
                    struct propstat propstat[], void *rock)
{
    struct buf attrib = BUF_INITIALIZER;
    xmlNodePtr node;
    int r = 0;

    if (fctx->req_tgt->resource) {
        if (!fctx->record) return HTTP_NOT_FOUND;
        return propfind_fromresource(name, ns, fctx, prop, resp, propstat, rock);
    }

    buf_reset(&fctx->buf);
    buf_printf(&fctx->buf, DAV_ANNOT_NS "<%s>%s",
               (const char *) ns->href, name);

    if (fctx->mbentry && !fctx->record) {
        r = annotatemore_lookupmask(fctx->mbentry->name,
                                    buf_cstring(&fctx->buf),
                                    httpd_userid, &attrib);
    }

    if (r) return HTTP_SERVER_ERROR;
    if (!buf_len(&attrib)) return HTTP_NOT_FOUND;

    node = xml_add_prop(HTTP_OK, fctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
                        name, ns, NULL, 0);
    xmlAddChild(node, xmlNewCDataBlock(fctx->root->doc,
                                       BAD_CAST buf_cstring(&attrib),
                                       buf_len(&attrib)));

    buf_free(&attrib);

    return 0;
}

/* Callback to write a property to annotation DB */
int proppatch_todb(xmlNodePtr prop, unsigned set,
                   struct proppatch_ctx *pctx,
                   struct propstat propstat[],
                   void *rock)
{
    xmlChar *freeme = NULL;
    annotate_state_t *astate = NULL;
    struct buf value = BUF_INITIALIZER;
    int r;

    if (pctx->txn->req_tgt.resource)
        return proppatch_toresource(prop, set, pctx, propstat, NULL);

    buf_reset(&pctx->buf);
    buf_printf(&pctx->buf, DAV_ANNOT_NS "<%s>%s",
               (const char *) prop->ns->href, prop->name);

    if (set) {
        if (rock) {
            buf_init_ro_cstr(&value, (const char *)rock);
        }
        else {
            freeme = xmlNodeGetContent(prop);
            buf_init_ro_cstr(&value, (const char *)freeme);
        }
    }

    r = mailbox_get_annotate_state(pctx->mailbox, 0, &astate);
    if (!r) r = annotate_state_writemask(astate, buf_cstring(&pctx->buf),
                                         httpd_userid, &value);

    if (!r) {
        xml_add_prop(HTTP_OK, pctx->ns[NS_DAV], &propstat[PROPSTAT_OK],
                     prop->name, prop->ns, NULL, 0);
    }
    else {
        xml_add_prop(HTTP_SERVER_ERROR, pctx->ns[NS_DAV],
                     &propstat[PROPSTAT_ERROR], prop->name, prop->ns, NULL, 0);
    }

    buf_free(&value);
    if (freeme) xmlFree(freeme);

    return 0;
}

/* annotatemore_findall callback for adding dead properties (allprop/propname) */
static int allprop_cb(const char *mailbox __attribute__((unused)),
                      uint32_t uid __attribute__((unused)),
                      const char *entry,
                      const char *userid, const struct buf *attrib,
                      const struct annotate_metadata *mdata __attribute__((unused)),
                      void *rock)
{
    struct allprop_rock *arock = (struct allprop_rock *) rock;
    const struct prop_entry *pentry;
    char *href, *name;
    xmlNsPtr ns;
    xmlNodePtr node;

    /* Make sure its a shared entry or the user's private one */
    if (userid && *userid && strcmp(userid, arock->fctx->userid)) return 0;

    /* Split entry into namespace href and name ( <href>name ) */
    buf_setcstr(&arock->fctx->buf, entry + strlen(DAV_ANNOT_NS) + 1);
    href = (char *) buf_cstring(&arock->fctx->buf);
    if ((name = strchr(href, '>'))) *name++ = '\0';
    else if ((name = strchr(href, ':'))) *name++ = '\0';

    /* Look for a match against live properties */
    for (pentry = arock->fctx->lprops;
         pentry->name &&
             (strcmp(name, pentry->name) ||
              strcmp(href, known_namespaces[pentry->ns].href));
         pentry++);

    if (pentry->name &&
        (arock->fctx->mode == PROPFIND_ALL    /* Skip all live properties */
         || (pentry->flags & PROP_ALLPROP)))  /* Skip those already included */
        return 0;

    /* Look for an instance of this namespace in our response */
    ns = hash_lookup(href, arock->fctx->ns_table);

    /* XXX - can return the same property multiple times with annotate masks! */

    /* Add the dead property to the response */
    node = xml_add_prop(HTTP_OK, arock->fctx->ns[NS_DAV],
                        &arock->propstat[PROPSTAT_OK],
                        BAD_CAST name, ns, NULL, 0);
    if (!ns) {
        /* Add the namespace directly to the property -
           its too late to add it to the root when chunking the responses */
        char prefix[9];
        snprintf(prefix, sizeof(prefix), "X%X", strhash(href));
        xmlSetNs(node, xmlNewNs(node, BAD_CAST href, BAD_CAST prefix));
    }

    if (arock->fctx->mode == PROPFIND_ALL) {
        xmlAddChild(node, xmlNewCDataBlock(arock->fctx->root->doc,
                                           BAD_CAST attrib->s, attrib->len));
    }

    return 0;
}


static int prescreen_prop(const struct prop_entry *entry,
                          xmlNodePtr prop,
                          struct propfind_ctx *fctx)
{
    unsigned allowed = 1;

    if (fctx->req_tgt->resource && !(entry->flags & PROP_RESOURCE)) allowed = 0;
    else if (entry->flags & PROP_PRESCREEN) {
        allowed = !entry->get(BAD_CAST entry->name, NULL, fctx,
                              prop, NULL, NULL, entry->rock);
    }

    return allowed;
}


/* Parse the requested properties and create a linked list of fetch callbacks.
 * The list gets reused for each href if Depth > 0
 */
HIDDEN int preload_proplist(xmlNodePtr proplist, struct propfind_ctx *fctx)
{
    int ret = 0;
    xmlNodePtr prop;
    const struct prop_entry *entry;
    struct propfind_entry_list *tail = NULL;

    switch (fctx->mode) {
    case PROPFIND_ALL:
    case PROPFIND_NAME:
        /* Add live properties for allprop/propname */
        for (entry = fctx->lprops; entry->name; entry++) {
            if (entry->flags & PROP_ALLPROP) {
                /* Pre-screen request based on prop flags */
                int allowed = prescreen_prop(entry, NULL, fctx);

                if (allowed || fctx->mode == PROPFIND_ALL) {
                    struct propfind_entry_list *nentry =
                        xzmalloc(sizeof(struct propfind_entry_list));

                    ensure_ns(fctx->ns, entry->ns, fctx->root,
                              known_namespaces[entry->ns].href,
                              known_namespaces[entry->ns].prefix);

                    nentry->name = xmlStrdup(BAD_CAST entry->name);
                    nentry->ns = fctx->ns[entry->ns];
                    if (allowed) {
                        nentry->flags = entry->flags;
                        nentry->get = entry->get;
                        nentry->prop = NULL;
                        nentry->rock = entry->rock;
                    }

                    /* Append new entry to linked list */
                    if (tail) {
                        tail->next = nentry;
                        tail = nentry;
                    }
                    else tail = fctx->elist = nentry;
                }
            }
        }
        /* Fall through and build hash table of namespaces */
        GCC_FALLTHROUGH

    case PROPFIND_EXPAND:
        /* Add all namespaces attached to the response to our hash table */
        if (!fctx->ns_table->size) {
            xmlNsPtr nsDef;

            construct_hash_table(fctx->ns_table, 10, 1);

            for (nsDef = fctx->root->nsDef; nsDef; nsDef = nsDef->next) {
                hash_insert((const char *) nsDef->href, nsDef, fctx->ns_table);
            }
        }
    }

    /* Iterate through requested properties */
    for (prop = proplist; !*fctx->ret && prop; prop = prop->next) {
        if (prop->type == XML_ELEMENT_NODE) {
            struct propfind_entry_list *nentry;
            xmlChar *name, *namespace = NULL;
            xmlNsPtr ns;
            const char *ns_href;
            unsigned i;

            if (!prop->ns) return HTTP_BAD_REQUEST;

            ns = prop->ns;
            ns_href = (const char *) ns->href;

            if (fctx->mode == PROPFIND_EXPAND) {
                /* Get name/namespace from <property> */
                name = xmlGetProp(prop, BAD_CAST "name");
                namespace = xmlGetProp(prop, BAD_CAST "namespace");

                if (namespace) {
                    ns_href = (const char *) namespace;
                    ns = NULL;
                }
            }
            else {
                /* node IS the property */
                name = xmlStrdup(prop->name);
            }

            /* Look for this namespace in our known array */
            for (i = 0; i < NUM_NAMESPACE; i++) {
                if (!strcmp(ns_href, known_namespaces[i].href)) {
                    ensure_ns(fctx->ns, i, fctx->root,
                              known_namespaces[i].href,
                              known_namespaces[i].prefix);
                    ns = fctx->ns[i];
                    break;
                }
            }

            if (namespace) {
                if (!ns) {
                    /* Look for namespace in hash table */
                    ns = hash_lookup(ns_href, fctx->ns_table);
                    if (!ns) {
                        char prefix[6];
                        snprintf(prefix, sizeof(prefix),
                                 "X%X", strhash(ns_href) & 0xffff);
                        ns = xmlNewNs(fctx->root,
                                      BAD_CAST ns_href, BAD_CAST prefix);
                        hash_insert(ns_href, ns, fctx->ns_table);
                    }
                }
                xmlFree(namespace);
            }

            /* Look for a match against our known properties */
            for (entry = fctx->lprops;
                 entry->name &&
                     (strcmp((const char *) name, entry->name) ||
                      strcmp((const char *) ns->href,
                             known_namespaces[entry->ns].href));
                 entry++);

            /* Skip properties already included by allprop */
            if (fctx->mode == PROPFIND_ALL && (entry->flags & PROP_ALLPROP)) {
                xmlFree(name);
                continue;
            }

            nentry = xzmalloc(sizeof(struct propfind_entry_list));
            nentry->name = name;
            nentry->ns = ns;
            if (entry->name) {
                /* Found a match - Pre-screen request based on prop flags */
                if (prescreen_prop(entry, prop, fctx)) {
                    nentry->flags = entry->flags;
                    nentry->get = entry->get;
                    nentry->prop = prop;
                    nentry->rock = entry->rock;
                }
                ret = *fctx->ret;
            }
            else {
                /* No match, treat as a dead property.
                   Need to look at both collections and resources */
                nentry->flags = PROP_COLLECTION | PROP_RESOURCE;
                nentry->get = propfind_fromdb;
                nentry->prop = NULL;
                nentry->rock = NULL;
            }

            /* Append new entry to linked list */
            if (tail) {
                tail->next = nentry;
                tail = nentry;
            }
            else tail = fctx->elist = nentry;
        }
    }

    return ret;
}


/* Execute the given property patch instructions */
static int do_proppatch(struct proppatch_ctx *pctx, xmlNodePtr instr)
{
    struct propstat propstat[NUM_PROPSTAT];
    int i;

    memset(propstat, 0, NUM_PROPSTAT * sizeof(struct propstat));

    /* Iterate through propertyupdate children */
    for (; instr; instr = instr->next) {
        if (instr->type == XML_ELEMENT_NODE) {
            xmlNodePtr prop;
            unsigned set = 0;

            if (!xmlStrcmp(instr->name, BAD_CAST "set")) set = 1;
            else if ((pctx->txn->meth == METH_PROPPATCH) &&
                     !xmlStrcmp(instr->name, BAD_CAST "remove")) set = 0;
            else {
                syslog(LOG_INFO, "Unknown PROPPATCH instruction");
                pctx->txn->error.desc = "Unknown PROPPATCH instruction";
                return HTTP_BAD_REQUEST;
            }

            /* Find child element */
            for (prop = instr->children;
                 prop && prop->type != XML_ELEMENT_NODE; prop = prop->next);
            if (!prop || xmlStrcmp(prop->name, BAD_CAST "prop")) {
                pctx->txn->error.desc = "Missing prop element";
                return HTTP_BAD_REQUEST;
            }

            /* Iterate through requested properties */
            for (prop = prop->children; prop; prop = prop->next) {
                if (prop->type == XML_ELEMENT_NODE) {
                    const struct prop_entry *entry;

                    /* Look for a match against our known properties */
                    for (entry = pctx->lprops;
                         entry->name &&
                             (strcmp((const char *) prop->name, entry->name) ||
                              !prop->ns ||
                              strcmp((const char *) prop->ns->href,
                                     known_namespaces[entry->ns].href));
                         entry++);

                    if (entry->name) {
                        int rights = httpd_myrights(httpd_authstate,
                                                    pctx->txn->req_tgt.mbentry);
                        if (!entry->put) {
                            /* Protected property */
                            xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV],
                                         &propstat[PROPSTAT_FORBID],
                                         prop->name, prop->ns, NULL,
                                         DAV_PROT_PROP);
                            *pctx->ret = HTTP_FORBIDDEN;
                        }
                        else if ((pctx->txn->meth == METH_PROPPATCH) &&
                                 !(rights & ((entry->flags & PROP_PERUSER) ?
                                             DACL_READ : DACL_PROPCOL))) {
                            /* DAV:need-privileges */
                            xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV],
                                         &propstat[PROPSTAT_FORBID],
                                         prop->name, prop->ns, NULL,
                                         DAV_NEED_PRIVS);
                            *pctx->ret = HTTP_FORBIDDEN;
                        }
                        else {
                            /* Write "live" property */
                            entry->put(prop, set, pctx, propstat, entry->rock);
                        }
                    }
                    else if (!prop->ns) {
                        /* Property with no namespace */
                        xmlNodePtr newprop =
                            xml_add_prop(HTTP_FORBIDDEN, pctx->ns[NS_DAV],
                                         &propstat[PROPSTAT_FORBID],
                                         prop->name, NULL, NULL, 0);
                        xmlSetNs(newprop, NULL);
                        *pctx->ret = HTTP_FORBIDDEN;
                    }
                    else if (pctx->txn->req_tgt.namespace->id != URL_NS_PRINCIPAL) {
                        /* Write "dead" property */
                        proppatch_todb(prop, set, pctx, propstat, NULL);
                    }
                }
            }
        }
    }

    /* One or more of the properties failed */
    if (*pctx->ret && propstat[PROPSTAT_OK].root) {
        /* 200 status must become 424 */
        propstat[PROPSTAT_FAILEDDEP].root = propstat[PROPSTAT_OK].root;
        propstat[PROPSTAT_FAILEDDEP].status = HTTP_FAILED_DEP;
        propstat[PROPSTAT_OK].root = NULL;
    }

    /* Add status and optional error to the propstat elements
       and then add them to the response element */
    for (i = 0; i < NUM_PROPSTAT; i++) {
        struct propstat *stat = &propstat[i];

        if (stat->root) {
            xmlNewChild(stat->root, NULL, BAD_CAST "status",
                        BAD_CAST http_statusline(VER_1_1, stat->status));
            if (stat->precond) {
                struct error_t error = { NULL, stat->precond, NULL, NULL, 0 };
                xml_add_error(stat->root, &error, pctx->ns);
            }

            xmlAddChild(pctx->root, stat->root);
        }
    }

    return 0;
}


/* Parse an XML body into a tree */
int parse_xml_body(struct transaction_t *txn, xmlNodePtr *root,
                   const char *spec_type)
{
    const char **hdr;
    xmlDocPtr doc = NULL;
    int r = 0;

    *root = NULL;

    /* Read body */
    txn->req_body.flags |= BODY_DECODE;
    r = http_read_req_body(txn);
    if (r) {
        txn->flags.conn = CONN_CLOSE;
        return r;
    }

    if (!buf_len(&txn->req_body.payload)) return 0;

    /* Check Content-Type */
    if (!(hdr = spool_getheader(txn->req_hdrs, "Content-Type")) ||
        (!is_mediatype("text/xml", hdr[0]) &&
         !is_mediatype("application/xml", hdr[0]) &&
         !(spec_type && is_mediatype(spec_type, hdr[0])))) {
        txn->error.desc = "This method requires an XML body";
        return HTTP_BAD_MEDIATYPE;
    }

    /* Parse the XML request */
    doc = xmlCtxtReadMemory(txn->conn->xml, buf_cstring(&txn->req_body.payload),
                            buf_len(&txn->req_body.payload), NULL, NULL,
                            XML_PARSE_NOERROR | XML_PARSE_NOWARNING);
    if (!doc) {
        txn->error.desc = "Unable to parse XML body";
        return HTTP_BAD_REQUEST;
    }
    else {
        xmlErrorPtr err = xmlCtxtGetLastError(txn->conn->xml);
        if (err) {
            xmlFreeDoc(doc);
            txn->error.desc = err->message;
            return HTTP_BAD_REQUEST;
        }
    }

    /* Get the root element of the XML request */
    if (!(*root = xmlDocGetRootElement(doc))) {
        xmlFreeDoc(doc);
        txn->error.desc = "Missing root element in request";
        return HTTP_BAD_REQUEST;
    }

    return 0;
}

/* Perform an ACL request
 *
 * preconditions:
 *   DAV:no-ace-conflict
 *   DAV:no-protected-ace-conflict
 *   DAV:no-inherited-ace-conflict
 *   DAV:limited-number-of-aces
 *   DAV:deny-before-grant
 *   DAV:grant-only
 *   DAV:no-invert
 *   DAV:no-abstract
 *   DAV:not-supported-privilege
 *   DAV:missing-required-principal
 *   DAV:recognized-principal
 *   DAV:allowed-principal
 *
 * The standard behavior of the ACL method is to completely replace the existing
 * ACL with the one in the request.  We treat the <deny> element as providing
 * "negative" rights (-identifier) in IMAP-speak.  Additionally, we treat
 * the special DAV identifiers as follows:
 *
 *   <all> == IMAP "anyone"
 *   <unauthenticated> == IMAP "anonymous"
 *   <authenticated> == IMAP "anyone -anonymous"
 */
int meth_acl(struct transaction_t *txn, void *params)
{
    struct meth_params *aparams = (struct meth_params *) params;
    int ret = 0, r, rights;
    xmlDocPtr indoc = NULL;
    xmlNodePtr root, ace;
    struct mailbox *mailbox = NULL;
    struct buf acl = BUF_INITIALIZER;

    /* Response should not be cached */
    txn->flags.cc |= CC_NOCACHE;

    /* Parse the path */
    r = dav_parse_req_target(txn, aparams);
    if (r) return r;

    /* Make sure method is allowed (only allowed on collections) */
    if (!(txn->req_tgt.allow & ALLOW_ACL)) {
        txn->error.desc = "ACLs can only be set on collections\r\n";
        syslog(LOG_DEBUG, "Tried to set ACL on non-collection");
        return HTTP_NOT_ALLOWED;
    }

    if (!mboxname_userownsmailbox(httpd_userid, txn->req_tgt.mbentry->name)) {
        /* 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 */

    /* Parse the ACL body */
    ret = parse_xml_body(txn, &root, NULL);
    if (!ret && !root) {
        txn->error.desc = "Missing request body\r\n";
        ret = HTTP_BAD_REQUEST;
    }
    if (ret) goto done;

    indoc = root->doc;

    /* Make sure its an DAV:acl element */
    if (!root->ns || xmlStrcmp(root->ns->href, BAD_CAST XML_NS_DAV) ||
        xmlStrcmp(root->name, BAD_CAST "acl")) {
        txn->error.desc = "Missing DAV:acl element in ACL request";
        ret = HTTP_BAD_REQUEST;
        goto done;
    }

    /* Open mailbox for writing */
    r = mailbox_open_iwl(txn->req_tgt.mbentry->name, &mailbox);
    if (r) {
        syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s",
               txn->req_tgt.mbentry->name, error_message(r));
        txn->error.desc = error_message(r);
        ret = HTTP_SERVER_ERROR;
        goto done;
    }

    /* Parse the DAV:ace elements */
    for (ace = root->children; ace; ace = ace->next) {
        if (ace->type == XML_ELEMENT_NODE) {
            xmlNodePtr child = NULL, prin = NULL, privs = NULL;
            const char *userid = NULL;
            int deny = 0, rights = 0;
            char *freeme = NULL;
            char rightstr[100];
            struct request_target_t tgt;

            for (child = ace->children; child; child = child->next) {
                if (child->type == XML_ELEMENT_NODE) {
                    if (!xmlStrcmp(child->name, BAD_CAST "principal")) {
                        if (prin) {
                            txn->error.desc = "Multiple principals in ACE\r\n";
                            ret = HTTP_BAD_REQUEST;
                            goto done;
                        }

                        for (prin = child->children; prin &&
                             prin->type != XML_ELEMENT_NODE; prin = prin->next);
                        if (!prin) {
                            txn->error.desc = "Empty principal in ACE\r\n";
                            ret = HTTP_BAD_REQUEST;
                            goto done;
                        }
                    }
                    else if (!xmlStrcmp(child->name, BAD_CAST "grant")) {
                        if (privs) {
                            txn->error.desc = "Multiple grant|deny in ACE\r\n";
                            ret = HTTP_BAD_REQUEST;
                            goto done;
                        }

                        for (privs = child->children; privs &&
                             privs->type != XML_ELEMENT_NODE; privs = privs->next);
                    }
                    else if (!xmlStrcmp(child->name, BAD_CAST "deny")) {
                        if (privs) {
                            txn->error.desc = "Multiple grant|deny in ACE\r\n";
                            ret = HTTP_BAD_REQUEST;
                            goto done;
                        }

                        for (privs = child->children; privs &&
                             privs->type != XML_ELEMENT_NODE; privs = privs->next);
                        deny = 1;
                    }
                    else if (!xmlStrcmp(child->name, BAD_CAST "invert")) {
                        /* DAV:no-invert */
                        txn->error.precond = DAV_NO_INVERT;
                        ret = HTTP_FORBIDDEN;
                        goto done;
                    }
                    else {
                        txn->error.desc = "Unknown element in ACE\r\n";
                        ret = HTTP_BAD_REQUEST;
                        goto done;
                    }
                }
            }

            if (!xmlStrcmp(prin->name, BAD_CAST "self")) {
                userid = httpd_userid;
            }
            else if (!xmlStrcmp(prin->name, BAD_CAST "owner")) {
                userid = freeme = mboxname_to_userid(txn->req_tgt.mbentry->name);
            }
            else if (!xmlStrcmp(prin->name, BAD_CAST "all")) {
                userid = "anyone";
            }
            else if (!xmlStrcmp(prin->name, BAD_CAST "authenticated")) {
                if (deny) {
                    /* DAV:grant-only */
                    txn->error.precond = DAV_GRANT_ONLY;
                    ret = HTTP_FORBIDDEN;
                    goto done;
                }
                userid = "\a"; /* flagged for use below */
            }
            else if (!xmlStrcmp(prin->name, BAD_CAST "unauthenticated")) {
                userid = "anonymous";
            }
            else if (!xmlStrcmp(prin->name, BAD_CAST "href")) {
                xmlChar *href = xmlNodeGetContent(prin);
                xmlURIPtr uri;
                const char *errstr = NULL;
                size_t plen = strlen(namespace_principal.prefix);

                uri = parse_uri(METH_UNKNOWN, (const char *) href, 1, &errstr);
                if (uri &&
                    !strncmp(namespace_principal.prefix, uri->path, plen) &&
                    uri->path[plen] == '/') {
                    memset(&tgt, 0, sizeof(struct request_target_t));
                    tgt.namespace = &namespace_principal;
                    /* XXX: there is no doubt that this leaks memory */
                    r = principal_parse_path(uri->path, &tgt, &errstr);
                    if (!r && tgt.userid) userid = tgt.userid;
                }
                if (uri) xmlFreeURI(uri);
                xmlFree(href);
            }

            if (!userid) {
                /* DAV:recognized-principal */
                txn->error.precond = DAV_RECOG_PRINC;
                ret = HTTP_FORBIDDEN;
                free(freeme);
                goto done;
            }

            for (; privs; privs = privs->next) {
                if (privs->type == XML_ELEMENT_NODE) {
                    xmlNodePtr priv = privs->children;
                    for (; priv->type != XML_ELEMENT_NODE; priv = priv->next);

                    if (aparams->acl_ext &&
                        aparams->acl_ext(txn, priv, &rights)) {
                        /* Extension (CalDAV) privileges */
                        if (txn->error.precond) {
                            ret = HTTP_FORBIDDEN;
                            free(freeme);
                            goto done;
                        }
                    }
                    else if (!xmlStrcmp(priv->ns->href,
                                        BAD_CAST XML_NS_DAV)) {
                        /* WebDAV privileges */
                        if (!xmlStrcmp(priv->name,
                                       BAD_CAST "all")) {
                            if (deny)
                                rights |= ACL_FULL; /* wipe EVERYTHING */
                            else
                                rights |= DACL_ALL;
                        }
                        else if (!xmlStrcmp(priv->name,
                                            BAD_CAST "read"))
                            rights |= DACL_READ;
                        else if (!xmlStrcmp(priv->name,
                                            BAD_CAST "write"))
                            rights |= DACL_WRITE;
                        else if (!xmlStrcmp(priv->name,
                                            BAD_CAST "write-content"))
                            rights |= DACL_WRITECONT;
                        else if (!xmlStrcmp(priv->name,
                                            BAD_CAST "write-properties"))
                            rights |= DACL_WRITEPROPS;
                        else if (!xmlStrcmp(priv->name,
                                            BAD_CAST "bind"))
                            rights |= DACL_BIND;
                        else if (!xmlStrcmp(priv->name,
                                            BAD_CAST "unbind"))
                            rights |= DACL_UNBIND;
                        else if (!xmlStrcmp(priv->name,
                                            BAD_CAST "read-current-user-privilege-set")
                                 || !xmlStrcmp(priv->name,
                                               BAD_CAST "read-acl")
                                 || !xmlStrcmp(priv->name,
                                               BAD_CAST "write-acl")
                                 || !xmlStrcmp(priv->name,
                                               BAD_CAST "unlock")
                                 || !xmlStrcmp(priv->name,
                                               BAD_CAST "share")) {
                            /* DAV:no-abstract */
                            txn->error.precond = DAV_NO_ABSTRACT;
                            ret = HTTP_FORBIDDEN;
                            free(freeme);
                            goto done;
                        }
                        else {
                            /* DAV:not-supported-privilege */
                            txn->error.precond = DAV_SUPP_PRIV;
                            ret = HTTP_FORBIDDEN;
                            free(freeme);
                            goto done;
                        }
                    }
                    else if (!xmlStrcmp(priv->ns->href,
                                   BAD_CAST XML_NS_CALDAV)) {
                        if (!xmlStrcmp(priv->name,
                                       BAD_CAST "read-free-busy"))
                            rights |= DACL_READFB;
                        else {
                            /* DAV:not-supported-privilege */
                            txn->error.precond = DAV_SUPP_PRIV;
                            ret = HTTP_FORBIDDEN;
                            free(freeme);
                            goto done;
                        }
                    }
                    else if (!xmlStrcmp(priv->ns->href,
                                   BAD_CAST XML_NS_CYRUS)) {
                        /* Cyrus-specific privileges */
                        if (!xmlStrcmp(priv->name,
                                       BAD_CAST "write-properties-collection"))
                            rights |= DACL_PROPCOL;
                        else if (!xmlStrcmp(priv->name,
                                       BAD_CAST "write-properties-resource"))
                            rights |= DACL_PROPRSRC;
                        else if (!xmlStrcmp(priv->name,
                                       BAD_CAST "make-collection"))
                            rights |= DACL_MKCOL;
                        else if (!xmlStrcmp(priv->name,
                                       BAD_CAST "remove-collection"))
                            rights |= DACL_RMCOL;
                        else if (!xmlStrcmp(priv->name,
                                       BAD_CAST "add-resource"))
                            rights |= DACL_ADDRSRC;
                        else if (!xmlStrcmp(priv->name,
                                       BAD_CAST "remove-resource"))
                            rights |= DACL_RMRSRC;
                        else if (!xmlStrcmp(priv->name,
                                       BAD_CAST "admin"))
                            rights |= DACL_ADMIN;
                        else {
                            /* DAV:not-supported-privilege */
                            txn->error.precond = DAV_SUPP_PRIV;
                            ret = HTTP_FORBIDDEN;
                            free(freeme);
                            goto done;
                        }
                    }
                    else {
                        /* DAV:not-supported-privilege */
                        txn->error.precond = DAV_SUPP_PRIV;
                        ret = HTTP_FORBIDDEN;
                        free(freeme);
                        goto done;
                    }
                }
            }

            /* gotta have something to do! */
            if (rights) {
                cyrus_acl_masktostr(rights, rightstr);

                if (*userid == '\a') {
                    /* authenticated = "anyone -anonymous" */
                    buf_printf(&acl, "anyone\t%s\t-anonymous\t%s\t",
                               rightstr, rightstr);
                }
                else {
                    buf_printf(&acl, "%s%s\t%s\t",
                               deny ? "-" : "", userid, rightstr);
                }
            }

            free(freeme);
        }
    }

    r = mboxlist_sync_setacls(txn->req_tgt.mbentry->name, buf_cstring(&acl), mailbox_modseq_dirty(mailbox));
    if (!r) r = mailbox_set_acl(mailbox, buf_cstring(&acl));
    if (!r) {
        char *userid = mboxname_to_userid(txn->req_tgt.mbentry->name);
        r = caldav_update_shareacls(userid);
        free(userid);
    }
    if (r) {
        syslog(LOG_ERR, "mboxlist_sync_setacls(%s) failed: %s",
               txn->req_tgt.mbentry->name, error_message(r));
        txn->error.desc = error_message(r);
        ret = HTTP_SERVER_ERROR;
        goto done;
    }

    response_header(HTTP_OK, txn);

  done:
    buf_free(&acl);
    if (indoc) xmlFreeDoc(indoc);
    mailbox_close(&mailbox);
    return ret;
}


struct move_rock {
    int omlen;
    int nmlen;
    struct buf newname;
    const char *urlprefix;
    xmlNodePtr root;
    xmlNsPtr ns[NUM_NAMESPACE];
};

/* Callback for use by dav_move_collection() to move single collection */
static int move_collection(const mbentry_t *mbentry, void *rock)
{
    struct move_rock *mrock = (struct move_rock *) rock;
    int r = 0;

    buf_truncate(&mrock->newname, mrock->nmlen);
    buf_appendcstr(&mrock->newname, mbentry->name + mrock->omlen);

    if (buf_len(&mrock->newname) >= MAX_MAILBOX_NAME) {
        r = IMAP_MAILBOX_BADNAME;
    }
    else {
        /* Rename mailbox -
           Pretend we're an admin since we already renamed the parent */
        r = mboxlist_renamemailbox(mbentry, buf_cstring(&mrock->newname),
                                   NULL /* partition */, 0 /* uidvalidity */,
                                   1 /* admin */, httpd_userid, httpd_authstate,
                                   NULL, 0, 0, 1 /* ignorequota */, 0, 0, 0);
    }

    if (r) {
        struct error_t err = { error_message(r), 0, NULL, NULL, 0 };
        struct buf href = BUF_INITIALIZER;
        xmlNodePtr resp;
        mbname_t *mbname;
        const strarray_t *boxes;
        int n, size, code;

        if (!mrock->root) {
            /* Create new <multistatus> */
            mrock->root =
                init_xml_response("multistatus", NS_DAV, NULL, mrock->ns);
            if (!mrock->root) return HTTP_SERVER_ERROR;
        }

        /* Add new <response> element */
        resp = xmlNewChild(mrock->root, mrock->ns[NS_DAV],
                           BAD_CAST "response", NULL);
        if (!resp) return HTTP_SERVER_ERROR;

        /* Generate href for destination collection */
        mbname = mbname_from_intname(buf_cstring(&mrock->newname));

        buf_setcstr(&href, mrock->urlprefix);

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

            buf_printf(&href, "/%s/%s",
                       USER_COLLECTION_PREFIX, mbname_localpart(mbname));
            if (domain) buf_printf(&href, "@%s", domain);
        }
        buf_putc(&href, '/');

        boxes = mbname_boxes(mbname);
        size = strarray_size(boxes);
        for (n = 1; n < size; n++) {
            buf_appendcstr(&href, strarray_nth(boxes, n));
            buf_putc(&href, '/');
        }
        mbname_free(&mbname);

        /* Add <href> element */
        xml_add_href(resp, NULL, buf_cstring(&href));

        /* Determine HTTP response code */
        switch (r) {
        case IMAP_MAILBOX_BADNAME:
            code = HTTP_FORBIDDEN;
            break;

        case IMAP_MAILBOX_EXISTS:
            code = HTTP_PRECOND_FAILED;
            break;

        case IMAP_PERMISSION_DENIED:
            code = HTTP_FORBIDDEN;
            err.precond = DAV_NEED_PRIVS;
            err.rights = DACL_UNBIND;
            err.resource = buf_cstring(&href);
            break;

        default:
            code = HTTP_SERVER_ERROR;
            break;
        }

        /* Add <status> element */
        xmlNewChild(resp, NULL, BAD_CAST "status",
                    BAD_CAST http_statusline(VER_1_1, code));

        /* Add <error> element */
        xml_add_error(resp, &err, mrock->ns);

        buf_free(&href);

        /* XXX  Per RFC 4918, we SHOULD continue to move non-children */
    }

    return r;
}

/* Callback for use by dav_move_collection() ro remove a single collection */
static int remove_collection(const mbentry_t *mbentry,
                             void *rock __attribute__((unused)))
{
    int r;

    /* Delete mailbox -
       Pretend we're an admin since we already deleted the parent */
    if (mboxlist_delayed_delete_isenabled()) {
        r = mboxlist_delayed_deletemailbox(mbentry->name, 1, /* admin */
                                           httpd_userid, httpd_authstate,
                                           NULL, 1 /* checkacl */,
                                           0 /* localonly */, 0 /* force */,
                                           0 /* keep_intermediaries */);

    }
    else {
        r = mboxlist_deletemailbox(mbentry->name, 1, /* admin */
                                   httpd_userid, httpd_authstate,
                                   NULL, 1 /* checkacl */,
                                   0 /* localonly */, 0 /* force */,
                                   0 /* keep_intermediaries */);
    }

    return r;
}

static int dav_move_collection(struct transaction_t *txn,
                               struct request_target_t *dest_tgt,
                               int overwrite)
{
    int r = 0, recursive = 1, rights;
    int omlen, nmlen;
    char *oldmailboxname = txn->req_tgt.mbentry->name;
    char *newmailboxname = dest_tgt->mbentry->name;
    struct mboxevent *mboxevent = NULL;

    /* Make sure we're moving within the same user */
    if (!mboxname_same_userid(newmailboxname, oldmailboxname)) {
        txn->error.desc = "Can only move within same user";
        return HTTP_FORBIDDEN;
    }

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

    if (txn->req_tgt.mbentry->server) {
        /* Remote source 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 source mailbox */

    /* If we're renaming something inside of something else,
       don't recursively rename */
    omlen = strlen(oldmailboxname);
    nmlen = strlen(newmailboxname);
    if (omlen < nmlen) {
        if (!strncmp(oldmailboxname, newmailboxname, omlen) &&
            newmailboxname[omlen] == '.') {
            recursive = 0;
        }
    } else {
        if (!strncmp(oldmailboxname, newmailboxname, nmlen) &&
            oldmailboxname[nmlen] == '.') {
            recursive = 0;
        }
    }

    struct mboxlock *namespacelock = mboxname_usernamespacelock(newmailboxname);

    r = mboxlist_createmailboxcheck(newmailboxname, 0, NULL, httpd_userisadmin,
                                    httpd_userid, httpd_authstate,
                                    NULL, NULL, 0 /* force */);

    if (r == IMAP_MAILBOX_EXISTS && overwrite) {
        /* Attempt to delete existing base mailbox */
        overwrite = -1;

        mboxevent = mboxevent_new(EVENT_MAILBOX_DELETE);

        if (mboxlist_delayed_delete_isenabled()) {
            r = mboxlist_delayed_deletemailbox(newmailboxname,
                                               httpd_userisadmin,
                                               httpd_userid, httpd_authstate,
                                               mboxevent, 1 /* checkacl */,
                                               0 /* localonly*/, 0 /* force */,
                                               0 /* keep_intermediaries */);

        }
        else {
            r = mboxlist_deletemailbox(newmailboxname,
                                       httpd_userisadmin,
                                       httpd_userid, httpd_authstate,
                                       mboxevent, 1 /* checkacl */,
                                       0 /* localonly*/, 0 /* force */,
                                       0 /* keep_intermediaries */);
        }

        if (!r) mboxevent_notify(&mboxevent);
        mboxevent_free(&mboxevent);

        /* Attempt to delete all existing submailboxes */
        if (!r && recursive) {
            char nmbn[MAX_MAILBOX_BUFFER];

            strcpy(nmbn, newmailboxname);
            strcat(nmbn, ".");

            r = mboxlist_allmbox(nmbn, remove_collection, NULL, 0);
        }
    }
    if (r) goto done;

    /* Attempt to rename the base mailbox */
    mboxevent = mboxevent_new(EVENT_MAILBOX_RENAME);

    r = mboxlist_renamemailbox(txn->req_tgt.mbentry, newmailboxname,
                               NULL /* partition */, 0 /* uidvalidity */,
                               httpd_userisadmin, httpd_userid, httpd_authstate,
                               mboxevent, 0, 0, 1 /* ignorequota */, 0, 0, 0);

    if (!r) mboxevent_notify(&mboxevent);
    mboxevent_free(&mboxevent);

    /* Attempt to rename all submailboxes */
    if (!r && recursive) {
        char ombn[MAX_MAILBOX_BUFFER];
        struct move_rock mrock = { ++omlen, ++nmlen, BUF_INITIALIZER,
                                   dest_tgt->namespace->prefix, NULL, {0} };

        strcpy(ombn, oldmailboxname);
        strcat(ombn, ".");

        /* Setup the rock */
        buf_setcstr(&mrock.newname, newmailboxname);
        buf_putc(&mrock.newname, '.');

        r = mboxlist_allmbox(ombn, move_collection, &mrock, 0);
        buf_free(&mrock.newname);

        if (mrock.root) {
            xml_response(HTTP_MULTI_STATUS, txn, mrock.root->doc);
            xmlFreeDoc(mrock.root->doc);
            mboxname_release(&namespacelock);
            return 0;
        }
    }

  done:
    mboxname_release(&namespacelock);
    switch (r) {
    case 0:
        return (overwrite < 0) ? HTTP_NO_CONTENT : HTTP_CREATED;

    case IMAP_MAILBOX_EXISTS:
        return HTTP_PRECOND_FAILED;

    case IMAP_PERMISSION_DENIED:
        /* DAV:need-privileges */
        txn->error.precond = DAV_NEED_PRIVS;
        txn->error.resource = dest_tgt->path;
        txn->error.rights = (overwrite < 0) ? DACL_UNBIND : DACL_BIND;
        return HTTP_NO_PRIVS;

    default:
        txn->error.desc = error_message(r);
        return HTTP_SERVER_ERROR;
    }
}



/* Perform a COPY/MOVE request
 *
 * preconditions:
 *   *DAV:need-privileges
 */
int meth_copy_move(struct transaction_t *txn, void *params)
{
    struct meth_params *cparams = (struct meth_params *) params;
    int ret = HTTP_CREATED, overwrite = 1, r, precond, rights;
    const char **hdr;
    xmlURIPtr dest_uri;
    static struct request_target_t dest_tgt;  /* Parsed destination URL -
                                                 static for Location resp hdr */
    struct backend *src_be = NULL, *dest_be = NULL;
    struct mailbox *src_mbox = NULL, *dest_mbox = NULL;
    struct dav_data *ddata;
    struct index_record src_rec;
    const char *etag = NULL;
    time_t lastmod = 0;
    unsigned meth_move = (txn->meth == METH_MOVE);
    void *src_davdb = NULL, *dest_davdb = NULL, *obj = NULL;
    struct buf msg_buf = BUF_INITIALIZER;
    struct buf body_buf = BUF_INITIALIZER;

    memset(&dest_tgt, 0, sizeof(struct request_target_t));

    /* Response should not be cached */
    txn->flags.cc |= CC_NOCACHE;

    /* Parse the source path */
    r = dav_parse_req_target(txn, cparams);
    if (r) return r;

    /* Make sure method is allowed (not allowed on collections yet) */
    if (!(txn->req_tgt.allow & ALLOW_WRITE)) return HTTP_NOT_ALLOWED;

    /* Check for mandatory Destination header */
    if (!(hdr = spool_getheader(txn->req_hdrs, "Destination"))) {
        txn->error.desc = "Missing Destination header";
        return HTTP_BAD_REQUEST;
    }

    /* Parse destination URI */
    if (!(dest_uri = parse_uri(METH_UNKNOWN, hdr[0], 1, &txn->error.desc))) {
        txn->error.desc = "Illegal Destination target URI";
        return HTTP_BAD_REQUEST;
    }

    /* Make sure source and dest resources are NOT the same */
    if (!strcmp(txn->req_uri->path, dest_uri->path)) {
        txn->error.desc = "Source and destination resources are the same";
        ret = HTTP_FORBIDDEN;
        goto done;
    }

    /* Check for COPY/MOVE on collection */
    if (!txn->req_tgt.resource) {
        if (meth_move) {
            /* Use our own entry for dest to suppress lookup in parse_path() */
            dest_tgt.mbentry = mboxlist_entry_create();
        }
        else {
            /* We don't yet handle COPY on collections */
            ret = HTTP_NOT_ALLOWED;
            goto done;
        }
    }

    /* Parse the destination path */
    dest_tgt.namespace = txn->req_tgt.namespace;
    r = cparams->parse_path(dest_uri->path, &dest_tgt, &txn->error.desc);
    if (r) {
        ret = (r == HTTP_MOVED) ? HTTP_FORBIDDEN : r;
        goto done;
    }

    /* Replace cached Destination header with just the absolute path */
    spool_replace_header(xstrdup("Destination"),
                         xstrdup(dest_tgt.path), txn->req_hdrs);

    /* Check for optional Overwrite header */
    if ((hdr = spool_getheader(txn->req_hdrs, "Overwrite")) &&
        !strcmp(hdr[0], "F")) {
        overwrite = 0;
    }

    /* Handle MOVE on collection */
    if (!txn->req_tgt.resource) {
        ret = dav_move_collection(txn, &dest_tgt, overwrite);
        goto done;
    }

    /* Make sure we have a dest resource */
    if (!dest_tgt.resource) {
        txn->error.desc = "No destination resource specified";
        ret = HTTP_BAD_REQUEST;
        goto done;
    }

    /* Check ACL for current user on source mailbox */
    rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry);
    if (((rights & DACL_READ) != DACL_READ) ||
        (meth_move && !(rights & DACL_RMRSRC))) {
        /* DAV:need-privileges */
        txn->error.precond = DAV_NEED_PRIVS;
        txn->error.resource = txn->req_tgt.path;
        txn->error.rights =
            (rights & DACL_READ) != DACL_READ ? DACL_READ : DACL_RMRSRC;
        ret = HTTP_NO_PRIVS;
        goto done;
    }

    /* Check ACL for current user on destination */
    rights = httpd_myrights(httpd_authstate, dest_tgt.mbentry);
    if (!(rights & DACL_ADDRSRC) || !(rights & DACL_WRITECONT)) {
        /* DAV:need-privileges */
        txn->error.precond = DAV_NEED_PRIVS;
        txn->error.resource = dest_tgt.path;
        txn->error.rights =
            !(rights & DACL_ADDRSRC) ? DACL_ADDRSRC : DACL_WRITECONT;
        ret = HTTP_NO_PRIVS;
        goto done;
    }

    /* Check we're not copying within the same user */
    if (!meth_move && cparams->copy.uid_conf_precond &&
        mboxname_same_userid(dest_tgt.mbentry->name,
                             txn->req_tgt.mbentry->name)) {
        txn->error.precond = cparams->copy.uid_conf_precond;
        txn->error.desc = "Can not copy resources within same user";
        ret = HTTP_NOT_ALLOWED;
        goto done;
    }

    if (txn->req_tgt.mbentry->server) {
        /* Remote source mailbox */

        if (!dest_tgt.mbentry->server) {
            /* Local destination mailbox */

            /* XXX  Currently only supports standard Murder */
            txn->error.desc = "COPY/MOVE only supported in a standard Murder";
            ret = HTTP_NOT_ALLOWED;
        }
        else if (!(src_be = proxy_findserver(txn->req_tgt.mbentry->server,
                                             &http_protocol, httpd_userid,
                                             &backend_cached, NULL, NULL,
                                             httpd_in))) {
            txn->error.desc = "Unable to connect to source backend";
            ret = HTTP_UNAVAILABLE;
        }
        else if (!(dest_be = proxy_findserver(dest_tgt.mbentry->server,
                                              &http_protocol, httpd_userid,
                                              &backend_cached, NULL, NULL,
                                              httpd_in))) {
            txn->error.desc = "Unable to connect to destination backend";
            ret = HTTP_UNAVAILABLE;
        }
        else if (src_be == dest_be) {
            /* Simply send the COPY to the backend */
            ret = http_pipe_req_resp(src_be, txn);
        }
        else {
            /* This is the harder case: GET from source and PUT on dest */
            ret = http_proxy_copy(src_be, dest_be, txn);
        }

        goto done;
    }
    else if (dest_tgt.mbentry->server) {
        /* Local source and remote destination mailbox */

        /* XXX  Currently only supports standard Murder */
        txn->error.desc = "COPY/MOVE only supported in a standard Murder";
        ret = HTTP_NOT_ALLOWED;
        goto done;
    }

    /* Local source and destination mailboxes */

    if (!strcmp(txn->req_tgt.mbentry->name, dest_tgt.mbentry->name)) {
        /* Same source and destination - Open source mailbox for writing */
        r = mailbox_open_iwl(txn->req_tgt.mbentry->name, &src_mbox);
        dest_mbox = src_mbox;
    }
    else if (meth_move) {
        /* Open source mailbox for writing */
        r = mailbox_open_iwl(txn->req_tgt.mbentry->name, &src_mbox);
    }
    else {
        /* Open source mailbox for reading */
        r = mailbox_open_irl(txn->req_tgt.mbentry->name, &src_mbox);
    }
    if (r) {
        syslog(LOG_ERR, "mailbox_open_i%cl(%s) failed: %s",
               (meth_move ||
                !strcmp(txn->req_tgt.mbentry->name, dest_tgt.mbentry->name)) ?
               'w' : 'r',
               txn->req_tgt.mbentry->name, error_message(r));
        txn->error.desc = error_message(r);
        ret = HTTP_SERVER_ERROR;
        goto done;
    }

    /* Open the DAV DB corresponding to the src mailbox */
    src_davdb = cparams->davdb.open_db(src_mbox);

    /* Find message UID for the source resource */
    cparams->davdb.lookup_resource(src_davdb, txn->req_tgt.mbentry->name,
                                   txn->req_tgt.resource,
                                   (void **) &ddata, 0);
    if (!ddata->rowid) {
        ret = HTTP_NOT_FOUND;
        goto done;
    }

    /* Fetch resource validators */
    r = cparams->get_validators(src_mbox, (void *) ddata, httpd_userid,
                                &src_rec, &etag, &lastmod);
    if (r) {
        txn->error.desc = error_message(r);
        ret = HTTP_SERVER_ERROR;
        goto done;
    }

    /* Check any preconditions on source */
    precond = cparams->check_precond(txn, params, src_mbox,
                                     (void *) ddata, etag, lastmod);

    switch (precond) {
    case HTTP_OK:
        break;

    case HTTP_LOCKED:
        txn->error.precond = DAV_NEED_LOCK_TOKEN;
        txn->error.resource = txn->req_tgt.path;

        GCC_FALLTHROUGH

    default:
        /* We failed a precondition - don't perform the request */
        ret = precond;
        goto done;
    }

    if (src_rec.uid) {
        /* Mapped URL - Load message containing the resource and parse data */
        mailbox_map_record(src_mbox, &src_rec, &msg_buf);
        buf_init_ro(&body_buf, buf_base(&msg_buf) + src_rec.header_size,
                    buf_len(&msg_buf) - src_rec.header_size);
        obj = cparams->mime_types[0].to_object(&body_buf);
    }
    else {
        /* Unmapped URL (empty resource) */
        buf_init_ro_cstr(&body_buf, "");
        obj = &body_buf;
        src_rec.recno = ddata->rowid; /* For deleting DAV record */
    }

    if (dest_mbox != src_mbox) {
        if (!meth_move) {
            /* Done with source mailbox */
            mailbox_unlock_index(src_mbox, NULL);
        }

        /* Open dest mailbox for writing */
        r = mailbox_open_iwl(dest_tgt.mbentry->name, &dest_mbox);
        if (r) {
            syslog(LOG_ERR, "mailbox_open_iwl(%s) failed: %s",
                   dest_tgt.mbentry->name, error_message(r));
            txn->error.desc = error_message(r);
            ret = HTTP_SERVER_ERROR;
            goto done;
        }

        /* Open the DAV DB corresponding to the dest mailbox */
        dest_davdb = cparams->davdb.open_db(dest_mbox);
    }
    else {
        dest_davdb = src_davdb;
    }

    /* Find message UID for the dest resource, if exists */
    cparams->davdb.lookup_resource(dest_davdb, dest_tgt.mbentry->name,
                                   dest_tgt.resource, (void **) &ddata, 0);
    /* XXX  Check errors */

    /* Check any preconditions on destination */
    if (ddata->rowid && !overwrite) {
        /* Don't overwrite the destination resource */
        ret = HTTP_PRECOND_FAILED;
        goto done;
    }

    /* Store the resource at destination */
    ret = cparams->copy.proc(txn, obj,
                             dest_mbox, dest_tgt.resource, dest_davdb, 0);

    if (dest_mbox != src_mbox) {
        /* Done with destination mailbox */
        mailbox_unlock_index(dest_mbox, NULL);
    }

    switch (ret) {
    case HTTP_CREATED:
    case HTTP_NO_CONTENT:
        if (meth_move) {
            /* Expunge the source message */
            if (src_rec.uid) {
                /* Mapped URL */
                src_rec.internal_flags |= FLAG_INTERNAL_EXPUNGED;
                if ((r = mailbox_rewrite_index_record(src_mbox, &src_rec))) {
                    syslog(LOG_ERR, "expunging src record (%s) failed: %s",
                           txn->req_tgt.mbentry->name, error_message(r));
                    txn->error.desc = error_message(r);
                    ret = HTTP_SERVER_ERROR;
                    goto done;
                }
            }
            else {
                /* Unmapped URL (empty resource) */
                cparams->davdb.delete_resourceLOCKONLY(src_davdb, src_rec.recno);
            }
        }
    }

  done:
    if (ret == HTTP_CREATED) {
        /* Tell client where to find the new resource */
        txn->location = dest_tgt.path;
    }
    else {
        /* Don't confuse client by providing ETag of Destination resource */
        txn->resp_body.etag = NULL;
    }

    if (obj && cparams->mime_types[0].free) cparams->mime_types[0].free(obj);
    if (dest_mbox != src_mbox) {
        if (dest_davdb) cparams->davdb.close_db(dest_davdb);
        if (dest_mbox) mailbox_close(&dest_mbox);
    }
    if (src_davdb) cparams->davdb.close_db(src_davdb);
    if (src_mbox) mailbox_close(&src_mbox);

    buf_free(&msg_buf);
    buf_free(&body_buf);
    xmlFreeURI(dest_uri);
    free(dest_tgt.userid);
    mboxlist_entry_free(&dest_tgt.mbentry);

    return ret;
}


struct delete_rock {
    struct transaction_t *txn;
    struct mailbox *mailbox;
    delete_proc_t deletep;
};

static int delete_cb(void *rock, void *data)
{
    struct delete_rock *drock = (struct delete_rock *) rock;
    struct dav_data *ddata = (struct dav_data *) data;
    struct index_record record;
    int r;

    if (!ddata->imap_uid) {
        /* Unmapped URL (empty resource) */
        return 0;
    }

    /* Fetch index record for the resource */
    r = mailbox_find_index_record(drock->mailbox, ddata->imap_uid, &record);
    if (r) {
        drock->txn->error.desc = error_message(r);
        return HTTP_SERVER_ERROR;
    }

    r = drock->deletep(drock->txn, drock->mailbox, &record, data);

    return r;
}

/* Perform a DELETE request */
int meth_delete(struct transaction_t *txn, void *params)
{
    struct meth_params *dparams = (struct meth_params *) params;
    int ret = HTTP_NO_CONTENT, r = 0, precond, rights, needrights;
    struct mboxevent *mboxevent = NULL;
    struct mailbox *mailbox = NULL;
    struct dav_data *ddata;
    struct index_record record;
    const char *etag = NULL;
    time_t lastmod = 0;
    void *davdb = NULL;

    /* Response should not be cached */
    txn->flags.cc |= CC_NOCACHE;

    /* Parse the path */
    r = dav_parse_req_target(txn, dparams);
    if (r) return r;

    /* Make sure method is allowed */
    if (!(txn->req_tgt.allow & ALLOW_DELETE)) return HTTP_NOT_ALLOWED;

    /* if FastMail sharing, we need to remove ACLs */
    if (config_getswitch(IMAPOPT_FASTMAILSHARING) &&!txn->req_tgt.resource &&
        !mboxname_userownsmailbox(httpd_userid, txn->req_tgt.mbentry->name)) {
        r = mboxlist_setacl(&httpd_namespace, txn->req_tgt.mbentry->name,
                            httpd_userid, /*rights*/NULL, /*isadmin*/1,
                            httpd_userid, httpd_authstate);
        if (r) {
            syslog(LOG_ERR, "meth_delete(%s) failed to remove acl: %s",
                   txn->req_tgt.mbentry->name, error_message(r));
            txn->error.desc = error_message(r);
            return HTTP_SERVER_ERROR;
        }
        return HTTP_OK;
    }

    /* Special case of deleting a shared collection */
    if (!txn->req_tgt.resource && (txn->req_tgt.flags == TGT_DAV_SHARED)) {
        char *inboxname = mboxname_user_mbox(txn->req_tgt.userid, NULL);
        mbentry_t *mbentry = NULL;

        r = http_mlookup(inboxname, &mbentry, NULL);
        if (r) {
            syslog(LOG_ERR, "mlookup(%s) failed: %s",
                   inboxname, error_message(r));
            ret = HTTP_NOT_FOUND;
        }
        else if (mbentry->server) {
            /* Remote mailbox */
            struct backend *be;

            be = proxy_findserver(mbentry->server, &http_protocol, httpd_userid,
                                  &backend_cached, NULL, NULL, httpd_in);
            if (!be) ret = HTTP_UNAVAILABLE;
            else ret = http_pipe_req_resp(be, txn);
        }
        else {
            /* Local Mailbox */
            struct mailbox *mailbox = NULL;

            /* Unsubscribe */
            r = mboxlist_changesub(txn->req_tgt.mbentry->name,
                                   txn->req_tgt.userid,
                                   httpd_authstate, 0 /* remove */, 0, 0);
            if (r) {
                syslog(LOG_ERR, "mboxlist_changesub(%s, %s) failed: %s",
                       txn->req_tgt.mbentry->name, txn->req_tgt.userid,
                       error_message(r));
                txn->error.desc = error_message(r);
                ret = HTTP_SERVER_ERROR;
            }
            else ret = HTTP_NO_CONTENT;

            /* Set invite status to declined */
            r = mailbox_open_iwl(txn->req_tgt.mbentry->name, &mailbox);
            if (r) {
                syslog(LOG_ERR,
                       "IOERROR: failed to open mailbox %s for DELETE share",
                       txn->req_tgt.mbentry->name);
            }
            else {
                annotate_state_t *astate = NULL;

                r = mailbox_get_annotate_state(mailbox, 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, "invite-declined");
                    r = annotate_state_writemask(astate, annot,
                                                 txn->req_tgt.userid, &value);
                }

                mailbox_close(&mailbox);
            }
        }

        mboxlist_entry_free(&mbentry);
        free(inboxname);

        return ret;
    }

    /* Check ACL for current user */
    rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry);
    needrights = txn->req_tgt.resource ? DACL_RMRSRC : DACL_RMCOL;
    if (!(rights & needrights)) {
        /* DAV:need-privileges */
        txn->error.precond = DAV_NEED_PRIVS;
        txn->error.resource = txn->req_tgt.path;
        txn->error.rights = needrights;
        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 */

    if (!txn->req_tgt.resource) {
        /* DELETE collection */

        if (dparams->delete) {
            /* Do special processing on all resources */
            struct delete_rock drock = { txn, NULL, dparams->delete };

            /* Open mailbox for reading */
            r = mailbox_open_irl(txn->req_tgt.mbentry->name, &mailbox);
            if (r) {
                syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s",
                       txn->req_tgt.mbentry->name, error_message(r));
                txn->error.desc = error_message(r);
                return HTTP_SERVER_ERROR;
            }

            /* Open the DAV DB corresponding to the mailbox */
            davdb = dparams->davdb.open_db(mailbox);

            drock.mailbox = mailbox;
            r = dparams->davdb.foreach_resource(davdb, mailbox->name,
                                                  &delete_cb, &drock);
            /* we need the mailbox closed before we delete it */
            mailbox_close(&mailbox);
            if (r) {
                txn->error.desc = error_message(r);
                return HTTP_SERVER_ERROR;
            }
        }

        mbname_t *mbname = mbname_from_intname(txn->req_tgt.mbentry->name);
        struct mboxlock *namespacelock = user_namespacelock(mbname_userid(mbname));

        mboxevent = mboxevent_new(EVENT_MAILBOX_DELETE);

        if (mboxlist_delayed_delete_isenabled()) {
            r = mboxlist_delayed_deletemailbox(txn->req_tgt.mbentry->name,
                                       httpd_userisadmin || httpd_userisproxyadmin,
                                       httpd_userid, httpd_authstate, mboxevent,
                                       /*checkack*/1, /*localonly*/0, /*force*/0,
                                       /* keep_intermediaries */0);
        }
        else {
            r = mboxlist_deletemailbox(txn->req_tgt.mbentry->name,
                                       httpd_userisadmin || httpd_userisproxyadmin,
                                       httpd_userid, httpd_authstate, mboxevent,
                                       /*checkack*/1, /*localonly*/0, /*force*/0,
                                       /* keep_intermediaries */0);
        }
        if (!r) {
            r = caldav_update_shareacls(mbname_userid(mbname));
        }
        if (r == IMAP_PERMISSION_DENIED) ret = HTTP_FORBIDDEN;
        else if (r == IMAP_MAILBOX_NONEXISTENT) ret = HTTP_NOT_FOUND;
        else if (r) ret = HTTP_SERVER_ERROR;

        mboxname_release(&namespacelock);
        mbname_free(&mbname);

        goto done;
    }

    /* DELETE resource */

    /* Open mailbox for writing */
    r = mailbox_open_iwl(txn->req_tgt.mbentry->name, &mailbox);
    if (r) {
        syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s",
               txn->req_tgt.mbentry->name, error_message(r));
        txn->error.desc = error_message(r);
        ret = HTTP_SERVER_ERROR;
        goto done;
    }

    /* Open the DAV DB corresponding to the mailbox */
    davdb = dparams->davdb.open_db(mailbox);

    /* Find message UID for the resource, if exists */
    dparams->davdb.lookup_resource(davdb, txn->req_tgt.mbentry->name,
                                   txn->req_tgt.resource, (void **) &ddata, 0);
    if (!ddata->rowid) {
        ret = HTTP_NOT_FOUND;
        goto done;
    }

    /* Fetch resource validators */
    r = dparams->get_validators(mailbox, (void *) ddata, httpd_userid,
                                &record, &etag, &lastmod);
    if (r) {
        txn->error.desc = error_message(r);
        ret = HTTP_SERVER_ERROR;
        goto done;
    }

    /* Check any preconditions */
    precond = dparams->check_precond(txn, params, mailbox,
                                     (void *) ddata, etag, lastmod);

    switch (precond) {
    case HTTP_OK:
        break;

    case HTTP_LOCKED:
        txn->error.precond = DAV_NEED_LOCK_TOKEN;
        txn->error.resource = txn->req_tgt.path;

        GCC_FALLTHROUGH

    default:
        /* We failed a precondition - don't perform the request */
        ret = precond;
        goto done;
    }

    if (record.uid) {
        /* Do any special processing */
        if (dparams->delete) dparams->delete(txn, mailbox, &record, ddata);

        /* Expunge the resource */
        record.internal_flags |= FLAG_INTERNAL_EXPUNGED;

        mboxevent = mboxevent_new(EVENT_MESSAGE_EXPUNGE);

        r = mailbox_rewrite_index_record(mailbox, &record);

        if (r) {
            syslog(LOG_ERR, "expunging record (%s) failed: %s",
                   txn->req_tgt.mbentry->name, error_message(r));
            txn->error.desc = error_message(r);
            ret = HTTP_SERVER_ERROR;
            goto done;
        }

        mboxevent_extract_record(mboxevent, mailbox, &record);
        mboxevent_extract_mailbox(mboxevent, mailbox);
        mboxevent_set_numunseen(mboxevent, mailbox, -1);
        mboxevent_set_access(mboxevent, NULL, NULL, httpd_userid,
                             txn->req_tgt.mbentry->name, 0);
    }

  done:
    if (davdb) dparams->davdb.close_db(davdb);
    mailbox_close(&mailbox);

    if (!r)
        mboxevent_notify(&mboxevent);
    mboxevent_free(&mboxevent);

    return ret;
}


/* Perform a GET/HEAD request on a DAV resource */
int meth_get_head(struct transaction_t *txn, void *params)
{
    struct meth_params *gparams = (struct meth_params *) params;
    const char **hdr;
    struct mime_type_t *mime = NULL;
    int ret = 0, r = 0, precond, rights;
    const char *data = NULL;
    unsigned long datalen = 0, offset = 0;
    struct buf msg_buf = BUF_INITIALIZER;
    struct resp_body_t *resp_body = &txn->resp_body;
    struct mailbox *mailbox = NULL;
    struct dav_data *ddata;
    struct index_record record;
    const char *etag = NULL;
    time_t lastmod = 0;
    void *davdb = NULL, *obj = NULL;
    char *freeme = NULL;

    /* Parse the path */
    r = dav_parse_req_target(txn, gparams);
    if (r) return r;

    if (txn->req_tgt.namespace->id == URL_NS_PRINCIPAL) {
        /* Special "principal" */
        if (txn->req_tgt.flags == TGT_SERVER_INFO) return get_server_info(txn);

        /* No content for principals (yet) */
        return HTTP_NO_CONTENT;
    }

    if (!txn->req_tgt.resource) {
        /* Do any collection processing */
        if (gparams->get) return gparams->get(txn, NULL, NULL, NULL, NULL);

        /* We don't handle GET on a collection */
        return HTTP_NO_CONTENT;
    }

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

    if (gparams->mime_types) {
        /* Check requested MIME type:
           1st entry in gparams->mime_types array MUST be default MIME type */
        if ((hdr = spool_getheader(txn->req_hdrs, "Accept")))
            mime = get_accept_type(hdr, gparams->mime_types);
        else mime = gparams->mime_types;
        if (!mime) return HTTP_NOT_ACCEPTABLE;
    }

    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 */

    /* Open mailbox for reading */
    r = mailbox_open_irl(txn->req_tgt.mbentry->name, &mailbox);
    if (r) {
        syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s",
               txn->req_tgt.mbentry->name, error_message(r));
        goto done;
    }

    /* Open the DAV DB corresponding to the mailbox */
    davdb = gparams->davdb.open_db(mailbox);

    /* Find message UID for the resource */
    gparams->davdb.lookup_resource(davdb, txn->req_tgt.mbentry->name,
                                   txn->req_tgt.resource, (void **) &ddata, 0);
    if (!ddata->rowid) {
        ret = HTTP_NOT_FOUND;
        goto done;
    }

    /* Fetch resource validators */
    r = gparams->get_validators(mailbox, (void *) ddata, httpd_userid,
                                &record, &etag, &lastmod);
    if (r) {
        txn->error.desc = error_message(r);
        ret = HTTP_SERVER_ERROR;
        goto done;
    }

    txn->flags.ranges = (ddata->imap_uid != 0);

    /* Check any preconditions, including range request */
    precond = gparams->check_precond(txn, params, mailbox,
                                     (void *) ddata, etag, lastmod);

    switch (precond) {
    case HTTP_OK:
    case HTTP_PARTIAL:
    case HTTP_NOT_MODIFIED:
        /* Fill in ETag, Last-Modified, Expires, and Cache-Control */
        resp_body->etag = etag;
        resp_body->lastmod = lastmod;
        resp_body->maxage = 3600;       /* 1 hr */
        txn->flags.cc |= CC_MAXAGE | CC_REVALIDATE;  /* don't use stale data */
        if (httpd_userid) txn->flags.cc |= CC_PRIVATE;

        if (precond != HTTP_NOT_MODIFIED && record.uid) break;

        GCC_FALLTHROUGH

    default:
        /* We failed a precondition - don't perform the request */
        ret = precond;
        goto done;
    }

    /* Do any special processing */
    if (gparams->get) {
        ret = gparams->get(txn, mailbox, &record, ddata, &obj);
        if (ret != HTTP_CONTINUE) goto done;

        ret = 0;
    }

    if (mime && !resp_body->type) {
        txn->flags.vary |= VARY_ACCEPT;
        resp_body->type = mime->content_type;
    }

    if (!obj) {
        /* Raw resource - length doesn't include RFC 5322 header */
        offset = record.header_size;
        datalen = record.size - offset;

        if (txn->meth == METH_GET) {
            /* Load message containing the resource */
            r = mailbox_map_record(mailbox, &record, &msg_buf);
            if (r) goto done;

            data = buf_base(&msg_buf) + offset;

            if (mime != gparams->mime_types) {
                /* Not the storage format - create resource object */
                struct buf inbuf = BUF_INITIALIZER;
                buf_init_ro(&inbuf, data, datalen);
                obj = gparams->mime_types[0].to_object(&inbuf);
                buf_free(&inbuf);
            }
        }
    }

    if (obj) {
        /* Convert object into requested MIME type */
        struct buf *outbuf = mime->from_object(obj);

        datalen = buf_len(outbuf);
        if (txn->meth == METH_GET) data = freeme = buf_release(outbuf);
        buf_destroy(outbuf);

        if (gparams->mime_types[0].free) gparams->mime_types[0].free(obj);
    }

    write_body(precond, txn, data, datalen);

    buf_free(&msg_buf);
    free(freeme);

  done:
    if (davdb) gparams->davdb.close_db(davdb);
    if (r) {
        txn->error.desc = error_message(r);
        ret = HTTP_SERVER_ERROR;
    }
    mailbox_close(&mailbox);

    return ret;
}


/* Perform a LOCK request
 *
 * preconditions:
 *   DAV:need-privileges
 *   DAV:no-conflicting-lock
 *   DAV:lock-token-submitted
 */
int meth_lock(struct transaction_t *txn, void *params)
{
    struct meth_params *lparams = (struct meth_params *) params;
    int ret = HTTP_OK, r, precond, rights;
    struct mailbox *mailbox = NULL;
    struct dav_data *ddata;
    struct index_record oldrecord;
    const char *etag;
    time_t lastmod;
    xmlDocPtr indoc = NULL, outdoc = NULL;
    xmlNodePtr root = NULL;
    xmlNsPtr ns[NUM_NAMESPACE];
    xmlBufferPtr owner = NULL;
    time_t now = time(NULL);
    void *davdb = NULL;

    /* XXX  We ignore Depth and Timeout header fields */

    /* Response should not be cached */
    txn->flags.cc |= CC_NOCACHE;

    /* Parse the path */
    r = dav_parse_req_target(txn, lparams);
    if (r) return r;

    /* Make sure method is allowed (only allowed on resources) */
    if (!(txn->req_tgt.allow & ALLOW_WRITE)) return HTTP_NOT_ALLOWED;

    /* Check ACL for current user */
    rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry);
    if (!(rights & DACL_WRITECONT) || !(rights & DACL_ADDRSRC)) {
        /* DAV:need-privileges */
        txn->error.precond = DAV_NEED_PRIVS;
        txn->error.resource = txn->req_tgt.path;
        txn->error.rights =
            !(rights & DACL_WRITECONT) ? DACL_WRITECONT : DACL_ADDRSRC;
        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 */

    /* Open mailbox for reading */
    r = mailbox_open_irl(txn->req_tgt.mbentry->name, &mailbox);
    if (r) {
        syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s",
               txn->req_tgt.mbentry->name, error_message(r));
        txn->error.desc = error_message(r);
        ret = HTTP_SERVER_ERROR;
        goto done;
    }

    /* Open the DAV DB corresponding to the mailbox */
    davdb = lparams->davdb.open_db(mailbox);
    lparams->davdb.begin_transaction(davdb);

    /* Find message UID for the resource, if exists */
    lparams->davdb.lookup_resource(davdb, txn->req_tgt.mbentry->name,
                                   txn->req_tgt.resource, (void *) &ddata, 1);

    /* Fetch resource validators */
    r = lparams->get_validators(mailbox, (void *) ddata, httpd_userid,
                                &oldrecord, &etag, &lastmod);
    if (r) {
        txn->error.desc = error_message(r);
        ret = HTTP_SERVER_ERROR;
        goto done;
    }

    if (!ddata->alive) {
        /* New resource */
        ddata->creationdate = now;
        ddata->mailbox = mailbox->name;
        ddata->resource = txn->req_tgt.resource;
        ddata->imap_uid = 0;
        ddata->lock_expire = 0;
        ddata->alive = 1;
    }

    /* Check any preconditions */
    precond = lparams->check_precond(txn, params, mailbox,
                                     (void *) ddata, etag, lastmod);

    switch (precond) {
    case HTTP_OK:
        break;

    case HTTP_LOCKED:
        if (strcmp(ddata->lock_ownerid, httpd_userid))
            txn->error.precond = DAV_LOCKED;
        else
            txn->error.precond = DAV_NEED_LOCK_TOKEN;
        txn->error.resource = txn->req_tgt.path;

        GCC_FALLTHROUGH

    default:
        /* We failed a precondition - don't perform the request */
        ret = precond;
        goto done;
    }

    if (ddata->lock_expire <= now) {
        /* Create new lock */
        xmlNodePtr node, sub;

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

        /* Make sure its a DAV:lockinfo element */
        indoc = root->doc;
        if (!root->ns || xmlStrcmp(root->ns->href, BAD_CAST XML_NS_DAV) ||
            xmlStrcmp(root->name, BAD_CAST "lockinfo")) {
            txn->error.desc = "Missing DAV:lockinfo element in LOCK request";
            ret = HTTP_BAD_MEDIATYPE;
            goto done;
        }

        /* Parse elements of lockinfo */
        for (node = root->children; node; node = node->next) {
            if (node->type != XML_ELEMENT_NODE) continue;

            if (!xmlStrcmp(node->name, BAD_CAST "lockscope")) {
                /* Find child element of lockscope */
                for (sub = node->children;
                     sub && sub->type != XML_ELEMENT_NODE; sub = sub->next);
                /* Make sure its an exclusive element */
                if (!sub || xmlStrcmp(sub->name, BAD_CAST "exclusive")) {
                    txn->error.desc = "Only exclusive locks are supported";
                    ret = HTTP_BAD_REQUEST;
                    goto done;
                }
            }
            else if (!xmlStrcmp(node->name, BAD_CAST "locktype")) {
                /* Find child element of locktype */
                for (sub = node->children;
                     sub && sub->type != XML_ELEMENT_NODE; sub = sub->next);
                /* Make sure its a write element */
                if (!sub || xmlStrcmp(sub->name, BAD_CAST "write")) {
                    txn->error.desc = "Only write locks are supported";
                    ret = HTTP_BAD_REQUEST;
                    goto done;
                }
            }
            else if (!xmlStrcmp(node->name, BAD_CAST "owner")) {
                /* Find child element of owner */
                owner = xmlBufferCreate();
                for (sub = node->children;
                     sub && sub->type != XML_ELEMENT_NODE; sub = sub->next);
                if (!sub) {
                    xmlNodeBufGetContent(owner, node);
                }
                /* Make sure its a href element */
                else if (xmlStrcmp(sub->name, BAD_CAST "href")) {
                    ret = HTTP_BAD_REQUEST;
                    goto done;
                }
                else {
                    xmlNodeBufGetContent(owner, sub);
                    xmlBufferAddHead(owner, BAD_CAST "<DAV:href>", 10);
                }
            }
        }

        ddata->lock_ownerid = httpd_userid;
        if (owner) ddata->lock_owner = (const char *) xmlBufferContent(owner);

        /* Construct lock-token */
        assert(!buf_len(&txn->buf));
        buf_printf(&txn->buf, LOCK_TOKEN_URL_SCHEME "%s", makeuuid());

        ddata->lock_token = buf_cstring(&txn->buf);
    }

    /* Update lock expiration */
    ddata->lock_expire = now + 300;  /* 5 min */

    /* Start construction of our prop response */
    if (!(root = init_xml_response("prop", NS_DAV, root, ns))) {
        ret = HTTP_SERVER_ERROR;
        txn->error.desc = "Unable to create XML response\r\n";
        goto done;
    }

    outdoc = root->doc;
    root = xmlNewChild(root, NULL, BAD_CAST "lockdiscovery", NULL);
    xml_add_lockdisc(root, txn->req_tgt.path, (struct dav_data *) ddata);

    r = lparams->davdb.write_resourceLOCKONLY(davdb, ddata);
    if (r) {
        syslog(LOG_ERR, "Unable to write lock record to DAV DB: %s",
               error_message(r));
        ret = HTTP_SERVER_ERROR;
        txn->error.desc = "Unable to create locked resource";
        goto done;
    }

    txn->resp_body.lock = ddata->lock_token;

    if (!ddata->rowid) {
        ret = HTTP_CREATED;

        /* Tell client where to find the new resource */
        txn->location = txn->req_tgt.path;
    }
    else ret = HTTP_OK;

    xml_response(ret, txn, outdoc);
    ret = 0;

  done:
    if (davdb) {
        /* XXX - error handling/abort */
        lparams->davdb.commit_transaction(davdb);
        lparams->davdb.close_db(davdb);
    }
    mailbox_close(&mailbox);
    if (outdoc) xmlFreeDoc(outdoc);
    if (indoc) xmlFreeDoc(indoc);
    if (owner) xmlBufferFree(owner);

    return ret;
}


/* Perform a MKCOL/MKCALENDAR request */
/*
 * preconditions:
 *   DAV:resource-must-be-null
 *   DAV:need-privileges
 *   DAV:valid-resourcetype
 *   CALDAV:calendar-collection-location-ok
 *   CALDAV:valid-calendar-data (CALDAV:calendar-timezone)
 */
int meth_mkcol(struct transaction_t *txn, void *params)
{
    struct meth_params *mparams = (struct meth_params *) params;
    int ret = 0, r = 0;
    xmlDocPtr indoc = NULL, outdoc = NULL;
    xmlNodePtr root = NULL, instr = NULL;
    xmlNsPtr ns[NUM_NAMESPACE];
    char *partition = NULL;
    struct proppatch_ctx pctx;
    struct mailbox *mailbox = NULL;

    memset(&pctx, 0, sizeof(struct proppatch_ctx));

    /* Response should not be cached */
    txn->flags.cc |= CC_NOCACHE;

    /* Parse the path (use our own entry to suppress lookup) */
    txn->req_tgt.mbentry = mboxlist_entry_create();
    r = mparams->parse_path(txn->req_uri->path, &txn->req_tgt, &txn->error.desc);

    /* Make sure method is allowed (only allowed on child of home-set) */
    if (!txn->req_tgt.collection || txn->req_tgt.resource) {
        txn->error.precond = mparams->mkcol.location_precond;
        return HTTP_FORBIDDEN;
    }
    else if (r) {
        switch (r) {
        case IMAP_MAILBOX_EXISTS:
            txn->error.precond = DAV_RES_EXISTS;
            break;

        case IMAP_PERMISSION_DENIED:
            txn->error.precond = DAV_NEED_PRIVS;
            txn->error.rights = DACL_BIND;
            buf_reset(&txn->buf);
            buf_printf(&txn->buf, "%s/%s/%s",
                       txn->req_tgt.namespace->prefix, USER_COLLECTION_PREFIX,
                       txn->req_tgt.userid);
            txn->error.resource = buf_cstring(&txn->buf);
            break;

        default:
            txn->error.precond = mparams->mkcol.location_precond;
            break;
        }

        return HTTP_FORBIDDEN;
    }

    if (config_mupdate_server && !config_getstring(IMAPOPT_PROXYSERVERS)) {
        /* Remote mailbox - find the parent */
        mbentry_t *parent = NULL;
        struct backend *be;

        r = mboxlist_findparent(txn->req_tgt.mbentry->name, &parent);
        if (r) {
            txn->error.precond = mparams->mkcol.location_precond;
            ret = HTTP_FORBIDDEN;
            goto done;
	}

        be = proxy_findserver(parent->server, &http_protocol, httpd_userid,
                              &backend_cached, NULL, NULL, httpd_in);

        if (!be) ret = HTTP_UNAVAILABLE;
        else ret = http_pipe_req_resp(be, txn);

        goto done;
    }

    /* Local Mailbox */

    /* Parse the MKCOL/MKCALENDAR body, if exists */
    ret = parse_xml_body(txn, &root, NULL);
    if (ret) goto done;

    if (root) {
        /* Check for correct root element (lowercase method name ) */
        const char *ns_href;

        indoc = root->doc;

        buf_setcstr(&txn->buf, http_methods[txn->meth].name);
        ns_href = buf_len(&txn->buf) > 5 ? XML_NS_CALDAV : XML_NS_DAV;
        if (!root->ns || xmlStrcmp(root->ns->href, BAD_CAST ns_href) ||
            xmlStrcmp(root->name, BAD_CAST buf_lcase(&txn->buf))) {
            txn->error.desc =
                "Incorrect root element in MKCOL/MKCALENDAR request";
            ret = HTTP_BAD_REQUEST;
            goto done;
        }

        instr = root->children;
    }

    struct mboxlock *namespacelock = mboxname_usernamespacelock(txn->req_tgt.mbentry->name);

    /* Create the mailbox */
    r = mboxlist_createmailbox(txn->req_tgt.mbentry->name,
                               mparams->mkcol.mbtype, partition,
                               httpd_userisadmin || httpd_userisproxyadmin,
                               httpd_userid, httpd_authstate,
                               /*localonly*/0, /*forceuser*/0,
                               /*dbonly*/0, /*notify*/0,
                               &mailbox);

    if (instr && !r) {
        /* Start construction of our mkcol/mkcalendar response */
        buf_appendcstr(&txn->buf, "-response");
        root = init_xml_response(buf_cstring(&txn->buf), NS_REQ_ROOT, root, ns);
        buf_reset(&txn->buf);
        if (!root) {
            ret = HTTP_SERVER_ERROR;
            txn->error.desc = "Unable to create XML response\r\n";
            mboxname_release(&namespacelock);
            goto done;
        }

        outdoc = root->doc;

        /* Populate our proppatch context */
        pctx.txn = txn;
        pctx.mailbox = mailbox;
        pctx.lprops = mparams->propfind.lprops;
        pctx.root = root;
        pctx.ns = ns;
        pctx.tid = NULL;
        pctx.ret = &r;

        /* Execute the property patch instructions */
        ret = do_proppatch(&pctx, instr);

        if (ret || r) {
            /* Setting properties failed - delete mailbox */
            mailbox_abort(mailbox);
            mailbox_close(&mailbox);
            mboxlist_deletemailbox(txn->req_tgt.mbentry->name,
                                   /*isadmin*/1, NULL, NULL, NULL,
                                   /*checkacl*/0, /*localonly*/0, /*force*/1,
                                   /*keep_intermediaries*/0);

            if (!ret) {
                /* Output the XML response */
                if (txn->meth == METH_MKCALENDAR) {
                    /* MKCALENDAR failure response MUST be 207 (Multi-Status) */
                    xmlNodeSetName(root, BAD_CAST "multistatus");
                    xmlSetNs(root, ns[NS_DAV]);
                    r = HTTP_MULTI_STATUS;
                }
                xml_response(r, txn, outdoc);
            }

            mboxname_release(&namespacelock);
            goto done;
        }
    }
    mboxname_release(&namespacelock);

    if (!r) {
        if (mparams->mkcol.proc) r = mparams->mkcol.proc(mailbox);

        assert(!buf_len(&txn->buf));
        dav_get_synctoken(mailbox, &txn->buf, "");
        txn->resp_body.ctag = buf_cstring(&txn->buf);
        ret = HTTP_CREATED;
    }
    else if (r == IMAP_PERMISSION_DENIED) ret = HTTP_NO_PRIVS;
    else if (r == IMAP_MAILBOX_EXISTS) {
        txn->error.precond = DAV_RES_EXISTS;
        ret = HTTP_FORBIDDEN;
    }
    else {
        txn->error.desc = error_message(r);
        ret = HTTP_SERVER_ERROR;
    }

  done:
    buf_free(&pctx.buf);
    mailbox_close(&mailbox);

    if (partition) free(partition);
    if (outdoc) xmlFreeDoc(outdoc);
    if (indoc) xmlFreeDoc(indoc);

    return ret;
}


/* dav_foreach() callback to find props on a resource */
int propfind_by_resource(void *rock, void *data)
{
    struct propfind_ctx *fctx = (struct propfind_ctx *) rock;
    struct dav_data *ddata = (struct dav_data *) data;
    struct index_record record;
    char *p;
    size_t len;
    int r = 0, ret = 0;

    keepalive_response(fctx->txn);

    /* Append resource name to URL path */
    if (!fctx->req_tgt->resource) {
        len = strlen(fctx->req_tgt->path);
        p = fctx->req_tgt->path + len;
    }
    else {
        p = fctx->req_tgt->resource;
        len = p - fctx->req_tgt->path;
    }

    if (p[-1] != '/') {
        *p++ = '/';
        len++;
    }
    strlcpy(p, ddata->resource, MAX_MAILBOX_PATH - len);
    fctx->req_tgt->resource = p;
    fctx->req_tgt->reslen = strlen(p);

    fctx->data = data;
    if (ddata->imap_uid && !fctx->record) {
        /* Fetch index record for the resource */
        r = mailbox_find_index_record(fctx->mailbox, ddata->imap_uid, &record);

        fctx->record = r ? NULL : &record;
    }

    if (r || (!ddata->imap_uid && ddata->lock_expire <= time(NULL))) {
        /* Add response for missing target */
        ret = xml_add_response(fctx, HTTP_NOT_FOUND, 0, NULL, NULL);
    }
    else if (!fctx->filter || fctx->filter(fctx, data)) {
        /* Add response for target */
        ret = xml_add_response(fctx, 0, 0, NULL, NULL);
    }

    buf_free(&fctx->msg_buf);
    if (fctx->obj) {
        fctx->free_obj(fctx->obj);
        fctx->obj = NULL;
    }
    fctx->record = NULL;
    fctx->data = NULL;

    return ret;
}


static int propfind_by_resources(struct propfind_ctx *fctx)
{
    int r = 0;
    sqlite3 *newdb;

    if (!fctx->mailbox) return 0;

    /* Open the DAV DB corresponding to the mailbox.
     *
     * Note we open the new one first before closing the old one, so we
     * get refcounted retaining of the open database within a single user */
    newdb = fctx->open_db(fctx->mailbox);
    if (fctx->davdb) fctx->close_db(fctx->davdb);
    fctx->davdb = newdb;

    if (fctx->req_tgt->resource) {
        /* Add response for target resource */
        struct dav_data *ddata;

        /* Find message UID for the resource */
        fctx->lookup_resource(fctx->davdb, fctx->mailbox->name,
                              fctx->req_tgt->resource, (void **) &ddata, 0);
        if (!ddata->rowid) {
            /* Add response for missing target */
            xml_add_response(fctx, HTTP_NOT_FOUND, 0, NULL, NULL);
            return HTTP_NOT_FOUND;
        }
        r = fctx->proc_by_resource(fctx, ddata);
    }
    else {
        /* Add responses for all contained resources */
        fctx->foreach_resource(fctx->davdb, fctx->mailbox->name,
                               fctx->proc_by_resource, fctx);

        /* Started with NULL resource, end with NULL resource */
        fctx->req_tgt->resource = NULL;
        fctx->req_tgt->reslen = 0;
    }

    return r;
}


HIDDEN size_t make_collection_url(struct buf *buf, const char *urlprefix, int haszzzz,
                                  const mbname_t *mbname, const char *userid)
{
    const strarray_t *boxes;
    int n, size;
    size_t len;

    buf_reset(buf);
    buf_printf(buf, "%s/", urlprefix);

    if (userid) {
        const char *owner = mbname_userid(mbname);
        if (!owner) owner = "";

        if (config_getswitch(IMAPOPT_FASTMAILSHARING)) {
            buf_printf(buf, "%s/%s/", haszzzz ? "zzzz" : USER_COLLECTION_PREFIX, owner);
        }
        else {
            buf_printf(buf, "%s/", USER_COLLECTION_PREFIX);

            if (*userid) {
                buf_printf(buf, "%s/", userid);

                if (strcmp(owner, userid)) {
                    /* Encode shared collection as: <owner> "." <mboxname> */
                    buf_printf(buf, "%s%c", owner, SHARED_COLLECTION_DELIM);
                }
            }
            else buf_printf(buf, "%s/", owner);
        }
    }

    len = buf_len(buf);

    /* add collection(s) to path */
    boxes = mbname_boxes(mbname);
    size = strarray_size(boxes);
    for (n = 1; n < size; n++) {
        buf_appendcstr(buf, strarray_nth(boxes, n));
        buf_putc(buf, '/');
    }

    return len;
}


/* mboxlist_findall() callback to find props on a collection */
int propfind_by_collection(const mbentry_t *mbentry, void *rock)
{
    struct propfind_ctx *fctx = (struct propfind_ctx *) rock;
    const char *mboxname = mbentry->name;
    struct buf writebuf = BUF_INITIALIZER;
    struct mailbox *mailbox = NULL;
    char *p;
    size_t len;
    int r = 0, rights = 0;

    /* skip deleted items */
    if (mboxname_isdeletedmailbox(mbentry->name, 0) ||
        (mbentry->mbtype & MBTYPE_DELETED)) {
        goto done;
    }

    /* Check ACL on mailbox for current user */
    rights = httpd_myrights(httpd_authstate, mbentry);
    if ((rights & fctx->reqd_privs) != fctx->reqd_privs) goto done;

    /* We only match known types */
    if (!(mbentry->mbtype & fctx->req_tgt->namespace->mboxtype)) goto done;

    p = strrchr(mboxname, '.');
    if (!p) goto done;
    p++; /* skip dot */

    switch (fctx->req_tgt->namespace->id) {
    case URL_NS_DRIVE:
        if (fctx->req_tgt->flags == TGT_DRIVE_USER) {
            /* Special case of listing users with DAV #drives */
            p = strchr(mboxname+5, '.') + 1;  /* skip "user.XXX." */
            if (strcmp(p, fctx->req_tgt->mboxprefix)) goto done;
        }
        else if (p - mboxname > 1 + (int) strlen(fctx->req_tgt->mbentry->name)) {
            /* Reject folders that are more than one level deep */
            goto done;
        }
        break;

    default:
        /* Magic folder filter */
        if (httpd_extrafolder && strcasecmp(p, httpd_extrafolder)) goto done;
        break;
    }

    /* skip toplevels */
    if (config_getswitch(IMAPOPT_FASTMAILSHARING) && *p == '#')
        goto done;


    /* Open mailbox for reading */
    if ((r = mailbox_open_irl(mboxname, &mailbox))) {
        syslog(LOG_INFO, "mailbox_open_irl(%s) failed: %s",
               mboxname, error_message(r));
    }

    fctx->mbentry = mbentry;
    fctx->mailbox = mailbox;
    fctx->record = NULL;

    if (!fctx->req_tgt->resource) {
        /* we always have zzzz if it's already in the URL */
        int haszzzz = fctx->req_tgt->flags & TGT_USER_ZZZZ;

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

        /* we also need to deal with the discovery case,
         * where mboxname doesn't match request path */
        if (fctx->req_tgt->userid &&
            strcmpsafe(mbname_userid(mbname), fctx->req_tgt->userid)) {
            haszzzz = 1;
        }

        len = make_collection_url(&writebuf, fctx->req_tgt->namespace->prefix,
                                  haszzzz, mbname, fctx->req_tgt->userid);

        mbname_free(&mbname);

        /* copy it all back into place... in theory we should check against
         * 'last' and make sure it doesn't change from the original request.
         * yay for micro-optimised memory usage... */
        strlcpy(fctx->req_tgt->path, buf_cstring(&writebuf), MAX_MAILBOX_PATH);
        p = fctx->req_tgt->path + len;
        fctx->req_tgt->collection = p;
        fctx->req_tgt->collen = strlen(p);

        /* If not filtering by calendar resource, and not excluding root,
           add response for collection */
        if (!r && !fctx->filter_crit && !(fctx->prefer & PREFER_NOROOT) &&
            (r = xml_add_response(fctx, 0, 0, NULL, NULL))) goto done;
    }

    if (r) {
        xml_add_response(fctx, HTTP_SERVER_ERROR, 0, error_message(r), NULL);
        goto done;
    }

    if (fctx->depth > 1 && fctx->open_db) { // can't do davdb searches if no dav db
        /* Resource(s) */
        r = propfind_by_resources(fctx);
    }

  done:
    buf_free(&writebuf);
    if (mailbox) mailbox_close(&mailbox);

    return 0;
}

/* Free an entry list */
HIDDEN void free_entry_list(struct propfind_entry_list *elist)
{
    while (elist) {
        struct propfind_entry_list *freeme = elist;

        elist = elist->next;
        if (freeme->flags & PROP_CLEANUP) {
            freeme->get(freeme->name, NULL, NULL,
                        NULL, NULL, NULL, freeme->rock);
        }

        xmlFree(freeme->name);
        free(freeme);
    }
}

/* Perform a PROPFIND request */
EXPORTED int meth_propfind(struct transaction_t *txn, void *params)
{
    struct meth_params *fparams = (struct meth_params *) params;
    int ret = 0, r;
    const char **hdr;
    unsigned depth;
    xmlDocPtr indoc = NULL, outdoc = NULL;
    xmlNodePtr root, cur = NULL, props = NULL;
    xmlNsPtr ns[NUM_NAMESPACE];
    struct hash_table ns_table = { 0, NULL, NULL };
    struct propfind_ctx fctx;

    memset(&fctx, 0, sizeof(struct propfind_ctx));

    /* Parse the path */
    if (fparams->parse_path) {
        r = dav_parse_req_target(txn, fparams);
        if (r) return r;
    }

    /* Make sure method is allowed */
    if (!(txn->req_tgt.allow & ALLOW_DAV)) return HTTP_NOT_ALLOWED;

    /* Check Depth */
    hdr = spool_getheader(txn->req_hdrs, "Depth");
    if (!hdr || !strcmp(hdr[0], "infinity")) {
        depth = 3;

        if ((txn->error.precond = fparams->propfind.finite_depth_precond)) {
            ret = HTTP_FORBIDDEN;
            goto done;
        }
    }
    else if (!strcmp(hdr[0], "1")) {
        depth = 1;
    }
    else if (!strcmp(hdr[0], "0")) {
        depth = 0;
    }
    else {
        txn->error.desc = "Illegal Depth value\r\n";
        return HTTP_BAD_REQUEST;
    }

    if (txn->req_tgt.mbentry) {
        int rights;

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

        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 */
    }

    /* Principal or Local Mailbox */

    /* Parse the PROPFIND body, if exists */
    ret = parse_xml_body(txn, &root, NULL);
    if (ret) goto done;

    if (!root) {
        /* Empty request */
        fctx.mode = PROPFIND_ALL;
    }
    else {
        indoc = root->doc;

        /* Make sure its a DAV:propfind element */
        if (!root->ns || xmlStrcmp(root->ns->href, BAD_CAST XML_NS_DAV) ||
            xmlStrcmp(root->name, BAD_CAST "propfind")) {
            txn->error.desc = "Missing DAV:propfind element in PROPFIND request";
            ret = HTTP_BAD_REQUEST;
            goto done;
        }

        /* Find child element of propfind */
        for (cur = root->children;
             cur && cur->type != XML_ELEMENT_NODE; cur = cur->next);

        if (!cur) {
            txn->error.desc = "Missing child node element in PROPFIND request";
            ret = HTTP_BAD_REQUEST;
            goto done;
        }

        /* Add propfind type to our header cache */
        spool_cache_header(xstrdup(":type"), xstrdup((const char *) cur->name),
                           txn->req_hdrs);

        /* Make sure its a known element */
        if (!xmlStrcmp(cur->name, BAD_CAST "allprop")) {
            fctx.mode = PROPFIND_ALL;
        }
        else if (!xmlStrcmp(cur->name, BAD_CAST "propname")) {
            fctx.mode = PROPFIND_NAME;
            fctx.prefer = PREFER_MIN;  /* Don't want 404 (Not Found) */
        }
        else if (!xmlStrcmp(cur->name, BAD_CAST "prop")) {
            fctx.mode = PROPFIND_PROP;
            props = cur->children;
        }
        else {
            ret = HTTP_BAD_REQUEST;
            goto done;
        }

        /* Check for extra elements */
        for (cur = cur->next; cur; cur = cur->next) {
            if (cur->type == XML_ELEMENT_NODE) {
                if ((fctx.mode == PROPFIND_ALL) && !props &&
                    /* Check for 'include' element */
                    !xmlStrcmp(cur->name, BAD_CAST "include")) {
                    props = cur->children;
                }
                else {
                    ret = HTTP_BAD_REQUEST;
                    goto done;
                }
            }
        }
    }

    /* Start construction of our multistatus response */
    root = init_xml_response("multistatus", NS_DAV, root, ns);
    if (!root) {
        ret = HTTP_SERVER_ERROR;
        txn->error.desc = "Unable to create XML response";
        goto done;
    }

    outdoc = root->doc;

    /* Populate our propfind context */
    fctx.txn = txn;
    fctx.req_tgt = &txn->req_tgt;
    fctx.depth = depth;
    fctx.prefer |= get_preferences(txn);
    fctx.userid = httpd_userid;
    fctx.userisadmin = httpd_userisadmin;
    fctx.authstate = httpd_authstate;
    fctx.mbentry = NULL;
    fctx.mailbox = NULL;
    fctx.record = NULL;
    fctx.get_validators = fparams->get_validators;
    fctx.reqd_privs = DACL_READ;
    fctx.filter = NULL;
    fctx.filter_crit = NULL;
    if (fparams->mime_types) fctx.free_obj = fparams->mime_types[0].free;
    fctx.open_db = fparams->davdb.open_db;
    fctx.close_db = fparams->davdb.close_db;
    fctx.lookup_resource = fparams->davdb.lookup_resource;
    fctx.foreach_resource = fparams->davdb.foreach_resource;
    fctx.proc_by_resource = &propfind_by_resource;
    fctx.elist = NULL;
    fctx.lprops = fparams->propfind.lprops;
    fctx.root = root;
    fctx.ns = ns;
    fctx.ns_table = &ns_table;
    fctx.ret = &ret;

    /* Parse the list of properties and build a list of callbacks */
    ret = preload_proplist(props, &fctx);
    if (ret) goto done;

    /* iCalendar/vCard data in response should not be transformed */
    if (fctx.flags.fetcheddata) txn->flags.cc |= CC_NOTRANSFORM;

    /* Setup for chunked response */
    txn->flags.te |= TE_CHUNKED;

    /* Begin XML response */
    xml_response(HTTP_MULTI_STATUS, txn, fctx.root->doc);

    /* Generate responses */
    if (txn->req_tgt.namespace->id == URL_NS_PRINCIPAL) {
        if (!depth || !(fctx.prefer & PREFER_NOROOT)) {
            /* Add response for target URL */
            xml_add_response(&fctx, 0, 0, NULL, NULL);
        }

        if (depth > 0 && !txn->req_tgt.userid) {
            size_t len = strlen(namespace_principal.prefix);
            char *p = txn->req_tgt.path + len;

            if (!strcmp(p, "/" USER_COLLECTION_PREFIX "/")) {
                /* Normalize depth so that:
                 * 0 = prin-set, 1+ = collection, 2+ = principal, 3+ = infinity!
                 */
                depth++;
            }
            else {
                /* Add a response for 'user' collection */
                snprintf(p, MAX_MAILBOX_PATH - len,
                         "/%s/", USER_COLLECTION_PREFIX);
                xml_add_response(&fctx, 0, 0, NULL, NULL);
            }

            if (depth >= 2) {
                /* Add responses for all user principals */
                ret = mboxlist_alluser(principal_search, &fctx);
            }
        }
    }
    else {
        /* Normalize depth so that:
         * 0 = home-set, 1+ = collection, 2+ = resource, 3+ = infinity!
         */
        if (txn->req_tgt.collection) depth++;
        if (txn->req_tgt.resource) depth++;

        fctx.depth = depth;

        if (!txn->req_tgt.collection &&
            (!depth || !(fctx.prefer & PREFER_NOROOT))) {
            /* Add response for home-set collection */
            if (txn->req_tgt.mbentry) {
                /* Open mailbox for reading */
                if ((r = mailbox_open_irl(txn->req_tgt.mbentry->name,
                                          &fctx.mailbox))
                    && r != IMAP_MAILBOX_NONEXISTENT) {
                    syslog(LOG_INFO, "mailbox_open_irl(%s) failed: %s",
                           txn->req_tgt.mbentry->name, error_message(r));
                    txn->error.desc = error_message(r);
                    ret = HTTP_SERVER_ERROR;
                    goto done;
                }

                fctx.mbentry = txn->req_tgt.mbentry;
            }

            if (!fctx.req_tgt->resource)
                xml_add_response(&fctx, 0, 0, NULL, NULL);

            /* Resource(s) */
            r = propfind_by_resources(&fctx);
            if (r) ret = r;

            mailbox_close(&fctx.mailbox);
        }

        if (depth > 0) {
            /* Collection(s) */

            if (txn->req_tgt.collection) {
                /* Add response for target collection */
                propfind_by_collection(txn->req_tgt.mbentry, &fctx);
            }
            else if (config_getswitch(IMAPOPT_FASTMAILSHARING)) {
                /* Add responses for all visible collections */
                mboxlist_usermboxtree(httpd_userid, httpd_authstate,
                                      propfind_by_collection,
                                      &fctx, MBOXTREE_PLUS_RACL);
            }
            else if (txn->req_tgt.mbentry) {
                /* Add responses for all contained collections */
                fctx.prefer &= ~PREFER_NOROOT;
                mboxlist_mboxtree(txn->req_tgt.mbentry->name,
                                  propfind_by_collection, &fctx,
                                  MBOXTREE_SKIP_ROOT);

                switch (txn->req_tgt.namespace->id) {
                case URL_NS_DRIVE:
                    if (txn->req_tgt.flags == TGT_DRIVE_ROOT) {
                        /* Add a response for 'user' hierarchy */
                        buf_setcstr(&fctx.buf, txn->req_tgt.namespace->prefix);
                        buf_printf(&fctx.buf, "/%s/", USER_COLLECTION_PREFIX);
                        strlcpy(fctx.req_tgt->path,
                                buf_cstring(&fctx.buf), MAX_MAILBOX_PATH);
                        fctx.mbentry = NULL;
                        fctx.mailbox = NULL;
                        r = xml_add_response(&fctx, 0, 0, NULL, NULL);
                    }
                    break;

                case URL_NS_CALENDAR:
                    if (fctx.flags.cs_sharing) {
                        /* Add response for notification collection */
                        r = propfind_csnotify_collection(&fctx, props);
                    }
                    /* Fall through */

                case URL_NS_ADDRESSBOOK:
                    /* Add responses for shared collections */
                    mboxlist_usersubs(txn->req_tgt.userid,
                                      propfind_by_collection, &fctx,
                                      MBOXTREE_SKIP_PERSONAL);
                    break;
                }
            }

            ret = *fctx.ret;
        }
    }

    if (fctx.davdb) fctx.close_db(fctx.davdb);

    /* End XML response */
    xml_partial_response(txn, fctx.root->doc, NULL /* end */, 0, &fctx.xmlbuf);
    xmlBufferFree(fctx.xmlbuf);

    /* End of output */
    write_body(0, txn, NULL, 0);
    ret = 0;

  done:
    /* Free the entry list */
    free_entry_list(fctx.elist);

    buf_free(&fctx.buf);

    free_hash_table(&ns_table, NULL);

    if (outdoc) xmlFreeDoc(outdoc);
    if (indoc) xmlFreeDoc(indoc);

    return ret;
}


/* Perform a PROPPATCH request
 *
 * preconditions:
 *   DAV:cannot-modify-protected-property
 *   CALDAV:valid-calendar-data (CALDAV:calendar-timezone)
 */
int meth_proppatch(struct transaction_t *txn, void *params)
{
    struct meth_params *pparams = (struct meth_params *) params;
    int ret = 0, r = 0;
    xmlDocPtr indoc = NULL, outdoc = NULL;
    xmlNodePtr root, instr, resp;
    xmlNsPtr ns[NUM_NAMESPACE];
    struct mailbox *mailbox = NULL;
    struct proppatch_ctx pctx;
    struct index_record record;
    void *davdb = NULL;

    memset(&pctx, 0, sizeof(struct proppatch_ctx));

    /* Response should not be cached */
    txn->flags.cc |= CC_NOCACHE;

    /* Parse the path */
    r = dav_parse_req_target(txn, pparams);
    if (r) return r;

    if (!txn->req_tgt.collection && !txn->req_tgt.userid) {
        txn->error.desc = "PROPPATCH requires a collection";
        return HTTP_NOT_ALLOWED;
    }

    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 */

    r = mailbox_open_iwl(txn->req_tgt.mbentry->name, &mailbox);
    if (r) {
        syslog(LOG_ERR, "IOERROR: failed to open mailbox %s for proppatch",
               txn->req_tgt.mbentry->name);
        return HTTP_SERVER_ERROR;
    }

    /* Parse the PROPPATCH body */
    ret = parse_xml_body(txn, &root, NULL);
    if (!ret && !root) {
        txn->error.desc = "Missing request body\r\n";
        ret = HTTP_BAD_REQUEST;
    }
    if (ret) goto done;

    indoc = root->doc;

    /* Make sure its a DAV:propertyupdate element */
    if (!root->ns || xmlStrcmp(root->ns->href, BAD_CAST XML_NS_DAV) ||
        xmlStrcmp(root->name, BAD_CAST "propertyupdate")) {
        txn->error.desc =
            "Missing DAV:propertyupdate element in PROPPATCH request";
        ret = HTTP_BAD_REQUEST;
        goto done;
    }
    instr = root->children;

    /* Start construction of our multistatus response */
    if (!(root = init_xml_response("multistatus", NS_DAV, root, ns))) {
        txn->error.desc = "Unable to create XML response\r\n";
        ret = HTTP_SERVER_ERROR;
        goto done;
    }

    outdoc = root->doc;

    /* Add a response tree to 'root' for the specified href */
    resp = xmlNewChild(root, NULL, BAD_CAST "response", NULL);
    if (!resp) syslog(LOG_ERR, "new child response failed");
    xmlNewChild(resp, NULL, BAD_CAST "href", BAD_CAST txn->req_tgt.path);

    /* Populate our proppatch context */
    pctx.txn = txn;
    pctx.mailbox = mailbox;
    pctx.record = NULL;
    pctx.lprops = pparams->propfind.lprops;
    pctx.root = resp;
    pctx.ns = ns;
    pctx.tid = NULL;
    pctx.ret = &r;

    if (txn->req_tgt.resource) {
        struct dav_data *ddata;
        /* gotta find the resource */
        /* Open the DAV DB corresponding to the mailbox */
        davdb = pparams->davdb.open_db(mailbox);

        /* Find message UID for the resource */
        pparams->davdb.lookup_resource(davdb, txn->req_tgt.mbentry->name,
                                       txn->req_tgt.resource, (void **) &ddata, 0);
        if (!ddata->imap_uid) {
            ret = HTTP_NOT_FOUND;
            goto done;
        }

        memset(&record, 0, sizeof(struct index_record));
        /* Mapped URL - Fetch index record for the resource */
        r = mailbox_find_index_record(mailbox, ddata->imap_uid, &record);
        if (r) {
            ret = HTTP_NOT_FOUND;
            goto done;
        }

        pctx.record = &record;
    }

    /* Execute the property patch instructions */
    ret = do_proppatch(&pctx, instr);

    /* Output the XML response */
    if (!ret) {
        if (r) mailbox_abort(mailbox);
        else if (get_preferences(txn) & PREFER_MIN) {
            ret = HTTP_OK;
            goto done;
        }

        xml_response(HTTP_MULTI_STATUS, txn, outdoc);
    }

  done:
    if (davdb) pparams->davdb.close_db(davdb);
    mailbox_close(&mailbox);
    buf_free(&pctx.buf);

    if (outdoc) xmlFreeDoc(outdoc);
    if (indoc) xmlFreeDoc(indoc);

    return ret;
}


static int dav_post_import(struct transaction_t *txn,
                          struct meth_params *pparams)
{
    int ret = 0, r, precond = HTTP_OK, rights;
    const char **hdr;
    struct mime_type_t *mime = NULL;
    struct mailbox *mailbox = NULL;
    quota_t qdiffs[QUOTA_NUMRESOURCES] = QUOTA_DIFFS_INITIALIZER;
    void *davdb = NULL, *obj = NULL;
    xmlDocPtr outdoc = NULL;
    xmlNodePtr root;
    xmlNsPtr ns[NUM_NAMESPACE];
    unsigned data_ns = pparams->post.bulk.data_ns;

    /* Check Content-Type */
    mime = pparams->mime_types;
    if ((hdr = spool_getheader(txn->req_hdrs, "Content-Type"))) {
        for (; mime->content_type; mime++) {
            if (is_mediatype(mime->content_type, hdr[0])) break;
        }
        if (!mime->content_type) {
            txn->error.precond = pparams->put.supp_data_precond;
            return HTTP_FORBIDDEN;
        }
    }

    /* Check ACL for current user */
    rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry);
    if (!(rights & DACL_WRITECONT) || !(rights & DACL_ADDRSRC)) {
        /* DAV:need-privileges */
        txn->error.precond = DAV_NEED_PRIVS;
        txn->error.resource = txn->req_tgt.path;
        txn->error.rights =
            !(rights & DACL_WRITECONT) ? DACL_WRITECONT : DACL_ADDRSRC;
        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 */
    txn->req_body.flags |= BODY_DECODE;
    r = http_read_req_body(txn);
    if (r) {
        txn->flags.conn = CONN_CLOSE;
        return r;
    }

    /* Check if we can append a new message to mailbox */
    qdiffs[QUOTA_STORAGE] = buf_len(&txn->req_body.payload);
    if ((r = append_check(txn->req_tgt.mbentry->name, httpd_authstate,
                          ACL_INSERT, ignorequota ? NULL : qdiffs))) {
        syslog(LOG_ERR, "append_check(%s) failed: %s",
               txn->req_tgt.mbentry->name, error_message(r));
        txn->error.desc = error_message(r);
        return HTTP_SERVER_ERROR;
    }

    /* Open mailbox for writing */
    r = mailbox_open_iwl(txn->req_tgt.mbentry->name, &mailbox);
    if (r) {
        syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s",
               txn->req_tgt.mbentry->name, error_message(r));
        txn->error.desc = error_message(r);
        return HTTP_SERVER_ERROR;
    }

    /* Open the DAV DB corresponding to the mailbox */
    davdb = pparams->davdb.open_db(mailbox);

    /* Check any preconditions */
    assert(!buf_len(&txn->buf));
    buf_printf(&txn->buf, "%u-%u-%u", mailbox->i.uidvalidity,
               mailbox->i.last_uid, mailbox->i.exists);
    ret = precond = pparams->check_precond(txn, pparams, mailbox, NULL,
                                           buf_cstring(&txn->buf),
                                           mailbox->index_mtime);
    buf_reset(&txn->buf);

    switch (precond) {
    case HTTP_OK:
        break;

    case HTTP_LOCKED:
        txn->error.precond = DAV_NEED_LOCK_TOKEN;
        txn->error.resource = txn->req_tgt.path;

        GCC_FALLTHROUGH

    case HTTP_PRECOND_FAILED:
    default:
        /* We failed a precondition */
        ret = precond;
        goto done;
    }

    /* Parse and validate the resources */
    obj = mime->to_object(&txn->req_body.payload);
    ret = pparams->post.bulk.import(txn, obj, NULL, NULL, NULL, NULL, 0);
    if (ret) goto done;

    /* Start construction of our multistatus response */
    root = init_xml_response("multistatus", NS_DAV, NULL, ns);
    if (!root) {
        ret = HTTP_SERVER_ERROR;
        txn->error.desc = "Unable to create XML response";
        goto done;
    }
    ensure_ns(ns, NS_CS, root, XML_NS_CS, "CS");
    ensure_ns(ns, data_ns, root, known_namespaces[data_ns].href,
              known_namespaces[data_ns].prefix);

    outdoc = root->doc;

    /* Setup for chunked response */
    txn->flags.te |= TE_CHUNKED;

    /* Include CTag in trailer */
    txn->flags.trailer |= TRAILER_CTAG;

    /* Begin XML response */
    xml_response(HTTP_MULTI_STATUS, txn, outdoc);

    /* Store the resources */
    ret = pparams->post.bulk.import(txn, obj, mailbox, davdb,
                                    root, ns, get_preferences(txn));

    /* Validators */
    dav_get_synctoken(mailbox, &txn->buf, "");
    txn->resp_body.ctag = buf_cstring(&txn->buf);

    /* End of output */
    write_body(0, txn, NULL, 0);
    ret = 0;

  done:
    if (outdoc) xmlFreeDoc(outdoc);
    if (obj) {
        if (pparams->mime_types[0].free) pparams->mime_types[0].free(obj);
    }
    if (davdb) pparams->davdb.close_db(davdb);
    mailbox_close(&mailbox);

    return ret;
}


/* Perform a POST request */
int meth_post(struct transaction_t *txn, void *params)
{
    struct meth_params *pparams = (struct meth_params *) params;
    struct strlist *action;
    int r, ret;
    size_t len;

    /* Response should not be cached */
    txn->flags.cc |= CC_NOCACHE;
    if (httpd_userid) txn->flags.cc |= CC_PRIVATE;

    /* Parse the path */
    r = dav_parse_req_target(txn, pparams);
    if (r) return r;

    /* Make sure method is allowed (only allowed on certain collections) */
    if (!(txn->req_tgt.allow & ALLOW_POST)) return HTTP_NOT_ALLOWED;

    /* Do any special processing */
    if (pparams->post.proc) {
        ret = pparams->post.proc(txn);
        if (ret != HTTP_CONTINUE) return ret;
    }

    /* Check for query params */
    action = hash_lookup("action", &txn->req_qparams);

    if (!action) {
        /* Check Content-Type */
        const char **hdr = spool_getheader(txn->req_hdrs, "Content-Type");

        if ((pparams->post.allowed & POST_SHARE) && hdr &&
            (is_mediatype(hdr[0], DAVSHARING_CONTENT_TYPE) ||
             is_mediatype(hdr[0], "text/xml"))) {
            /* Sharing request */
            return dav_post_share(txn, pparams);
        }
        else if (pparams->post.bulk.data_prop && hdr &&
                 is_mediatype(hdr[0], "application/xml")) {
            /* Bulk CRUD */
            return HTTP_FORBIDDEN;
        }
        else if (pparams->post.bulk.import && hdr) {
            /* Bulk import */
            return dav_post_import(txn, pparams);
        }
        else return HTTP_BAD_REQUEST;
    }

    if (!(pparams->post.allowed & POST_ADDMEMBER) ||
        !action || action->next || strcmp(action->s, "add-member")) {
        return HTTP_BAD_REQUEST;
    }

    /* POST add-member to regular collection */

    /* Append a unique resource name to URL path and perform a PUT */
    len = strlen(txn->req_tgt.path);
    txn->req_tgt.resource = txn->req_tgt.path + len;
    txn->req_tgt.reslen =
        snprintf(txn->req_tgt.resource, MAX_MAILBOX_PATH - len,
                 "%s.%s", makeuuid(), pparams->mime_types[0].file_ext ?
                 pparams->mime_types[0].file_ext : "");

    /* Tell client where to find the new resource */
    txn->location = txn->req_tgt.path;

    ret = meth_put(txn, params);

    if (ret != HTTP_CREATED) txn->location = NULL;

    return ret;
}


/* Perform a PATCH request
 *
 * preconditions:
 */
int meth_patch(struct transaction_t *txn, void *params)
{
    struct meth_params *pparams = (struct meth_params *) params;
    int ret, r, precond, rights;
    const char **hdr, *etag;
    struct patch_doc_t *patch_doc = NULL;
    struct mailbox *mailbox = NULL;
    struct dav_data *ddata;
    struct index_record oldrecord;
    time_t lastmod;
    unsigned flags = 0;
    void *davdb = NULL, *obj = NULL;
    struct buf msg_buf = BUF_INITIALIZER;

    /* Response should not be cached */
    txn->flags.cc |= CC_NOCACHE;

    /* Parse the path */
    r = dav_parse_req_target(txn, pparams);
    if (r) return r;

    /* Make sure method is allowed (only allowed on resources) */
    if (!(txn->req_tgt.allow & ALLOW_PATCH)) return HTTP_NOT_ALLOWED;

    /* Check Content-Type */
    if ((hdr = spool_getheader(txn->req_hdrs, "Content-Type"))) {
        for (patch_doc = pparams->patch_docs; patch_doc->format; patch_doc++) {
            if (is_mediatype(patch_doc->format, hdr[0])) break;
        }
    }
    if (!patch_doc || !patch_doc->format) {
        txn->resp_body.patch = pparams->patch_docs;
        return HTTP_BAD_MEDIATYPE;
    }

    /* Check ACL for current user */
    rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry);
    if (!(rights & DACL_WRITECONT)) {
        /* DAV:need-privileges */
        txn->error.precond = DAV_NEED_PRIVS;
        txn->error.resource = txn->req_tgt.path;
        txn->error.rights = DACL_WRITECONT;
        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 */
    txn->req_body.flags |= BODY_DECODE;
    ret = http_read_req_body(txn);
    if (ret) {
        txn->flags.conn = CONN_CLOSE;
        return ret;
    }

    /* Check if we can append a new message to mailbox */
    /* XXX  Can we guess-timate the size difference? */
    if ((r = append_check(txn->req_tgt.mbentry->name, httpd_authstate,
                          ACL_INSERT, NULL))) {
        syslog(LOG_ERR, "append_check(%s) failed: %s",
               txn->req_tgt.mbentry->name, error_message(r));
        txn->error.desc = error_message(r);
        return HTTP_SERVER_ERROR;
    }

    /* Open mailbox for writing */
    r = mailbox_open_iwl(txn->req_tgt.mbentry->name, &mailbox);
    if (r) {
        syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s",
               txn->req_tgt.mbentry->name, error_message(r));
        txn->error.desc = error_message(r);
        return HTTP_SERVER_ERROR;
    }

    /* Open the DAV DB corresponding to the mailbox */
    davdb = pparams->davdb.open_db(mailbox);

    /* Find message UID for the resource */
    pparams->davdb.lookup_resource(davdb, txn->req_tgt.mbentry->name,
                                   txn->req_tgt.resource, (void *) &ddata, 0);
    if (!ddata->imap_uid) {
        ret = HTTP_NOT_FOUND;
        goto done;
    }

    /* Fetch resource validators */
    r = pparams->get_validators(mailbox, (void *) ddata, httpd_userid,
                                &oldrecord, &etag, &lastmod);
    if (r) {
        txn->error.desc = error_message(r);
        ret = HTTP_SERVER_ERROR;
        goto done;
    }

    /* Check any preferences */
    flags = get_preferences(txn);

    /* Check any preconditions */
    ret = precond = pparams->check_precond(txn, params, mailbox,
                                           (void *) ddata, etag, lastmod);

    switch (precond) {
    case HTTP_PRECOND_FAILED:
        if (!(flags & PREFER_REP)) goto done;

        /* Fill in ETag and Last-Modified */
        txn->resp_body.etag = etag;
        txn->resp_body.lastmod = lastmod;

        /* Fall through and load message */
        GCC_FALLTHROUGH

    case HTTP_OK: {
        unsigned offset;
        struct buf buf = BUF_INITIALIZER;

        /* Load message containing the resource */
        mailbox_map_record(mailbox, &oldrecord, &msg_buf);

        /* Resource length doesn't include RFC 5322 header */
        offset = oldrecord.header_size;

        /* Parse existing resource */
        buf_init_ro(&buf, buf_base(&msg_buf) + offset,
                    buf_len(&msg_buf) - offset);
        obj = pparams->mime_types[0].to_object(&buf);
        buf_free(&buf);

        if (precond == HTTP_OK) {
            /* Parse, validate, and apply the patch document to the resource */
            ret = patch_doc->proc(txn, obj);
            if (!ret) {
                ret = pparams->put.proc(txn, obj, mailbox,
                                        txn->req_tgt.resource, davdb, flags);
                if (ret == HTTP_FORBIDDEN) ret = HTTP_UNPROCESSABLE;
            }
        }

        break;
    }

    case HTTP_LOCKED:
        txn->error.precond = DAV_NEED_LOCK_TOKEN;
        txn->error.resource = txn->req_tgt.path;

    default:
        /* We failed a precondition */
        goto done;
    }

    if (flags & PREFER_REP) {
        struct resp_body_t *resp_body = &txn->resp_body;
        struct mime_type_t *mime = pparams->mime_types;
        struct buf *data;

        if ((hdr = spool_getheader(txn->req_hdrs, "Accept"))) {
            mime = get_accept_type(hdr, pparams->mime_types);
            if (!mime) goto done;
        }

        switch (ret) {
        case HTTP_NO_CONTENT:
            ret = HTTP_OK;

            GCC_FALLTHROUGH

        case HTTP_CREATED:
        case HTTP_PRECOND_FAILED:
            /* Convert into requested MIME type */
            data = mime->from_object(obj);

            /* Fill in Content-Type, Content-Length */
            resp_body->type = mime->content_type;
            resp_body->len = buf_len(data);

            /* Fill in Content-Location */
            resp_body->loc = txn->req_tgt.path;

            /* Fill in Expires and Cache-Control */
            resp_body->maxage = 3600;   /* 1 hr */
            txn->flags.cc = CC_MAXAGE
                | CC_REVALIDATE         /* don't use stale data */
                | CC_NOTRANSFORM;       /* don't alter iCal data */

            /* Output current representation */
            write_body(ret, txn, buf_base(data), buf_len(data));

            buf_destroy(data);
            ret = 0;
            break;

        default:
            /* failure - do nothing */
            break;
        }
    }

  done:
    if (obj) {
        if (pparams->mime_types[0].free) pparams->mime_types[0].free(obj);
        buf_free(&msg_buf);
    }
    if (davdb) pparams->davdb.close_db(davdb);
    mailbox_close(&mailbox);

    return ret;
}


/* Perform a PUT request
 *
 * preconditions:
 *   *DAV:supported-address-data
 */
int meth_put(struct transaction_t *txn, void *params)
{
    struct meth_params *pparams = (struct meth_params *) params;
    int ret, r, precond, rights, reqd_rights;
    const char **hdr, *etag;
    struct mime_type_t *mime = NULL;
    struct mailbox *mailbox = NULL;
    struct dav_data *ddata;
    struct index_record oldrecord;
    quota_t qdiffs[QUOTA_NUMRESOURCES] = QUOTA_DIFFS_INITIALIZER;
    time_t lastmod;
    unsigned flags = 0;
    void *davdb = NULL, *obj = NULL;
    struct buf msg_buf = BUF_INITIALIZER;

    if (txn->meth == METH_POST) {
        reqd_rights = DACL_ADDRSRC;
    }
    else {
        /* Response should not be cached */
        txn->flags.cc |= CC_NOCACHE;

        /* Parse the path */
        r = dav_parse_req_target(txn, pparams);
        if (r) {
            switch (r){
            case HTTP_MOVED:
            case HTTP_SERVER_ERROR: return r;
            default: return HTTP_FORBIDDEN;
            }
        }

        /* Make sure method is allowed (only allowed on resources) */
        if (!((txn->req_tgt.allow & ALLOW_WRITE) && txn->req_tgt.resource))
            return HTTP_NOT_ALLOWED;

        reqd_rights = DACL_WRITECONT;

        if (txn->req_tgt.allow & ALLOW_USERDATA) reqd_rights |= DACL_PROPRSRC;
    }

    /* Make sure mailbox type is correct */
    if (txn->req_tgt.mbentry->mbtype != txn->req_tgt.namespace->mboxtype)
        return HTTP_FORBIDDEN;

    /* Make sure Content-Range isn't specified */
    if (spool_getheader(txn->req_hdrs, "Content-Range"))
        return HTTP_BAD_REQUEST;

    /* Check Content-Type */
    mime = pparams->mime_types;
    if ((hdr = spool_getheader(txn->req_hdrs, "Content-Type"))) {
        for (; mime->content_type; mime++) {
            if (is_mediatype(mime->content_type, hdr[0])) break;
        }
        if (!mime->content_type) {
            txn->error.precond = pparams->put.supp_data_precond;
            return HTTP_FORBIDDEN;
        }
    }

    /* Check ACL for current user */
    rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry);
    if (!(rights & reqd_rights)) {
        /* DAV:need-privileges */
        txn->error.precond = DAV_NEED_PRIVS;
        txn->error.resource = txn->req_tgt.path;
        txn->error.rights = reqd_rights;
        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 */
    txn->req_body.flags |= BODY_DECODE;
    ret = http_read_req_body(txn);
    if (ret) {
        txn->flags.conn = CONN_CLOSE;
        return ret;
    }

    if (rights & DACL_WRITECONT) {
        /* Check if we can append a new message to mailbox */
        qdiffs[QUOTA_STORAGE] = buf_len(&txn->req_body.payload);
        if ((r = append_check(txn->req_tgt.mbentry->name, httpd_authstate,
                              ACL_INSERT, ignorequota ? NULL : qdiffs))) {
            syslog(LOG_ERR, "append_check(%s) failed: %s",
                   txn->req_tgt.mbentry->name, error_message(r));
            txn->error.desc = error_message(r);
            return HTTP_SERVER_ERROR;
        }
    }

    /* Open mailbox for writing */
    r = mailbox_open_iwl(txn->req_tgt.mbentry->name, &mailbox);
    if (r) {
        syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s",
               txn->req_tgt.mbentry->name, error_message(r));
        txn->error.desc = error_message(r);
        return HTTP_SERVER_ERROR;
    }

    /* Open the DAV DB corresponding to the mailbox */
    davdb = pparams->davdb.open_db(mailbox);

    /* Find message UID for the resource, if exists */
    pparams->davdb.lookup_resource(davdb, txn->req_tgt.mbentry->name,
                                   txn->req_tgt.resource, (void *) &ddata, 0);
    /* XXX  Check errors */

    /* Fetch resource validators */
    r = pparams->get_validators(mailbox, (void *) ddata, httpd_userid,
                                &oldrecord, &etag, &lastmod);
    if (r) {
        txn->error.desc = error_message(r);
        ret = HTTP_SERVER_ERROR;
        goto done;
    }

    /* Check any preferences */
    flags = get_preferences(txn);

    /* Check any preconditions */
    if (txn->meth == METH_POST) {
        assert(!buf_len(&txn->buf));
        buf_printf(&txn->buf, "%u-%u-%u", mailbox->i.uidvalidity,
                   mailbox->i.last_uid, mailbox->i.exists);
        ret = precond = pparams->check_precond(txn, params, mailbox, NULL,
                                               buf_cstring(&txn->buf),
                                               mailbox->index_mtime);
        buf_reset(&txn->buf);
    }
    else {
        ret = precond = pparams->check_precond(txn, params, mailbox,
                                               (void *) ddata, etag, lastmod);
    }

    switch (precond) {
    case HTTP_OK:
        /* Parse, validate, and store the resource */
        obj = mime->to_object(&txn->req_body.payload);
        ret = pparams->put.proc(txn, obj, mailbox,
                                txn->req_tgt.resource, davdb, flags);
        break;

    case HTTP_PRECOND_FAILED:
        if ((flags & PREFER_REP) && ((rights & DACL_READ) == DACL_READ)) {
            /* Fill in ETag and Last-Modified */
            txn->resp_body.etag = etag;
            txn->resp_body.lastmod = lastmod;

            if (pparams->get) {
                r = pparams->get(txn, mailbox, &oldrecord, (void *) ddata, &obj);
                if (r != HTTP_CONTINUE) flags &= ~PREFER_REP;
            }
            else {
                unsigned offset;
                struct buf buf = BUF_INITIALIZER;

                /* Load message containing the resource */
                mailbox_map_record(mailbox, &oldrecord, &msg_buf);

                /* Resource length doesn't include RFC 5322 header */
                offset = oldrecord.header_size;

                /* Parse existing resource */
                buf_init_ro(&buf, buf_base(&msg_buf) + offset,
                            buf_len(&msg_buf) - offset);
                obj = pparams->mime_types[0].to_object(&buf);
                buf_free(&buf);
            }
        }
        break;

    case HTTP_LOCKED:
        txn->error.precond = DAV_NEED_LOCK_TOKEN;
        txn->error.resource = txn->req_tgt.path;

    default:
        /* We failed a precondition */
        goto done;
    }

    if (txn->req_tgt.allow & ALLOW_PATCH) {
        /* Add Accept-Patch formats to response */
        txn->resp_body.patch = pparams->patch_docs;
    }

    if (flags & PREFER_REP) {
        struct resp_body_t *resp_body = &txn->resp_body;
        const char **hdr;
        struct buf *data;

        if ((hdr = spool_getheader(txn->req_hdrs, "Accept"))) {
            mime = get_accept_type(hdr, pparams->mime_types);
            if (!mime) goto done;
        }

        switch (ret) {
        case HTTP_NO_CONTENT:
            ret = HTTP_OK;

            GCC_FALLTHROUGH

        case HTTP_CREATED:
        case HTTP_PRECOND_FAILED:
            /* Convert into requested MIME type */
            data = mime->from_object(obj);

            /* Fill in Content-Type, Content-Length */
            resp_body->type = mime->content_type;
            resp_body->len = buf_len(data);

            /* Fill in Content-Location */
            resp_body->loc = txn->req_tgt.path;

            /* Fill in Expires and Cache-Control */
            resp_body->maxage = 3600;   /* 1 hr */
            txn->flags.cc = CC_MAXAGE
                | CC_REVALIDATE         /* don't use stale data */
                | CC_NOTRANSFORM;       /* don't alter iCal data */
            if (httpd_userid) txn->flags.cc |= CC_PRIVATE;

            /* Output current representation */
            write_body(ret, txn, buf_base(data), buf_len(data));

            buf_destroy(data);
            ret = 0;
            break;

        default:
            /* failure - do nothing */
            break;
        }
    }

  done:
    if (obj && pparams->mime_types[0].free)
        pparams->mime_types[0].free(obj);
    buf_free(&msg_buf);
    if (davdb) pparams->davdb.close_db(davdb);
    mailbox_close(&mailbox);

    return ret;
}


/* CALDAV:calendar-multiget/CARDDAV:addressbook-multiget REPORT */
int report_multiget(struct transaction_t *txn, struct meth_params *rparams,
                    xmlNodePtr inroot, struct propfind_ctx *fctx)
{
    int r, ret = 0;
    struct mailbox *mailbox = NULL;
    xmlNodePtr node;

    /* Setup for chunked response */
    txn->flags.te |= TE_CHUNKED;

    /* Begin XML response */
    xml_response(HTTP_MULTI_STATUS, txn, fctx->root->doc);

    /* Get props for each href */
    for (node = inroot->children; node; node = node->next) {
        if ((node->type == XML_ELEMENT_NODE) &&
            !xmlStrcmp(node->name, BAD_CAST "href")) {
            xmlChar *href = xmlNodeListGetString(inroot->doc, node->children, 1);
            xmlURIPtr uri;
            struct request_target_t tgt;
            struct dav_data *ddata;
            const char *resultstr = NULL;

            memset(&tgt, 0, sizeof(struct request_target_t));

            /* Parse the URI */
            uri = parse_uri(METH_REPORT, (const char *) href,
                            1 /* path required */, &resultstr);
            xmlFree(href);
            if (!uri) {
                r = HTTP_FORBIDDEN;
            }
            else {
                /* Parse the path */
                tgt.namespace = txn->req_tgt.namespace;

                r = rparams->parse_path(uri->path, &tgt, &resultstr);
                xmlFreeURI(uri);
            }
            if (r) {
                if (r == HTTP_MOVED)
                    xml_add_response(fctx, HTTP_MOVED, 0, NULL, resultstr);
                else
                    xml_add_response(fctx, r, 0, resultstr, NULL);
                goto next;
            }

            fctx->req_tgt = &tgt;
            fctx->mbentry = tgt.mbentry;

            /* Check if we already have this mailbox open */
            if (!mailbox || strcmp(mailbox->name, tgt.mbentry->name)) {
                if (mailbox) mailbox_close(&mailbox);

                /* Open mailbox for reading */
                r = mailbox_open_irl(tgt.mbentry->name, &mailbox);
                if (r && r != IMAP_MAILBOX_NONEXISTENT) {
                    syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s",
                           tgt.mbentry->name, error_message(r));
                    xml_add_response(fctx, HTTP_SERVER_ERROR,
                                     0, error_message(r), NULL);
                    goto next;
                }

                fctx->mailbox = mailbox;
            }

            if (!fctx->mailbox || !tgt.resource) {
                /* Add response for missing target */
                xml_add_response(fctx, HTTP_NOT_FOUND, 0, NULL, NULL);
                goto next;
            }

            /* Open the DAV DB corresponding to the mailbox */
            fctx->davdb = rparams->davdb.open_db(fctx->mailbox);

            /* Find message UID for the resource */
            rparams->davdb.lookup_resource(fctx->davdb, tgt.mbentry->name,
                                           tgt.resource, (void **) &ddata, 0);
            ddata->resource = tgt.resource;
            /* XXX  Check errors */

            fctx->proc_by_resource(fctx, ddata);

            rparams->davdb.close_db(fctx->davdb);

        next:
            /* XXX - split this into a req_tgt cleanup */
            free(tgt.userid);
            mboxlist_entry_free(&tgt.mbentry);
        }
    }

    /* End XML response */
    xml_partial_response(txn, fctx->root->doc, NULL /* end */, 0, &fctx->xmlbuf);
    xmlBufferFree(fctx->xmlbuf);

    /* End of output */
    write_body(0, txn, NULL, 0);

    mailbox_close(&mailbox);

    return ret;
}


struct updates_rock {
    struct propfind_ctx *fctx;
    get_modseq_t get_modseq;
    uint32_t limit;
    modseq_t syncmodseq;
    modseq_t basemodseq;
    modseq_t *respmodseq;
    uint32_t *nresp;
};

static int updates_cb(void *rock, void *data)
{
    struct dav_data *ddata = (struct dav_data *) data;
    struct updates_rock *urock = (struct updates_rock *) rock;
    struct propfind_ctx *fctx = urock->fctx;
    modseq_t modseq = urock->get_modseq(fctx->mailbox, data, fctx->userid);

    if (!ddata->alive) {
        if (modseq <= urock->basemodseq) {
            /* Initial sync - ignore unmapped resources */
            return 0;
        }

        /* Report resource as NOT FOUND
           IMAP UID of 0 will cause index record to be ignored
           propfind_by_resource() will append our resource name */
        ddata->imap_uid = 0;
    }
    else if (modseq <= urock->syncmodseq) {
        /* Per-user modseq hasn't changed */
        return 0;
    }


    if (*urock->nresp >= urock->limit) {
        /* Number of responses has reached client-specified limit */
        return HTTP_NO_STORAGE;
    }
    else {
        /* Bump response count */
        *urock->nresp += 1;
    }

    /* respmodseq will be highest modseq of the resources we return */
    *(urock->respmodseq) = MAX(modseq, *(urock->respmodseq));

    /* Add <response> element for this resource to root */
    fctx->proc_by_resource(fctx, ddata);
    fctx->record = NULL;

    return 0;
}


/* DAV:sync-collection REPORT */
int report_sync_col(struct transaction_t *txn, struct meth_params *rparams,
                    xmlNodePtr inroot, struct propfind_ctx *fctx)
{
    int ret = 0, r;
    struct mailbox *mailbox = NULL;
    uint32_t uidvalidity = 0;
    modseq_t syncmodseq = 0;
    modseq_t basemodseq = 0;
    modseq_t highestmodseq = 0;
    modseq_t respmodseq = 0;
    uint32_t limit = UINT32_MAX - 1;
    uint32_t nresp = 0;
    xmlNodePtr node;
    char tokenuri[MAX_MAILBOX_PATH+1];

    /* XXX  Handle Depth (cal-home-set at toplevel) */

    /* Open mailbox for reading */
    r = mailbox_open_irl(txn->req_tgt.mbentry->name, &mailbox);
    if (r) {
        syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s",
               txn->req_tgt.mbentry->name, error_message(r));
        txn->error.desc = error_message(r);
        ret = HTTP_SERVER_ERROR;
        goto done;
    }

    fctx->mbentry = txn->req_tgt.mbentry;
    fctx->mailbox = mailbox;

    highestmodseq = mailbox->i.highestmodseq;

    /* Parse children element of report */
    for (node = inroot->children; node; node = node->next) {
        xmlNodePtr node2;
        xmlChar *str = NULL;
        if (node->type == XML_ELEMENT_NODE) {
            if (!xmlStrcmp(node->name, BAD_CAST "sync-token") &&
                (str = xmlNodeListGetString(inroot->doc, node->children, 1))) {
                /* Add sync-token to our header cache */
                spool_cache_header(xstrdup(":token"),
                                   xstrdup((const char *) str), txn->req_hdrs);

                /* Parse sync-token */
                r = sscanf((char *) str, SYNC_TOKEN_URL_SCHEME
                           "%u-" MODSEQ_FMT "-" MODSEQ_FMT "%1s",
                           &uidvalidity, &syncmodseq, &basemodseq,
                           tokenuri /* test for trailing junk */);

                syslog(LOG_DEBUG, "scanned token %s to %d %u %llu %llu",
                       str, r, uidvalidity, syncmodseq, basemodseq);
                /* Sanity check the token components */
                if (r < 2 || r > 3 ||
                    (uidvalidity != mailbox->i.uidvalidity) ||
                    (syncmodseq > highestmodseq)) {
                    fctx->txn->error.desc = "Invalid sync-token";
                }
                else if (r == 3) {
                    /* Previous partial read token */
                    if (basemodseq > highestmodseq) {
                        fctx->txn->error.desc = "Invalid sync-token";
                    }
                    else if (basemodseq < mailbox->i.deletedmodseq) {
                        fctx->txn->error.desc = "Stale sync-token";
                    }
                }
                else {
                    /* Regular token */
                    if (syncmodseq < mailbox->i.deletedmodseq) {
                        fctx->txn->error.desc = "Stale sync-token";
                    }
                }

                if (fctx->txn->error.desc) {
                    /* DAV:valid-sync-token */
                    txn->error.precond = DAV_SYNC_TOKEN;
                    ret = HTTP_FORBIDDEN;
                }
            }
            else if (!xmlStrcmp(node->name, BAD_CAST "sync-level") &&
                (str = xmlNodeListGetString(inroot->doc, node->children, 1))) {
                if (!strcmp((char *) str, "infinity")) {
                    fctx->txn->error.desc =
                        "This server DOES NOT support infinite depth requests";
                    ret = HTTP_SERVER_ERROR;
                }
                else if ((sscanf((char *) str, "%u", &fctx->depth) != 1) ||
                         (fctx->depth != 1)) {
                    fctx->txn->error.desc = "Illegal sync-level";
                    ret = HTTP_BAD_REQUEST;
                }
            }
            else if (!xmlStrcmp(node->name, BAD_CAST "limit")) {
                errno = 0;

                for (node2 = node->children; node2; node2 = node2->next) {
                    if ((node2->type == XML_ELEMENT_NODE) &&
                        !xmlStrcmp(node2->name, BAD_CAST "nresults") &&
                        (!(str = xmlNodeListGetString(inroot->doc,
                                                      node2->children, 1)) ||
                         (sscanf((char *) str, "%u", &limit) != 1) ||
                         (errno != 0) || (limit >= UINT32_MAX))) {
                        txn->error.precond = DAV_OVER_LIMIT;
                        ret = HTTP_FORBIDDEN;
                    }
                }
            }

            if (str) xmlFree(str);
            if (ret) goto done;
        }
    }

    /* Check Depth */
    if (!fctx->depth) {
        fctx->txn->error.desc = "Illegal sync-level";
        ret = HTTP_BAD_REQUEST;
        goto done;
    }

    if (!syncmodseq) {
        /* Initial sync - set basemodseq in case client limits results */
        basemodseq = highestmodseq;
    }

    /* Open the DAV DB corresponding to the mailbox */
    fctx->davdb = rparams->davdb.open_db(fctx->mailbox);

    /* Setup for chunked response */
    txn->flags.te |= TE_CHUNKED;

    /* Begin XML response */
    xml_response(HTTP_MULTI_STATUS, txn, fctx->root->doc);

    /* Report the resources within the client requested limit (if any) */
    struct updates_rock rock = { fctx, rparams->get_modseq, limit,
                                 syncmodseq, basemodseq, &respmodseq, &nresp };

    r = rparams->davdb.foreach_update(fctx->davdb, syncmodseq, mailbox->name,
                                      -1 /* ALL kinds of resources */,
                                      (syncmodseq && basemodseq) ? 0 : limit + 1,
                                      &updates_cb, &rock);
    if (r) {
        /* Tell client we truncated the responses */
        if (fctx->req_tgt->resource) *(fctx->req_tgt->resource) = '\0';
        xml_add_response(fctx, HTTP_NO_STORAGE, DAV_OVER_LIMIT, NULL, NULL);
    }
    else {
        /* Full response - respmodseq will be highestmodseq of mailbox */
        respmodseq = highestmodseq;
    }

    if (fctx->davdb) rparams->davdb.close_db(fctx->davdb);

    /* Add sync-token element to root */
    if (respmodseq < basemodseq) {
        /* Client limited results of initial sync - include basemodseq */
        snprintf(tokenuri, MAX_MAILBOX_PATH,
                 SYNC_TOKEN_URL_SCHEME "%u-" MODSEQ_FMT "-" MODSEQ_FMT,
                 mailbox->i.uidvalidity, respmodseq, basemodseq);
    }
    else {
        snprintf(tokenuri, MAX_MAILBOX_PATH,
                 SYNC_TOKEN_URL_SCHEME "%u-" MODSEQ_FMT,
                 mailbox->i.uidvalidity, respmodseq);
    }
    node =
        xmlNewChild(fctx->root, NULL, BAD_CAST "sync-token", BAD_CAST tokenuri);

    /* Add sync-token element to output buffer */
    xml_partial_response(NULL /* !output */,
                         fctx->root->doc, node, 1, &fctx->xmlbuf);

    /* End XML response */
    xml_partial_response(txn, fctx->root->doc, NULL /* end */, 0, &fctx->xmlbuf);
    xmlBufferFree(fctx->xmlbuf);

    /* End of output */
    write_body(0, txn, NULL, 0);

  done:
    mailbox_close(&mailbox);

    return ret;
}


int expand_property(xmlNodePtr inroot, struct propfind_ctx *fctx,
                    struct namespace_t *namespace, const char *href,
                    parse_path_t parse_path, const struct prop_entry *lprops,
                    xmlNodePtr root, int depth)
{
    int ret = 0, r;
    struct propfind_ctx prev_ctx;
    struct request_target_t req_tgt;

    memcpy(&prev_ctx, fctx, sizeof(struct propfind_ctx));
    memset(&req_tgt, 0, sizeof(struct request_target_t));

    fctx->mode = PROPFIND_EXPAND;
    fctx->prefer &= ~PREFER_NOROOT;
    if (href) {
        /* Parse the URL */
        req_tgt.namespace = namespace;
        parse_path(href, &req_tgt, &fctx->txn->error.desc);

        fctx->req_tgt = &req_tgt;
    }
    fctx->lprops = lprops;
    fctx->elist = NULL;
    fctx->root = root;
    fctx->depth = depth;
    fctx->mbentry = NULL;
    fctx->mailbox = NULL;

    ret = preload_proplist(inroot->children, fctx);
    if (ret) goto done;

    if (!fctx->req_tgt->collection && !fctx->depth) {
        /* Add response for principal or home-set collection */
        struct mailbox *mailbox = NULL;

        if (fctx->req_tgt->mbentry) {
            /* Open mailbox for reading */
            if ((r = mailbox_open_irl(fctx->req_tgt->mbentry->name, &mailbox))) {
                syslog(LOG_INFO, "mailbox_open_irl(%s) failed: %s",
                       fctx->req_tgt->mbentry->name, error_message(r));
                fctx->txn->error.desc = error_message(r);
                ret = HTTP_SERVER_ERROR;
                goto done;
            }
            fctx->mbentry = fctx->req_tgt->mbentry;
            fctx->mailbox = mailbox;
        }

        xml_add_response(fctx, 0, 0, NULL, NULL);

        mailbox_close(&mailbox);
    }

    if (fctx->depth > 0) {
        /* Collection(s) */

        if (fctx->req_tgt->collection) {
            /* Add response for target collection */
            propfind_by_collection(fctx->req_tgt->mbentry, fctx);
        }
        else {
            /* Add responses for all contained collections */
            mboxlist_mboxtree(fctx->req_tgt->mbentry->name,
                              propfind_by_collection, fctx,
                              MBOXTREE_SKIP_ROOT);

            switch (fctx->req_tgt->namespace->id) {
            case URL_NS_CALENDAR:
            case URL_NS_ADDRESSBOOK:
                /* Add responses for shared collections */
                mboxlist_usersubs(fctx->req_tgt->userid,
                                  propfind_by_collection, fctx,
                                  MBOXTREE_SKIP_PERSONAL);
                break;
            }
        }

        if (fctx->davdb) fctx->close_db(fctx->davdb);

        ret = *fctx->ret;
    }

  done:
    /* Free the entry list */
    free_entry_list(fctx->elist);

    free(req_tgt.userid);

    fctx->mbentry = prev_ctx.mbentry;
    fctx->mailbox = prev_ctx.mailbox;
    fctx->depth = prev_ctx.depth;
    fctx->root = prev_ctx.root;
    fctx->elist = prev_ctx.elist;
    fctx->lprops = prev_ctx.lprops;
    fctx->req_tgt = prev_ctx.req_tgt;
    fctx->prefer = prev_ctx.prefer;

    if (root != fctx->root) {
        /* Move any defined namespaces up to the previous parent */
        xmlNsPtr nsDef;

        if (fctx->root->nsDef) {
            /* Find last nsDef in list */
            for (nsDef = fctx->root->nsDef; nsDef->next; nsDef = nsDef->next);
            nsDef->next = root->nsDef;
        }
        else fctx->root->nsDef = root->nsDef;
        root->nsDef = NULL;
    }

    return ret;
}



/* DAV:expand-property REPORT */
int report_expand_prop(struct transaction_t *txn __attribute__((unused)),
                       struct meth_params *rparams __attribute__((unused)),
                       xmlNodePtr inroot, struct propfind_ctx *fctx)
{
    int ret = expand_property(inroot, fctx, NULL, NULL, NULL,
                              fctx->lprops, fctx->root, fctx->depth);

    return (ret ? ret : HTTP_MULTI_STATUS);
}


/* DAV:acl-principal-prop-set REPORT */
int report_acl_prin_prop(struct transaction_t *txn __attribute__((unused)),
                         struct meth_params *rparams __attribute__((unused)),
                         xmlNodePtr inroot __attribute__((unused)),
                         struct propfind_ctx *fctx)
{
    int ret = 0;
    struct request_target_t req_tgt;
    mbentry_t *mbentry = fctx->req_tgt->mbentry;
    char *userid, *nextid;

    /* Generate URL for user principal collection */
    buf_reset(&fctx->buf);
    buf_printf(&fctx->buf, "%s/%s/",
               namespace_principal.prefix, USER_COLLECTION_PREFIX);

    /* Allowed properties are for principals, NOT the request URL */
    memset(&req_tgt, 0, sizeof(struct request_target_t));
    principal_parse_path(buf_cstring(&fctx->buf), &req_tgt,
                         &fctx->txn->error.desc);
    fctx->req_tgt = &req_tgt;
    fctx->lprops = principal_props;
    fctx->proc_by_resource = &propfind_by_resource;

    /* Parse the ACL string (userid/rights pairs) */
    for (userid = mbentry->acl; userid; userid = nextid) {
        char *rightstr;

        rightstr = strchr(userid, '\t');
        if (!rightstr) break;
        *rightstr++ = '\0';

        nextid = strchr(rightstr, '\t');
        if (!nextid) break;
        *nextid++ = '\0';

        if (strcmp(userid, "anyone") && strcmp(userid, "anonymous")) {
            /* Add userid to principal URL */
            strcpy(req_tgt.tail, userid);
            req_tgt.userid = xstrdup(userid);

            /* Add response for URL */
            xml_add_response(fctx, 0, 0, NULL, NULL);

            free(req_tgt.userid);
        }
    }

    return (ret ? ret : HTTP_MULTI_STATUS);
}


struct search_crit {
    struct strlist *props;
    xmlChar *match;
    struct search_crit *next;
};


/* mboxlist_alluser() callback to find user principals (has Inbox) */
static int principal_search(const char *userid, void *rock)
{
    struct propfind_ctx *fctx = (struct propfind_ctx *) rock;
    struct search_crit *search_crit;
    size_t len;
    char *p;

    /* XXX - this function needs extradomain and virtdomains support */

    /* Check ACL for current user */
    if (principal_acl_check(userid, httpd_authstate)) return 0;

    /* Check against search criteria */
    for (search_crit = (struct search_crit *) fctx->filter_crit;
         search_crit; search_crit = search_crit->next) {
        struct strlist *prop;

        for (prop = search_crit->props; prop; prop = prop->next) {
            if (!strcmp(prop->s, "displayname")) {
                if (!xmlStrcasestr(BAD_CAST userid,
                                   search_crit->match)) return 0;
            }
            else if (!strcmp(prop->s, "calendar-user-address-set")) {
                char email[MAX_MAILBOX_NAME+1];

                snprintf(email, MAX_MAILBOX_NAME, "%s@%s",
                         userid, config_servername);
                if (!xmlStrcasestr(BAD_CAST email,
                                   search_crit->match)) return 0;
            }
            else if (!strcmp(prop->s, "calendar-user-type")) {
                if (!xmlStrcasestr(BAD_CAST "INDIVIDUAL",
                                   search_crit->match)) return 0;
            }
        }
    }

    /* Append principal name to URL path */
    len = strlen(namespace_principal.prefix);
    p = fctx->req_tgt->path + len;
    snprintf(p, MAX_MAILBOX_PATH - len, "/%s/%s/",
             USER_COLLECTION_PREFIX, userid);

    free(fctx->req_tgt->userid);
    fctx->req_tgt->userid = xstrdup(userid);

    return xml_add_response(fctx, 0, 0, NULL, NULL);
}


static const struct prop_entry prin_search_props[] = {

    /* WebDAV (RFC 4918) properties */
    { "displayname", NS_DAV, 0, NULL, NULL, NULL },

    /* CalDAV Scheduling (RFC 6638) properties */
    { "calendar-user-address-set", NS_CALDAV, 0, NULL, NULL, NULL },
    { "calendar-user-type", NS_CALDAV, 0, NULL, NULL, NULL },

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


/* DAV:principal-property-search REPORT */
static int report_prin_prop_search(struct transaction_t *txn,
                                   struct meth_params *rparams __attribute__((unused)),
                                   xmlNodePtr inroot,
                                   struct propfind_ctx *fctx)
{
    int ret = 0;
    xmlNodePtr node;
    struct search_crit *search_crit, *next;
    unsigned apply_prin_set = 0;

    /* Parse children element of report */
    fctx->filter_crit = NULL;
    for (node = inroot->children; node; node = node->next) {
        if (node->type == XML_ELEMENT_NODE) {
            if (!xmlStrcmp(node->name, BAD_CAST "property-search")) {
                xmlNodePtr search;

                search_crit = xzmalloc(sizeof(struct search_crit));
                search_crit->next = fctx->filter_crit;
                fctx->filter_crit = search_crit;

                for (search = node->children; search; search = search->next) {
                    if (search->type == XML_ELEMENT_NODE) {
                        if (!xmlStrcmp(search->name, BAD_CAST "prop")) {
                            xmlNodePtr prop;

                            for (prop = search->children;
                                 prop; prop = prop->next) {
                                if (prop->type == XML_ELEMENT_NODE) {
                                    const struct prop_entry *entry;

                                    for (entry = prin_search_props;
                                         entry->name &&
                                             xmlStrcmp(prop->name,
                                                       BAD_CAST entry->name);
                                         entry++);

                                    if (!entry->name) {
                                        txn->error.desc =
                                            "Unsupported XML search prop";
                                        ret = HTTP_BAD_REQUEST;
                                        goto done;
                                    }
                                    else {
                                        appendstrlist(&search_crit->props,
                                                      (char *) entry->name);
                                    }
                                }
                            }
                        }
                        else if (!xmlStrcmp(search->name, BAD_CAST "match")) {
                            if (search_crit->match) {
                                txn->error.desc =
                                    "Too many DAV:match XML elements";
                                ret = HTTP_BAD_REQUEST;
                                goto done;
                            }

                            search_crit->match =
                                xmlNodeListGetString(inroot->doc,
                                                     search->children, 1);
                        }
                        else {
                            txn->error.desc = "Unknown XML element";
                            ret = HTTP_BAD_REQUEST;
                            goto done;
                        }
                    }
                }

                if (!search_crit->props) {
                    txn->error.desc = "Missing DAV:prop XML element";
                    ret = HTTP_BAD_REQUEST;
                    goto done;
                }
                if (!search_crit->match) {
                    txn->error.desc = "Missing DAV:match XML element";
                    ret = HTTP_BAD_REQUEST;
                    goto done;
                }
            }
            else if (!xmlStrcmp(node->name, BAD_CAST "prop")) {
                /* Already parsed in meth_report() */
            }
            else if (!xmlStrcmp(node->name,
                                BAD_CAST "apply-to-principal-collection-set")) {
                apply_prin_set = 1;
            }
            else {
                txn->error.desc = "Unknown XML element";
                ret = HTTP_BAD_REQUEST;
                goto done;
            }
        }
    }

    if (!fctx->filter_crit) {
        txn->error.desc = "Missing DAV:property-search XML element";
        ret = HTTP_BAD_REQUEST;
        goto done;
    }

    /* Only search DAV:principal-collection-set */
    if (apply_prin_set || !fctx->req_tgt->userid) {
        /* XXX  Do LDAP/SQL lookup of CN/email-address(es) here */

        ret = mboxlist_alluser(principal_search, fctx);
    }

  done:
    for (search_crit = fctx->filter_crit; search_crit; search_crit = next) {
        next = search_crit->next;

        if (search_crit->match) xmlFree(search_crit->match);
        freestrlist(search_crit->props);
        free(search_crit);
    }

    return (ret ? ret : HTTP_MULTI_STATUS);
}


/* DAV:principal-search-property-set REPORT */
static int report_prin_search_prop_set(struct transaction_t *txn,
                                       struct meth_params *rparams __attribute__((unused)),
                                       xmlNodePtr inroot,
                                       struct propfind_ctx *fctx)
{
    xmlNodePtr node;
    const struct prop_entry *entry;

    /* Look for child elements in request */
    for (node = inroot->children; node; node = node->next) {
        if (node->type == XML_ELEMENT_NODE) {
            txn->error.desc =
                "DAV:principal-search-property-set XML element MUST be empty";
            return HTTP_BAD_REQUEST;
        }
    }

    for (entry = prin_search_props; entry->name; entry++) {
        node = xmlNewChild(fctx->root, NULL,
                           BAD_CAST "principal-search-property", NULL);
        node = xmlNewChild(node, NULL, BAD_CAST "prop", NULL);
        ensure_ns(fctx->ns, entry->ns, fctx->root,
                  known_namespaces[entry->ns].href,
                  known_namespaces[entry->ns].prefix);
        xmlNewChild(node, fctx->ns[entry->ns], BAD_CAST entry->name, NULL);
    }

    return HTTP_OK;
}


/* Perform a REPORT request */
int meth_report(struct transaction_t *txn, void *params)
{
    struct meth_params *rparams = (struct meth_params *) params;
    int ret = 0, r;
    const char **hdr;
    unsigned depth = 0;
    xmlNodePtr inroot = NULL, outroot = NULL, cur, prop = NULL, props = NULL;
    const struct report_type_t *report = NULL;
    xmlNsPtr ns[NUM_NAMESPACE];
    struct hash_table ns_table = { 0, NULL, NULL };
    struct propfind_ctx fctx;

    memset(&fctx, 0, sizeof(struct propfind_ctx));

    /* Parse the path */
    r = dav_parse_req_target(txn, rparams);
    if (r) return r;

    /* Make sure method is allowed */
    if (!(txn->req_tgt.allow & ALLOW_DAV)) return HTTP_NOT_ALLOWED;

    /* Check Depth */
    if ((hdr = spool_getheader(txn->req_hdrs, "Depth"))) {
        if (!strcmp(hdr[0], "infinity")) {
            depth = 2;
        }
        else if ((sscanf(hdr[0], "%u", &depth) != 1) || (depth > 1)) {
            txn->error.desc = "Illegal Depth value\r\n";
            return HTTP_BAD_REQUEST;
        }
    }

    /* Parse the REPORT body */
    ret = parse_xml_body(txn, &inroot, NULL);
    if (!ret && !inroot) {
        txn->error.desc = "Missing request body\r\n";
        return HTTP_BAD_REQUEST;
    }
    if (ret) goto done;

    /* Add report type to our header cache */
    spool_cache_header(xstrdup(":type"), xstrdup((const char *) inroot->name),
                       txn->req_hdrs);

    /* Check the report type against our supported list */
    for (report = rparams->reports; report && report->name; report++) {
        if (inroot->ns &&
            !xmlStrcmp(inroot->ns->href,
                       BAD_CAST known_namespaces[report->ns].href) &&
            !xmlStrcmp(inroot->name, BAD_CAST report->name)) break;
    }
    if (!report || !report->name) {
        syslog(LOG_WARNING, "REPORT %s", inroot->name);
        /* DAV:supported-report */
        txn->error.precond = DAV_SUPP_REPORT;
        ret = HTTP_FORBIDDEN;
        goto done;
    }

    /* Check any depth limit */
    if (depth && (report->flags & REPORT_DEPTH_ZERO)) {
        txn->error.desc = "Depth header field MUST have value zero (0)";
        ret = HTTP_BAD_REQUEST;
        goto done;
    }

    /* Normalize depth so that:
     * 0 = home-set collection, 1+ = calendar collection, 2+ = calendar resource
     */
    if (txn->req_tgt.collection) depth++;
    if (txn->req_tgt.resource) depth++;

    /* Check ACL and location of mailbox */
    if (report->flags & REPORT_NEED_MBOX) {
        int rights;

        /* Check ACL for current user */
        rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry);
        if ((rights & report->reqd_privs) != report->reqd_privs) {
            if (report->reqd_privs == DACL_READFB) ret = HTTP_NOT_FOUND;
            else {
                /* DAV:need-privileges */
                txn->error.precond = DAV_NEED_PRIVS;
                txn->error.resource = txn->req_tgt.path;
                txn->error.rights = report->reqd_privs;
                ret = HTTP_NO_PRIVS;
            }
            goto done;
        }

        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) ret = HTTP_UNAVAILABLE;
            else ret = http_pipe_req_resp(be, txn);
            goto done;
        }

        /* Local Mailbox */
    }

    /* Principal or Local Mailbox */

    if (report->flags & (REPORT_NEED_PROPS | REPORT_ALLOW_PROPS)) {
        /* Parse children element of report */
        for (cur = inroot->children; cur; cur = cur->next) {
            unsigned mode = PROPFIND_NONE;

            if (cur->type == XML_ELEMENT_NODE) {
                if (!xmlStrcmp(cur->name, BAD_CAST "allprop")) {
                    mode = PROPFIND_ALL;
                    prop = cur;
                }
                else if (!xmlStrcmp(cur->name, BAD_CAST "propname")) {
                    mode = PROPFIND_NAME;
                    fctx.prefer = PREFER_MIN;  /* Don't want 404 (Not Found) */
                    prop = cur;
                }
                else if (!xmlStrcmp(cur->name, BAD_CAST "prop")) {
                    mode = PROPFIND_PROP;
                    prop = cur;
                    props = cur->children;
                }
            }

            if (mode != PROPFIND_NONE) {
                if (fctx.mode != PROPFIND_NONE) {
                    txn->error.desc = "Multiple <*prop*> elements in REPORT";
                    ret = HTTP_BAD_REQUEST;
                    goto done;
                }

                fctx.mode = mode;
            }
        }

        if (!prop && (report->flags & REPORT_NEED_PROPS)) {
            txn->error.desc = "Missing <prop> element in REPORT";
            ret = HTTP_BAD_REQUEST;
            goto done;
        }
    }

    /* Start construction of our multistatus response */
    if (report->resp_root &&
        !(outroot = init_xml_response(report->resp_root, NS_DAV, inroot, ns))) {
        txn->error.desc = "Unable to create XML response\r\n";
        ret = HTTP_SERVER_ERROR;
        goto done;
    }

    /* Populate our propfind context */
    fctx.txn = txn;
    fctx.req_tgt = &txn->req_tgt;
    fctx.depth = depth;
    fctx.prefer |= get_preferences(txn);
    fctx.userid = httpd_userid;
    fctx.userisadmin = httpd_userisadmin;
    fctx.authstate = httpd_authstate;
    fctx.mbentry = NULL;
    fctx.mailbox = NULL;
    fctx.record = NULL;
    fctx.get_validators = rparams->get_validators;
    fctx.reqd_privs = report->reqd_privs;
    if (rparams->mime_types) fctx.free_obj = rparams->mime_types[0].free;
    fctx.proc_by_resource = &propfind_by_resource;
    fctx.elist = NULL;
    fctx.lprops = rparams->propfind.lprops;
    fctx.root = outroot;
    fctx.ns = ns;
    fctx.ns_table = &ns_table;
    fctx.ret = &ret;

    /* Parse the list of properties and build a list of callbacks */
    if (fctx.mode) {
        ret = preload_proplist(props, &fctx);

        /* iCalendar/vCard data in response should not be transformed */
        if (fctx.flags.fetcheddata) txn->flags.cc |= CC_NOTRANSFORM;
    }

    /* Process the requested report */
    if (!ret) ret = (*report->proc)(txn, rparams, inroot, &fctx);

    /* Output the XML response */
    if (outroot) {
        switch (ret) {
        case HTTP_OK:
        case HTTP_MULTI_STATUS:
            /* iCalendar/vCard data in response should not be transformed */
            if (fctx.flags.fetcheddata) txn->flags.cc |= CC_NOTRANSFORM;

            xml_response(ret, txn, outroot->doc);

            ret = 0;
            break;

        default:
            break;
        }
    }

  done:
    /* Free the entry list */
    free_entry_list(fctx.elist);

    buf_free(&fctx.buf);

    free_hash_table(&ns_table, NULL);

    if (inroot) xmlFreeDoc(inroot->doc);
    if (outroot) xmlFreeDoc(outroot->doc);

    return ret;
}


/* Perform a UNLOCK request
 *
 * preconditions:
 *   DAV:need-privileges
 *   DAV:lock-token-matches-request-uri
 */
int meth_unlock(struct transaction_t *txn, void *params)
{
    struct meth_params *lparams = (struct meth_params *) params;
    int ret = HTTP_NO_CONTENT, r, precond;
    const char **hdr, *token;
    struct mailbox *mailbox = NULL;
    struct dav_data *ddata;
    struct index_record record;
    const char *etag;
    time_t lastmod;
    size_t len;
    void *davdb = NULL;

    /* Response should not be cached */
    txn->flags.cc |= CC_NOCACHE;

    /* Parse the path */
    r = dav_parse_req_target(txn, lparams);
    if (r) return r;

    /* Make sure method is allowed (only allowed on resources) */
    if (!(txn->req_tgt.allow & ALLOW_WRITE)) return HTTP_NOT_ALLOWED;

    /* Check for mandatory Lock-Token header */
    if (!(hdr = spool_getheader(txn->req_hdrs, "Lock-Token"))) {
        txn->error.desc = "Missing Lock-Token header";
        return HTTP_BAD_REQUEST;
    }
    token = hdr[0];

    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 */

    /* Open mailbox for reading */
    r = mailbox_open_irl(txn->req_tgt.mbentry->name, &mailbox);
    if (r) {
        syslog(LOG_ERR, "http_mailbox_open(%s) failed: %s",
               txn->req_tgt.mbentry->name, error_message(r));
        txn->error.desc = error_message(r);
        ret = HTTP_SERVER_ERROR;
        goto done;
    }

    /* Open the DAV DB corresponding to the mailbox */
    davdb = lparams->davdb.open_db(mailbox);
    lparams->davdb.begin_transaction(davdb);

    /* Find message UID for the resource, if exists */
    lparams->davdb.lookup_resource(davdb, txn->req_tgt.mbentry->name,
                                   txn->req_tgt.resource, (void **) &ddata, 0);
    if (!ddata->rowid) {
        ret = HTTP_NOT_FOUND;
        goto done;
    }

    /* Check if resource is locked */
    if (ddata->lock_expire <= time(NULL)) {
        /* DAV:lock-token-matches-request-uri */
        txn->error.precond = DAV_BAD_LOCK_TOKEN;
        ret = HTTP_CONFLICT;
        goto done;
    }

    /* Check if current user owns the lock */
    if (strcmp(ddata->lock_ownerid, httpd_userid)) {
        /* Check ACL for current user */
        int 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;
            ret = HTTP_NO_PRIVS;
            goto done;
        }
    }

    /* Check if lock token matches */
    len = strlen(ddata->lock_token);
    if (token[0] != '<' || strlen(token) != len+2 || token[len+1] != '>' ||
        strncmp(token+1, ddata->lock_token, len)) {
        /* DAV:lock-token-matches-request-uri */
        txn->error.precond = DAV_BAD_LOCK_TOKEN;
        ret = HTTP_CONFLICT;
        goto done;
    }

    /* Fetch resource validators */
    r = lparams->get_validators(mailbox, (void *) ddata, httpd_userid,
                                &record, &etag, &lastmod);
    if (r) {
        txn->error.desc = error_message(r);
        ret = HTTP_SERVER_ERROR;
        goto done;
    }

    /* Check any preconditions */
    precond = lparams->check_precond(txn, params, mailbox,
                                     (void *) ddata, etag, lastmod);

    if (precond != HTTP_OK) {
        /* We failed a precondition - don't perform the request */
        ret = precond;
        goto done;
    }

    if (ddata->imap_uid) {
        /* Mapped URL - Remove the lock */
        ddata->lock_token = NULL;
        ddata->lock_owner = NULL;
        ddata->lock_ownerid = NULL;
        ddata->lock_expire = 0;

        lparams->davdb.write_resourceLOCKONLY(davdb, ddata);
    }
    else {
        /* Unmapped URL - Treat as lock-null and delete mapping entry */
        lparams->davdb.delete_resourceLOCKONLY(davdb, ddata->rowid);
    }

  done:
    if (davdb) {
        /* XXX error handling abort */
        lparams->davdb.commit_transaction(davdb);
        lparams->davdb.close_db(davdb);
    }
    mailbox_close(&mailbox);

    return ret;
}


int dav_store_resource(struct transaction_t *txn,
                       const char *data, size_t datalen,
                       struct mailbox *mailbox, struct index_record *oldrecord,
                       modseq_t createdmodseq, strarray_t *imapflags)
{
    int ret = HTTP_CREATED, r;
    hdrcache_t hdrcache = txn->req_hdrs;
    struct stagemsg *stage;
    FILE *f = NULL;
    const char **hdr, *cte;
    quota_t qdiffs[QUOTA_NUMRESOURCES] = QUOTA_DIFFS_DONTCARE_INITIALIZER;
    time_t now = time(NULL);
    struct appendstate as;

    /* Prepare to stage the message */
    if (!(f = append_newstage(mailbox->name, now, 0, &stage))) {
        syslog(LOG_ERR, "append_newstage(%s) failed", mailbox->name);
        txn->error.desc = "append_newstage() failed\r\n";
        return HTTP_SERVER_ERROR;
    }

    /* Create RFC 5322 header for resource */
    if ((hdr = spool_getheader(hdrcache, "User-Agent"))) {
        fprintf(f, "User-Agent: %s\r\n", hdr[0]);
    }

    if ((hdr = spool_getheader(hdrcache, "From"))) {
        fprintf(f, "From: %s\r\n", hdr[0]);
    }
    else {
        char *mimehdr;

        assert(!buf_len(&txn->buf));
        if (strchr(httpd_userid, '@')) {
            /* XXX  This needs to be done via an LDAP/DB lookup */
            buf_printf(&txn->buf, "<%s>", httpd_userid);
        }
        else {
            buf_printf(&txn->buf, "<%s@%s>", httpd_userid, config_servername);
        }

        mimehdr = charset_encode_mimeheader(buf_cstring(&txn->buf),
                                            buf_len(&txn->buf), 0);
        fprintf(f, "From: %s\r\n", mimehdr);
        free(mimehdr);
        buf_reset(&txn->buf);
    }

    if ((hdr = spool_getheader(hdrcache, "Subject"))) {
        fprintf(f, "Subject: %s\r\n", hdr[0]);
    }

    if ((hdr = spool_getheader(hdrcache, "Date"))) {
        fprintf(f, "Date: %s\r\n", hdr[0]);
    }
    else {
        char datestr[80];       /* XXX: Why do we need 80 character buffer? */
        time_to_rfc5322(now, datestr, sizeof(datestr));
        fprintf(f, "Date: %s\r\n", datestr);
    }

    if ((hdr = spool_getheader(hdrcache, "Message-ID"))) {
        fprintf(f, "Message-ID: %s\r\n", hdr[0]);
    }

    if ((hdr = spool_getheader(hdrcache, "X-Schedule-User-Address"))) {
        fprintf(f, "X-Schedule-User-Address: %s\r\n", hdr[0]);
    }

    if ((hdr = spool_getheader(hdrcache, "Content-Type"))) {
        fprintf(f, "Content-Type: %s\r\n", hdr[0]);
    }
    else fputs("Content-Type: application/octet-stream\r\n", f);

    if (!datalen) {
        datalen = strlen(data);
        cte = "8bit";
    }
    else {
        cte = strnchr(data, '\0', datalen) ? "binary" : "8bit";
    }
    fprintf(f, "Content-Transfer-Encoding: %s\r\n", cte);

    if ((hdr = spool_getheader(hdrcache, "Content-Disposition"))) {
        fprintf(f, "Content-Disposition: %s\r\n", hdr[0]);
    }

    if ((hdr = spool_getheader(hdrcache, "Content-Description"))) {
        fprintf(f, "Content-Description: %s\r\n", hdr[0]);
    }

    fprintf(f, "Content-Length: %u\r\n", (unsigned) datalen);

    fputs("MIME-Version: 1.0\r\n\r\n", f);

    /* Write the data to the file */
    fwrite(data, datalen, 1, f);
    qdiffs[QUOTA_STORAGE] = ftell(f);

    fclose(f);

    qdiffs[QUOTA_MESSAGE] = 1;

    /* Prepare to append the message to the mailbox */
    if ((r = append_setup_mbox(&as, mailbox, httpd_userid, httpd_authstate,
                          0, qdiffs, 0, 0, EVENT_MESSAGE_NEW|EVENT_CALENDAR))) {
        syslog(LOG_ERR, "append_setup(%s) failed: %s",
               mailbox->name, error_message(r));
        if (r == IMAP_QUOTA_EXCEEDED) {
            /* DAV:quota-not-exceeded */
            txn->error.precond = DAV_OVER_QUOTA;
            ret = HTTP_NO_STORAGE;
        } else {
            ret = HTTP_SERVER_ERROR;
        }
        txn->error.desc = "append_setup() failed\r\n";
    }
    else {
        struct body *body = NULL;

        strarray_t *flaglist = NULL;
        struct entryattlist *annots = NULL;

        if (oldrecord) {
            flaglist = mailbox_extract_flags(mailbox, oldrecord, httpd_userid);
            mailbox_get_annotate_state(mailbox, oldrecord->uid, NULL);
            annots = mailbox_extract_annots(mailbox, oldrecord);
        }

        /* XXX - casemerge?  Doesn't matter with flags */
        if (imapflags) {
            if (flaglist)
                strarray_cat(flaglist, imapflags);
            else
                flaglist = strarray_dup(imapflags);
        }

        /* Append the message to the mailbox */
        if ((r = append_fromstage(&as, &body, stage, now, createdmodseq, flaglist, 0, annots))) {
            syslog(LOG_ERR, "append_fromstage(%s) failed: %s",
                   mailbox->name, error_message(r));
            ret = HTTP_SERVER_ERROR;
            txn->error.desc = "append_fromstage() failed\r\n";
        }
        if (body) {
            message_free_body(body);
            free(body);
        }
        strarray_free(flaglist);
        freeentryatts(annots);

        if (r) append_abort(&as);
        else {
            /* Commit the append to the mailbox */
            if ((r = append_commit(&as))) {
                syslog(LOG_ERR, "append_commit(%s) failed: %s",
                       mailbox->name, error_message(r));
                ret = HTTP_SERVER_ERROR;
                txn->error.desc = "append_commit() failed\r\n";
            }
            else {
                if (oldrecord) {
                    /* Now that we have the replacement message in place
                       expunge the old one. */
                    int userflag;

                    ret = HTTP_NO_CONTENT;

                    /* Perform the actual expunge */
                    r = mailbox_user_flag(mailbox, DFLAG_UNBIND, &userflag, 1);
                    if (!r) {
                        oldrecord->user_flags[userflag/32] |= 1 << (userflag & 31);
                        oldrecord->internal_flags |= FLAG_INTERNAL_EXPUNGED;
                        r = mailbox_rewrite_index_record(mailbox, oldrecord);
                    }
                    if (r) {
                        syslog(LOG_ERR, "expunging record (%s) failed: %s",
                               mailbox->name, error_message(r));
                        txn->error.desc = error_message(r);
                        ret = HTTP_SERVER_ERROR;
                    }
                }

                if (!r) {
                    /* Read index record for new message (always the last one) */
                    struct index_record newrecord;
                    struct dav_data ddata;
                    static char etagbuf[256];
                    const char *etag;

                    ddata.alive = 1;
                    ddata.imap_uid = mailbox->i.last_uid;
                    dav_get_validators(mailbox, &ddata, httpd_userid, &newrecord,
                                       &etag, &txn->resp_body.lastmod);
                    strncpy(etagbuf, etag, 255);
                    etagbuf[255] = 0;
                    txn->resp_body.etag = etagbuf;
                }
            }
        }
    }

    append_removestage(stage);

    return ret;
}


static void my_dav_init(struct buf *serverinfo)
{
    time_t compile_time = calc_compile_time(__TIME__, __DATE__);
    struct stat sbuf;
    struct message_guid guid;
    xmlNodePtr root, node, apps, app;
    xmlNsPtr ns[NUM_NAMESPACE];

    buf_printf(serverinfo, " SQLite/%s", sqlite3_libversion());

    /* Generate token based on compile date/time of this source file,
       the number of available RSCALEs and the config file size/mtime */
    stat(config_filename, &sbuf);
    server_info_lastmod = MAX(compile_time, sbuf.st_mtime);

    buf_printf(&server_info_token, "%ld-%ld-%ld", (long) compile_time,
               sbuf.st_mtime, sbuf.st_size);
    message_guid_generate(&guid, buf_cstring(&server_info_token),
                          buf_len(&server_info_token));
    buf_setcstr(&server_info_token, message_guid_encode(&guid));

    /* Generate link header contents */
    buf_printf(&server_info_link,
               "<%s/%s>; rel=\"server-info\"; token=\"%s\"",
               namespace_principal.prefix, SERVER_INFO,
               buf_cstring(&server_info_token));

    /* Start construction of our server-info */
    if (!(root = init_xml_response("server-info", NS_DAV, NULL, ns))) {
        syslog(LOG_ERR, "Unable to create server-info XML");
        return;
    }

    /* Add token */
    xmlNewTextChild(root, ns[NS_DAV], BAD_CAST "token",
                    BAD_CAST buf_cstring(&server_info_token));

    /* Add server */
    if (config_serverinfo == IMAP_ENUM_SERVERINFO_ON) {
        node = xmlNewChild(root, NULL, BAD_CAST "server", NULL);
        xmlNewChild(node, ns[NS_DAV],
                    BAD_CAST "name", BAD_CAST "Cyrus-HTTP");
        xmlNewTextChild(node, ns[NS_DAV],
                        BAD_CAST "version", BAD_CAST CYRUS_VERSION);
    }

    /* Add global DAV features */
    node = xmlNewChild(root, NULL, BAD_CAST "features", NULL);
    xmlNewChild(node, ns[NS_DAV], BAD_CAST "class-1", NULL);
    xmlNewChild(node, ns[NS_DAV], BAD_CAST "class-2", NULL);
    xmlNewChild(node, ns[NS_DAV], BAD_CAST "class-3", NULL);
    xmlNewChild(node, ns[NS_DAV], BAD_CAST "access-control", NULL);
    xmlNewChild(node, ns[NS_DAV], BAD_CAST "extended-mkcol", NULL);
    xmlNewChild(node, ns[NS_DAV], BAD_CAST "quota", NULL);
    xmlNewChild(node, ns[NS_DAV], BAD_CAST "sync-collection", NULL);
    xmlNewChild(node, ns[NS_DAV], BAD_CAST "add-member", NULL);

    apps = xmlNewChild(root, NULL, BAD_CAST "applications", NULL);

    if (namespace_calendar.enabled) {
        app = xmlNewChild(apps, NULL, BAD_CAST "application", NULL);
        ensure_ns(ns, NS_CALDAV, app, XML_NS_CALDAV, "C");
        xmlNewChild(app, NULL, BAD_CAST "name", BAD_CAST "caldav");

        /* Add CalDAV features */
        node = xmlNewChild(app, NULL, BAD_CAST "features", NULL);
        xmlNewChild(node, ns[NS_CALDAV],
                    BAD_CAST "calendar-access", NULL);
        if (namespace_calendar.allow & ALLOW_CAL_SCHED)
            xmlNewChild(node, ns[NS_CALDAV],
                        BAD_CAST "calendar-auto-schedule", NULL);
        if (namespace_calendar.allow & ALLOW_CAL_NOTZ)
            xmlNewChild(node, ns[NS_CALDAV],
                        BAD_CAST "calendar-no-timezone", NULL);
        if (namespace_calendar.allow & ALLOW_CAL_AVAIL)
            xmlNewChild(node, ns[NS_CALDAV],
                        BAD_CAST "calendar-availability", NULL);
        if (namespace_calendar.allow & ALLOW_CAL_ATTACH) {
            xmlNewChild(node, ns[NS_CALDAV],
                        BAD_CAST "calendar-managed-attachments", NULL);
            xmlNewChild(node, ns[NS_CALDAV],
                        BAD_CAST "calendar-managed-attachments-no-recurrence",
                        NULL);
        }
    }

    if (namespace_addressbook.enabled) {
        app = xmlNewChild(apps, NULL, BAD_CAST "application", NULL);
        ensure_ns(ns, NS_CARDDAV, app, XML_NS_CARDDAV, "A");
        xmlNewChild(app, NULL, BAD_CAST "name", BAD_CAST "carddav");

        /* Add CardDAV features */
        node = xmlNewChild(app, NULL, BAD_CAST "features", NULL);
        xmlNewChild(node, ns[NS_CARDDAV], BAD_CAST "addressbook", NULL);
    }

    /* Dump XML response tree into a text buffer */
    xmlDocDumpFormatMemoryEnc(root->doc,
                                  &server_info, &server_info_size, "utf-8", 1);
    xmlFreeDoc(root->doc);

    if (!server_info) {
        syslog(LOG_ERR, "Unable to dump server-info XML tree");
    }

    return;
}


static void my_dav_shutdown(void)
{
    if (server_info) xmlFree(server_info);
    buf_free(&server_info_token);
    buf_free(&server_info_link);
}


static int get_server_info(struct transaction_t *txn)
{
    int precond;
    const char **hdr, *etag;

    if (!server_info) return HTTP_NOT_FOUND;

    if (!httpd_userid) return HTTP_UNAUTHORIZED;

    if ((hdr = spool_getheader(txn->req_hdrs, "Accept")) &&
        strcmp(hdr[0], "application/server-info+xml"))
        return HTTP_NOT_ACCEPTABLE;

    /* Check any preconditions, including range request */
    txn->flags.ranges = 1;
    etag = buf_cstring(&server_info_token);
    precond = check_precond(txn, etag, server_info_lastmod);

    switch (precond) {
    case HTTP_OK:
    case HTTP_PARTIAL:
    case HTTP_NOT_MODIFIED:
        /* Fill in Etag,  Last-Modified, and Expires */
        txn->resp_body.etag = etag;
        txn->resp_body.lastmod = server_info_lastmod;
        txn->resp_body.maxage = 86400;  /* 24 hrs */
        txn->flags.cc |= CC_MAXAGE;

        if (precond != HTTP_NOT_MODIFIED) break;

        GCC_FALLTHROUGH

    default:
        /* We failed a precondition - don't perform the request */
        return precond;
    }

    /* Output the XML response */
    txn->resp_body.type = "application/server-info+xml; charset=utf-8";
    write_body(precond, txn, (char *) server_info, server_info_size);

    return 0;
}

static void dav_parse_textmatch(xmlNodePtr node, struct text_match_t **match,
                                struct filter_profile_t *profile,
                                struct error_t *error)
{
    unsigned negate = 0;
    unsigned type = MATCH_TYPE_CONTAINS;
    unsigned collation = profile->collation;
    xmlChar *attr;

    *match = NULL;

    attr = xmlGetProp(node, BAD_CAST "negate-condition");
    if (attr) {
        if (!xmlStrcmp(attr, BAD_CAST "yes")) negate = 1;
        else if (xmlStrcmp(attr, BAD_CAST "no")) {
            error->precond = profile->filter_precond;
            error->desc = "negate-condition is a yes/no option";
            error->node = xmlCopyNode(node->parent, 1);
        }
        xmlFree(attr);
    }

    if (!error->precond) {
        attr = xmlGetProp(node, BAD_CAST "match-type");
        if (attr) {
            const struct match_type_t *match;

            for (match = dav_match_types; match->name; match++) {
                if (!xmlStrcmp(attr, BAD_CAST match->name)) break;
            }
            if (match->name) type = match->value;
            else {
                error->precond = profile->filter_precond;
                error->desc = "Unsupported match-type";
                error->node = xmlCopyNode(node->parent, 1);
            }
            xmlFree(attr);
        }
    }

    if (!error->precond) {
        attr = xmlGetProp(node, BAD_CAST "collation");
        if (attr) {
            if (xmlStrcmp(attr, BAD_CAST "default")) {
                const struct collation_t *col;

                for (col = dav_collations; col->name; col++) {
                    if (!xmlStrcmp(attr, BAD_CAST col->name)) break;
                }
                if (col->name) collation = col->value;
                else error->precond = profile->collation_precond;
            }
            xmlFree(attr);
        }
    }

    if (!error->precond) {
        *match = xzmalloc(sizeof(struct text_match_t));
        (*match)->text = xmlNodeGetContent(node);
        (*match)->negate = negate;
        (*match)->type = type;
        (*match)->collation = collation;
    }
}

static void dav_parse_paramfilter(xmlNodePtr root, struct param_filter **param,
                                  struct filter_profile_t *profile,
                                  struct error_t *error)
{
    xmlChar *attr;
    xmlNodePtr node;

    attr = xmlGetProp(root, BAD_CAST "name");
    if (!attr) {
        error->precond = profile->filter_precond;
        error->desc = "Missing 'name' attribute";
        error->node = xmlCopyNode(root, 2);
    }
    else {
        *param = xzmalloc(sizeof(struct param_filter));
        (*param)->name = attr;

        if (profile->param_string_to_kind) {
            (*param)->kind = profile->param_string_to_kind((const char *) attr);
        
            if ((*param)->kind == profile->no_param_value) {
                error->precond = profile->filter_precond;
                error->desc = "Unsupported parameter";
                error->node = xmlCopyNode(root, 2);
            }
        }
    }

    for (node = xmlFirstElementChild(root); node && !error->precond;
         node = xmlNextElementSibling(node)) {

        if ((*param)->not_defined) {
            error->precond = profile->filter_precond;
            error->desc = DAV_FILTER_ISNOTDEF_ERR;
            error->node = xmlCopyNode(root, 1);
        }
        else if (!xmlStrcmp(node->name, BAD_CAST "is-not-defined")) {
            if ((*param)->match) {
                error->precond = profile->filter_precond;
                error->desc = DAV_FILTER_ISNOTDEF_ERR;
                error->node = xmlCopyNode(root, 1);
            }
            else (*param)->not_defined = 1;
        }
        else if (!xmlStrcmp(node->name, BAD_CAST "text-match")) {
            if ((*param)->match) {
                error->precond = profile->filter_precond;
                error->desc = "Multiple text-match";
                error->node = xmlCopyNode(root, 1);
            }
            else {
                struct text_match_t *match = NULL;

                dav_parse_textmatch(node, &match, profile, error);
                (*param)->match = match;
            }
        }
        else {
            error->precond = profile->filter_precond;
            error->desc =
                "Unsupported element in param-filter";
            error->node = xmlCopyNode(root, 1);
        }
    }
}

void dav_parse_propfilter(xmlNodePtr root, struct prop_filter **prop,
                          struct filter_profile_t *profile,
                          struct error_t *error)
{
    xmlChar *attr;
    xmlNodePtr node;

    attr = xmlGetProp(root, BAD_CAST "name");
    if (!attr) {
        error->precond = profile->filter_precond;
        error->desc = "Missing 'name' attribute";
        error->node = xmlCopyNode(root, 2);
    }
    else {
        *prop = xzmalloc(sizeof(struct prop_filter));
        (*prop)->name = attr;
        (*prop)->allof = profile->allof;

        if (profile->prop_string_to_kind) {
            (*prop)->kind = profile->prop_string_to_kind((const char *) attr);

            if ((*prop)->kind == profile->no_prop_value) {
                error->precond = profile->filter_precond;
                error->desc = "Unsupported property";
                error->node = xmlCopyNode(root, 2);
            }
        }

        if (!error->precond) {
            attr = xmlGetProp(root, BAD_CAST "test");
            if (attr) {
                if (!xmlStrcmp(attr, BAD_CAST "allof")) (*prop)->allof = 1;
                else if (!xmlStrcmp(attr, BAD_CAST "anyof")) (*prop)->allof = 0;
                else {
                    error->precond = profile->filter_precond;
                    error->desc = "Unsupported test";
                    error->node = xmlCopyNode(root, 2);
                }
                xmlFree(attr);
            }
        }
    }

    for (node = xmlFirstElementChild(root); node && !error->precond;
         node = xmlNextElementSibling(node)) {

        if ((*prop)->not_defined) {
            error->precond = profile->filter_precond;
            error->desc = DAV_FILTER_ISNOTDEF_ERR;
            error->node = xmlCopyNode(root, 1);
        }
        else if (!xmlStrcmp(node->name, BAD_CAST "is-not-defined")) {
            if ((*prop)->other || (*prop)->match || (*prop)->param) {
                error->precond = profile->filter_precond;
                error->desc = DAV_FILTER_ISNOTDEF_ERR;
                error->node = xmlCopyNode(root, 1);
            }
            else (*prop)->not_defined = 1;
        }
        else if (!xmlStrcmp(node->name, BAD_CAST "text-match")) {
            struct text_match_t *match = NULL;

            dav_parse_textmatch(node, &match, profile, error);
            if (match) {
                if ((*prop)->match) match->next = (*prop)->match;
                (*prop)->match = match;
            }
        }
        else if (!xmlStrcmp(node->name, BAD_CAST "param-filter")) {
            struct param_filter *param = NULL;

            dav_parse_paramfilter(node, &param, profile, error);
            if (param) {
                if ((*prop)->param) param->next = (*prop)->param;
                (*prop)->param = param;
            }
        }
        else if (profile->parse_propfilter) {
            profile->parse_propfilter(node, *prop, error);
        }
        else {
            error->precond = profile->filter_precond;
            error->desc = "Unsupported element in prop-filter";
            error->node = xmlCopyNode(root, 1);
        }
    }
}

void dav_free_propfilter(struct prop_filter *prop)
{
    struct param_filter *param, *next;

    xmlFree(prop->name);
    if (prop->match) {
        xmlFree(prop->match->text);
        free(prop->match);
    }
    for (param = prop->param; param; param = next) {
        next = param->next;

        xmlFree(param->name);
        if (param->match) {
            xmlFree(param->match->text);
            free(param->match);
        }
        free(param);
    }
    free(prop);
}

int dav_apply_textmatch(xmlChar *text, struct text_match_t *match)
{
    const xmlChar *cp = NULL;
    int textlen, matchlen;
    int r = 0;

    switch (match->type) {
    case MATCH_TYPE_CONTAINS:
        switch (match->collation) {
        case COLLATION_UNICODE:
            /* XXX  how to do this? */
        case COLLATION_ASCII:
            cp = xmlStrcasestr(text, match->text);
            break;
        case COLLATION_OCTET:
            cp = xmlStrstr(text, match->text);
            break;
        }

        r = (cp != NULL);
        break;
    case MATCH_TYPE_EQUALS:
        switch (match->collation) {
        case COLLATION_UNICODE:
            /* XXX  how to do this? */
        case COLLATION_ASCII:
            r = !xmlStrcasecmp(text, match->text);
            break;
        case COLLATION_OCTET:
            r = xmlStrEqual(text, match->text);
            break;
        }
        break;
    case MATCH_TYPE_PREFIX:
        matchlen = xmlStrlen(match->text);

        switch (match->collation) {
        case COLLATION_UNICODE:
            /* XXX  how to do this? */
        case COLLATION_ASCII:
            r = !xmlStrncasecmp(text, match->text, matchlen);
            break;
        case COLLATION_OCTET:
            r = !xmlStrncmp(text, match->text, matchlen);
            break;
        }
        break;
    case MATCH_TYPE_SUFFIX:
        textlen = xmlStrlen(text);
        matchlen = xmlStrlen(match->text);

        if (textlen < matchlen) r = 0;
        else {
            cp = text += (textlen - matchlen);

            switch (match->collation) {
            case COLLATION_UNICODE:
                /* XXX  how to do this? */
            case COLLATION_ASCII:
                r = !xmlStrcasecmp(cp, match->text);
                break;
            case COLLATION_OCTET:
                r = xmlStrEqual(cp, match->text);
                break;
            }
        }
        break;
    }

    if (match->negate) r = !r;

    return r;
}
