/* http_carddav.c -- Routines for handling CardDAV collections in httpd
 *
 * Copyright (c) 1994-2013 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:
 *   Support <filter> for addressbook-query Report
 *
 */

#include <config.h>

#include <sysexits.h>
#include <syslog.h>

#include <libxml/tree.h>
#include <libxml/uri.h>
#include <sys/types.h>
#include <sys/wait.h>

#include "acl.h"
#include "append.h"
#include "carddav_db.h"
#include "global.h"
#include "hash.h"
#include "httpd.h"
#include "http_dav.h"
#include "http_dav_sharing.h"
#include "http_proxy.h"
#include "index.h"
#include "mailbox.h"
#include "mboxlist.h"
#include "message.h"
#include "message_guid.h"
#include "proxy.h"
#include "smtpclient.h"
#include "spool.h"
#include "strhash.h"
#include "stristr.h"
#include "times.h"
#include "user.h"
#include "util.h"
#include "vcard_support.h"
#include "version.h"
#include "vparse.h"
#include "xmalloc.h"
#include "xml_support.h"
#include "xstrlcat.h"
#include "xstrlcpy.h"

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

static struct carddav_db *auth_carddavdb = NULL;
static time_t compile_time;

static void my_carddav_init(struct buf *serverinfo);
static int my_carddav_auth(const char *userid);
static void my_carddav_reset(void);
static void my_carddav_shutdown(void);

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

static int carddav_copy(struct transaction_t *txn, void *obj,
                        struct mailbox *mailbox, const char *resource,
                        void *destdb, unsigned flags);

static int carddav_get(struct transaction_t *txn, struct mailbox *mailbox,
                       struct index_record *record, void *data, void **obj);

static int carddav_put(struct transaction_t *txn, void *obj,
                       struct mailbox *mailbox, const char *resource,
                       void *destdb, unsigned flags);

static int carddav_import(struct transaction_t *txn, void *obj,
                          struct mailbox *mailbox, void *destdb,
                          xmlNodePtr root, xmlNsPtr *ns, unsigned flags);

static int propfind_getcontenttype(const xmlChar *name, xmlNsPtr ns,
                                   struct propfind_ctx *fctx,
                                   xmlNodePtr prop, xmlNodePtr resp,
                                   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_addrdata(const xmlChar *name, xmlNsPtr ns,
                             struct propfind_ctx *fctx,
                             xmlNodePtr prop, xmlNodePtr resp,
                             struct propstat propstat[], void *rock);
static int propfind_suppaddrdata(const xmlChar *name, xmlNsPtr ns,
                                 struct propfind_ctx *fctx,
                                 xmlNodePtr prop, xmlNodePtr resp,
                                 struct propstat propstat[], void *rock);
static int propfind_addrgroups(const xmlChar *name, xmlNsPtr ns,
                               struct propfind_ctx *fctx,
                               xmlNodePtr prop, xmlNodePtr resp,
                               struct propstat propstat[], void *rock);

static int report_card_query(struct transaction_t *txn,
                             struct meth_params *rparams,
                             xmlNodePtr inroot, struct propfind_ctx *fctx);

static struct mime_type_t carddav_mime_types[] = {
    /* First item MUST be the default type and storage format */
    { "text/vcard; charset=utf-8", "3.0", "vcf",
      (struct buf* (*)(void *)) &vcard_as_buf,
      (void * (*)(const struct buf*)) &vcard_parse_buf,
      (void (*)(void *)) &vparse_free_card, NULL, NULL
    },
    { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }
};

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

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

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

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

    /* CardDAV (RFC 6352) REPORTs */
    { "addressbook-query", NS_CARDDAV, "multistatus", &report_card_query,
      DACL_READ, REPORT_NEED_MBOX | REPORT_ALLOW_PROPS },
    { "addressbook-multiget", NS_CARDDAV, "multistatus", &report_multiget,
      DACL_READ, REPORT_NEED_MBOX | REPORT_ALLOW_PROPS },

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

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

    /* WebDAV (RFC 4918) properties */
    { "creationdate", NS_DAV,
      PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
      propfind_creationdate, NULL, NULL },
    { "displayname", NS_DAV,
      PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE | PROP_PERUSER,
      propfind_collectionname, proppatch_todb, NULL },
    { "getcontentlanguage", NS_DAV,
      PROP_ALLPROP | PROP_RESOURCE,
      propfind_fromhdr, NULL, "Content-Language" },
    { "getcontentlength", NS_DAV,
      PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
      propfind_getlength, NULL, NULL },
    { "getcontenttype", NS_DAV,
      PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
      propfind_getcontenttype, NULL, "Content-Type" },
    { "getetag", NS_DAV,
      PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
      propfind_getetag, NULL, NULL },
    { "getlastmodified", NS_DAV,
      PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE,
      propfind_getlastmod, NULL, NULL },
    { "lockdiscovery", NS_DAV,
      PROP_ALLPROP | PROP_RESOURCE,
      propfind_lockdisc, NULL, NULL },
    { "resourcetype", NS_DAV,
      PROP_ALLPROP | PROP_COLLECTION | PROP_RESOURCE | PROP_PRESCREEN,
      propfind_restype, proppatch_restype, "addressbook" },
    { "supportedlock", NS_DAV,
      PROP_ALLPROP | PROP_RESOURCE,
      propfind_suplock, NULL, NULL },

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

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

    /* WebDAV Quota (RFC 4331) properties */
    { "quota-available-bytes", NS_DAV,
      PROP_COLLECTION,
      propfind_quota, NULL, NULL },
    { "quota-used-bytes", NS_DAV,
      PROP_COLLECTION,
      propfind_quota, NULL, NULL },

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

    /* WebDAV POST (RFC 5995) properties */
    { "add-member", NS_DAV,
      PROP_COLLECTION,
      propfind_addmember, NULL, NULL },

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

    /* WebDAV Sharing (draft-pot-webdav-resource-sharing) properties */
    { "share-access", NS_DAV,
      PROP_COLLECTION,
      propfind_shareaccess, NULL, NULL },
    { "invite", NS_DAV,
      PROP_COLLECTION,
      propfind_invite, NULL, NULL },
    { "sharer-resource-uri", NS_DAV,
      PROP_COLLECTION,
      propfind_sharedurl, NULL, NULL },

    /* CardDAV (RFC 6352) properties */
    { "address-data", NS_CARDDAV,
      PROP_RESOURCE | PROP_PRESCREEN | PROP_CLEANUP,
      propfind_addrdata, NULL, (void *) CARDDAV_SUPP_DATA },
    { "addressbook-description", NS_CARDDAV,
      PROP_COLLECTION | PROP_PERUSER,
      propfind_fromdb, proppatch_todb, NULL },
    { "supported-address-data", NS_CARDDAV,
      PROP_COLLECTION,
      propfind_suppaddrdata, NULL, NULL },
    { "supported-collation-set", NS_CARDDAV,
      PROP_COLLECTION,
      propfind_collationset, NULL, NULL },
    { "max-resource-size", NS_CARDDAV,
      PROP_COLLECTION,
      NULL, NULL, NULL },

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

    /* Apple Push Notifications Service properties */
    { "push-transports", NS_CS,
      PROP_COLLECTION | PROP_PRESCREEN,
      propfind_push_transports, NULL, (void *) MBTYPE_ADDRESSBOOK },
    { "pushkey", NS_CS,
      PROP_COLLECTION,
      propfind_pushkey, NULL, NULL },

    /* Cyrus properties */
    { "address-groups", NS_CYRUS,
      PROP_RESOURCE,
      propfind_addrgroups, NULL, NULL },

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

static struct meth_params carddav_params = {
    carddav_mime_types,
    &carddav_parse_path,
    &dav_get_validators,
    &dav_get_modseq,
    &dav_check_precond,
    { (db_open_proc_t) &carddav_open_mailbox,
      (db_close_proc_t) &carddav_close,
      (db_proc_t) &carddav_begin,
      (db_proc_t) &carddav_commit,
      (db_proc_t) &carddav_abort,
      (db_lookup_proc_t) &carddav_lookup_resource,
      (db_imapuid_proc_t) &carddav_lookup_imapuid,
      (db_foreach_proc_t) &carddav_foreach,
      (db_updates_proc_t) &carddav_get_updates,
      (db_write_proc_t) &carddav_write,
      (db_delete_proc_t) &carddav_delete },
    NULL,                                       /* No ACL extensions */
    { CARDDAV_UID_CONFLICT, &carddav_copy },
    NULL,                                       /* No special DELETE handling */
    &carddav_get,
    { CARDDAV_LOCATION_OK, MBTYPE_ADDRESSBOOK, NULL },
    NULL,                                       /* No PATCH handling */
    { POST_ADDMEMBER | POST_SHARE, NULL,        /* No special POST handling */
      { NS_CARDDAV, "addressbook-data", &carddav_import } },
    { CARDDAV_SUPP_DATA, &carddav_put },
    { DAV_FINITE_DEPTH, carddav_props },        /* Disable infinite depth */
    carddav_reports
};


/* Namespace for Carddav collections */
struct namespace_t namespace_addressbook = {
    URL_NS_ADDRESSBOOK, 0, "addressbook", "/dav/addressbooks", "/.well-known/carddav",
    http_allow_noauth_get, /*authschemes*/0,
    MBTYPE_ADDRESSBOOK,
    (ALLOW_READ | ALLOW_POST | ALLOW_WRITE | ALLOW_DELETE |
     ALLOW_DAV | ALLOW_PROPPATCH | ALLOW_MKCOL | ALLOW_ACL | ALLOW_CARD),
    &my_carddav_init, &my_carddav_auth, my_carddav_reset, &my_carddav_shutdown,
    &dav_premethod, /*bearer*/NULL,
    {
        { &meth_acl,            &carddav_params },      /* ACL          */
        { NULL,                 NULL },                 /* BIND         */
        { NULL,                 NULL },                 /* CONNECT      */
        { &meth_copy_move,      &carddav_params },      /* COPY         */
        { &meth_delete,         &carddav_params },      /* DELETE       */
        { &meth_get_head,       &carddav_params },      /* GET          */
        { &meth_get_head,       &carddav_params },      /* HEAD         */
        { &meth_lock,           &carddav_params },      /* LOCK         */
        { NULL,                 NULL },                 /* MKCALENDAR   */
        { &meth_mkcol,          &carddav_params },      /* MKCOL        */
        { &meth_copy_move,      &carddav_params },      /* MOVE         */
        { &meth_options,        &carddav_parse_path },  /* OPTIONS      */
        { NULL,                 NULL },                 /* PATCH        */
        { &meth_post,           &carddav_params },      /* POST         */
        { &meth_propfind,       &carddav_params },      /* PROPFIND     */
        { &meth_proppatch,      &carddav_params },      /* PROPPATCH    */
        { &meth_put,            &carddav_params },      /* PUT          */
        { &meth_report,         &carddav_params },      /* REPORT       */
        { &meth_trace,          &carddav_parse_path },  /* TRACE        */
        { NULL,                 NULL },                 /* UNBIND       */
        { &meth_unlock,         &carddav_params }       /* UNLOCK       */
    }
};

static void my_carddav_init(struct buf *serverinfo __attribute__((unused)))
{
    namespace_addressbook.enabled =
        config_httpmodules & IMAP_ENUM_HTTPMODULES_CARDDAV;

    if (!namespace_addressbook.enabled) return;

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

    carddav_init();

    namespace_principal.enabled = 1;
    /* Apple clients check principal resources for these DAV tokens */
    namespace_principal.allow |= ALLOW_CARD;

    compile_time = calc_compile_time(__TIME__, __DATE__);
}


#define DEFAULT_ADDRBOOK "Default"

static int _create_mailbox(const char *userid, const char *mailboxname, int type,
                           const char *displayname, struct mboxlock **namespacelockp)
{
    int r = 0;
    struct mailbox *mailbox = NULL;

    r = mboxlist_lookup(mailboxname, NULL, NULL);
    if (!r) return 0;
    if (r != IMAP_MAILBOX_NONEXISTENT) return r;

    if (!*namespacelockp) {
        *namespacelockp = mboxname_usernamespacelock(mailboxname);
        // maybe we lost the race on this one
        r = mboxlist_lookup(mailboxname, NULL, NULL);
        if (!r) return 0;
        if (r != IMAP_MAILBOX_NONEXISTENT) return r;
    }

    /* Create locally */
    r = mboxlist_createmailbox(mailboxname, type,
                               NULL, 0,
                               userid, httpd_authstate,
                               0, 0, 0, 0, displayname ? &mailbox : NULL);

    if (!r && displayname) {
        annotate_state_t *astate = NULL;

        r = mailbox_get_annotate_state(mailbox, 0, &astate);
        if (!r) {
            const char *disp_annot = DAV_ANNOT_NS "<" XML_NS_DAV ">displayname";
            struct buf value = BUF_INITIALIZER;

            buf_init_ro_cstr(&value, displayname);
            r = annotate_state_writemask(astate, disp_annot, userid, &value);
            buf_free(&value);
        }

        mailbox_close(&mailbox);
    }

    if (r) syslog(LOG_ERR, "IOERROR: failed to create %s (%s)",
                  mailboxname, error_message(r));

    return r;
}



EXPORTED int carddav_create_defaultaddressbook(const char *userid) {
    struct mboxlock *namespacelock = NULL;

    /* addressbook-home-set */
    mbname_t *mbname = mbname_from_userid(userid);
    mbname_push_boxes(mbname, config_getstring(IMAPOPT_ADDRESSBOOKPREFIX));
    int r = mboxlist_lookup(mbname_intname(mbname), NULL, NULL);
    if (r == IMAP_MAILBOX_NONEXISTENT) {
        /* Find location of INBOX */
        char *inboxname = mboxname_user_mbox(userid, NULL);
        mbentry_t *mbentry = NULL;

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

        if (!r) r = _create_mailbox(userid, mbname_intname(mbname),
                                    MBTYPE_ADDRESSBOOK, NULL,
                                    &namespacelock);
    }
    if (r) goto done;

    /* Default addressbook */
    mbname_push_boxes(mbname, DEFAULT_ADDRBOOK);
    r = mboxlist_lookup(mbname_intname(mbname), NULL, NULL);
    if (r == IMAP_MAILBOX_NONEXISTENT) {
        r = _create_mailbox(userid, mbname_intname(mbname),
                            MBTYPE_ADDRESSBOOK, "personal",
                            &namespacelock);
    }

 done:
    mboxname_release(&namespacelock);
    mbname_free(&mbname);
    return r;
}

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

    /* Auto-provision an addressbook for 'userid' */
    int r = carddav_create_defaultaddressbook(userid);
    if (r) {
        syslog(LOG_ERR, "could not autoprovision addressbook for userid %s: %s",
                userid, error_message(r));
        return HTTP_SERVER_ERROR;
    }
    return 0;
}


static void my_carddav_reset(void)
{
    if (auth_carddavdb) carddav_close(auth_carddavdb);
    auth_carddavdb = NULL;
}


static void my_carddav_shutdown(void)
{
    my_carddav_reset();
    carddav_done();
}


/* Parse request-target path in CardDAV namespace */
static int carddav_parse_path(const char *path, struct request_target_t *tgt,
                              const char **resultstr)
{
    return calcarddav_parse_path(path, tgt,
                                 config_getstring(IMAPOPT_ADDRESSBOOKPREFIX),
                                 resultstr);
}

/* Perform a COPY/MOVE/PUT request
 *
 * preconditions:
 *   CARDDAV:valid-address-data
 *   CARDDAV:no-uid-conflict (DAV:href)
 *   CARDDAV:max-resource-size
 */
static int store_resource(struct transaction_t *txn,
                          struct vparse_card *vcard,
                          struct mailbox *mailbox, const char *resource,
                          struct carddav_db *davdb, int dupcheck)
{
    struct vparse_entry *ventry;
    struct carddav_data *cdata;
    const char *version = NULL, *uid = NULL, *fullname = NULL;
    struct index_record *oldrecord = NULL, record;
    char *mimehdr;

    /* Validate the vCard data */
    if (!vcard ||
        !vcard->objects ||
        !vcard->objects->type ||
        strcasecmp(vcard->objects->type, "vcard")) {
        txn->error.precond = CARDDAV_VALID_DATA;
        return HTTP_FORBIDDEN;
    }

    /* Fetch some important properties */
    for (ventry = vcard->objects->properties; ventry; ventry = ventry->next) {
        const char *name = ventry->name;
        const char *propval = ventry->v.value;

        if (!name) continue;
        if (!propval) continue;

        if (!strcasecmp(name, "version")) {
            version = propval;
            if (strcmp(version, "3.0")) {
                txn->error.precond = CARDDAV_SUPP_DATA;
                return HTTP_FORBIDDEN;
            }
        }

        else if (!strcasecmp(name, "uid"))
            uid = propval;

        else if (!strcasecmp(name, "fn"))
            fullname = propval;
    }

    /* Sanity check data */
    if (!version || !uid || !fullname) {
        txn->error.precond = CARDDAV_VALID_DATA;
        return HTTP_FORBIDDEN;
    }

    /* Check for changed UID on existing resource */
    carddav_lookup_resource(davdb, mailbox->name, resource, &cdata, 0);
    if (cdata->dav.imap_uid && strcmpsafe(cdata->vcard_uid, uid)) {
        char *owner = mboxname_to_userid(cdata->dav.mailbox);

        txn->error.precond = CARDDAV_UID_CONFLICT;
        assert(!buf_len(&txn->buf));
        buf_printf(&txn->buf, "%s/%s/%s/%s/%s",
                   namespace_addressbook.prefix,
                   USER_COLLECTION_PREFIX, owner,
                   strrchr(cdata->dav.mailbox, '.')+1,
                   cdata->dav.resource);
        txn->error.resource = buf_cstring(&txn->buf);
        free(owner);
        return HTTP_FORBIDDEN;
    }

    if (dupcheck) {
        /* Check for different resource with same UID */
        carddav_lookup_uid(davdb, uid, &cdata);
        if (cdata->dav.imap_uid && (strcmp(cdata->dav.mailbox, mailbox->name) ||
                                    strcmp(cdata->dav.resource, resource))) {
            /* CARDDAV:no-uid-conflict */
            char *owner = mboxname_to_userid(cdata->dav.mailbox);

            txn->error.precond = CARDDAV_UID_CONFLICT;
            assert(!buf_len(&txn->buf));
            buf_printf(&txn->buf, "%s/%s/%s/%s/%s",
                       namespace_addressbook.prefix,
                       USER_COLLECTION_PREFIX, owner,
                       strrchr(cdata->dav.mailbox, '.')+1,
                       cdata->dav.resource);
            txn->error.resource = buf_cstring(&txn->buf);
            free(owner);
            return HTTP_FORBIDDEN;
        }
    }

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

    /* Create and cache RFC 5322 header fields for resource */
    mimehdr = charset_encode_mimeheader(fullname, 0, 0);
    spool_replace_header(xstrdup("Subject"), mimehdr, txn->req_hdrs);

    /* XXX - validate uid for mime safety? */
    if (strchr(uid, '@')) {
        spool_replace_header(xstrdup("Message-ID"),
                             xstrdup(uid), txn->req_hdrs);
    }
    else {
        assert(!buf_len(&txn->buf));
        buf_printf(&txn->buf, "<%s@%s>", uid, config_servername);
        spool_replace_header(xstrdup("Message-ID"),
                             buf_release(&txn->buf), txn->req_hdrs);
    }

    assert(!buf_len(&txn->buf));
    buf_printf(&txn->buf, "text/vcard; version=%s; charset=utf-8", version);
    spool_replace_header(xstrdup("Content-Type"),
                         buf_release(&txn->buf), txn->req_hdrs);

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

    spool_remove_header(xstrdup("Content-Description"), txn->req_hdrs);

    /* Store the resource */
    struct buf *buf = vcard_as_buf(vcard);
    int r = dav_store_resource(txn, buf_cstring(buf), 0,
                              mailbox, oldrecord, cdata->dav.createdmodseq, NULL);
    buf_destroy(buf);
    return r;
}

static int carddav_copy(struct transaction_t *txn, void *obj,
                        struct mailbox *mailbox, const char *resource,
                        void *destdb, unsigned flags __attribute__((unused)))
{
    struct carddav_db *db = (struct carddav_db *)destdb;
    struct vparse_card *vcard = (struct vparse_card *)obj;
    return store_resource(txn, vcard, mailbox, resource, db, /*dupcheck*/0);
}


static int export_addressbook(struct transaction_t *txn)
{
    int ret = 0, r, precond;
    struct resp_body_t *resp_body = &txn->resp_body;
    struct buf *buf = &resp_body->payload;
    struct mailbox *mailbox = NULL;
    static char etag[33];
    static const char *displayname_annot =
        DAV_ANNOT_NS "<" XML_NS_DAV ">displayname";
    struct buf attrib = BUF_INITIALIZER;
    const char **hdr, *sep = "";
    struct mime_type_t *mime = NULL;

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

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

    /* Check any preconditions */
    sprintf(etag, "%u-%u-%u",
            mailbox->i.uidvalidity, mailbox->i.last_uid, mailbox->i.exists);
    precond = check_precond(txn, etag, mailbox->index_mtime);

    switch (precond) {
    case HTTP_OK:
    case HTTP_NOT_MODIFIED:
        /* Fill in ETag, Last-Modified, Expires, and Cache-Control */
        txn->resp_body.etag = etag;
        txn->resp_body.lastmod = mailbox->index_mtime;
        txn->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) break;

        GCC_FALLTHROUGH

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

    /* Setup for chunked response */
    txn->flags.te |= TE_CHUNKED;
    txn->flags.vary |= VARY_ACCEPT;
    txn->resp_body.type = mime->content_type;

    /* Set filename of resource */
    r = annotatemore_lookupmask(mailbox->name, displayname_annot,
                                httpd_userid, &attrib);
    /* fall back to last part of mailbox name */
    if (r || !attrib.len) buf_setcstr(&attrib, strrchr(mailbox->name, '.') + 1);

    buf_reset(&txn->buf);
    buf_printf(&txn->buf, "%s.%s", buf_cstring(&attrib), mime->file_ext);
    txn->resp_body.dispo.fname = buf_cstring(&txn->buf);

    /* Short-circuit for HEAD request */
    if (txn->meth == METH_HEAD) {
        response_header(HTTP_OK, txn);
        ret = 0;
        goto done;
    }

    /* vCard data in response should not be transformed */
    txn->flags.cc |= CC_NOTRANSFORM;

    /* Begin (converted) vCard stream */
    if (mime->begin_stream)
        sep = mime->begin_stream(buf, mailbox, NULL, NULL, NULL, NULL);
    else buf_reset(buf);
    write_body(HTTP_OK, txn, buf_cstring(buf), buf_len(buf));

    struct mailbox_iter *iter =
        mailbox_iter_init(mailbox, 0, ITER_SKIP_EXPUNGED|ITER_SKIP_DELETED);

    const message_t *msg;
    while ((msg = mailbox_iter_step(iter))) {
        const struct index_record *record = msg_record(msg);
        struct vparse_card *vcard;

        /* Map and parse existing vCard resource */
        vcard = record_to_vcard(mailbox, record);

        if (vcard) {
            if (r++ && *sep) {
                /* Add separator, if necessary */
                buf_reset(buf);
                buf_printf_markup(buf, 0, "%s", sep);
                write_body(0, txn, buf_cstring(buf), buf_len(buf));
            }

            struct buf *card_str = mime->from_object(vcard);
            write_body(0, txn, buf_base(card_str), buf_len(card_str));
            buf_destroy(card_str);

            vparse_free_card(vcard);
        }
    }

    mailbox_iter_done(&iter);

    /* End (converted) vCard stream */
    if (mime->end_stream) {
        mime->end_stream(buf);
        write_body(0, txn, buf_cstring(buf), buf_len(buf));
    }

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

  done:
    buf_free(&attrib);
    mailbox_close(&mailbox);

    return ret;
}


/*
 * mboxlist_findall() callback function to list addressbooks
 */

struct addr_info {
    char shortname[MAX_MAILBOX_NAME];
    char displayname[MAX_MAILBOX_NAME];
    unsigned flags;
};

enum {
    ADDR_IS_DEFAULT =    (1<<0),
    ADDR_CAN_DELETE =    (1<<1),
    ADDR_CAN_ADMIN =     (1<<2),
    ADDR_IS_PUBLIC =     (1<<3)
};

struct list_addr_rock {
    struct addr_info *addr;
    unsigned len;
    unsigned alloc;
};

static int list_addr_cb(const mbentry_t *mbentry, void *rock)
{
    struct list_addr_rock *lrock = (struct list_addr_rock *) rock;
    struct addr_info *addr;
    static size_t defaultlen = 0;
    char *shortname;
    size_t len;
    int r, rights, any_rights = 0;
    static const char *displayname_annot =
        DAV_ANNOT_NS "<" XML_NS_DAV ">displayname";
    struct buf displayname = BUF_INITIALIZER;

    if (!defaultlen) defaultlen = strlen(DEFAULT_ADDRBOOK);

    /* Make sure its a addrendar */
    if (mbentry->mbtype != MBTYPE_ADDRESSBOOK) goto done;

    /* Make sure its readable */
    rights = httpd_myrights(httpd_authstate, mbentry);
    if ((rights & DACL_READ) != DACL_READ) goto done;

    shortname = strrchr(mbentry->name, '.') + 1;
    len = strlen(shortname);

    /* Lookup DAV:displayname */
    r = annotatemore_lookupmask(mbentry->name, displayname_annot,
                                httpd_userid, &displayname);
    /* fall back to the last part of the mailbox name */
    if (r || !displayname.len) buf_setcstr(&displayname, shortname);

    /* Make sure we have room in our array */
    if (lrock->len == lrock->alloc) {
        lrock->alloc += 100;
        lrock->addr = xrealloc(lrock->addr,
                              lrock->alloc * sizeof(struct addr_info));
    }

    /* Add our addressbook to the array */
    addr = &lrock->addr[lrock->len];
    strlcpy(addr->shortname, shortname, MAX_MAILBOX_NAME);
    strlcpy(addr->displayname, buf_cstring(&displayname), MAX_MAILBOX_NAME);
    addr->flags = 0;

    /* Is this the default addressbook? */
    if (len == defaultlen && !strncmp(shortname, SCHED_DEFAULT, defaultlen)) {
        addr->flags |= ADDR_IS_DEFAULT;
    }

    /* Can we delete this addrendar? */
    else if (rights & DACL_RMCOL) {
        addr->flags |= ADDR_CAN_DELETE;
    }

    /* Can we admin this addressbook? */
    if (rights & DACL_ADMIN) {
        addr->flags |= ADDR_CAN_ADMIN;
    }

    /* Is this addressbook public? */
    if (mbentry->acl) {
        struct auth_state *auth_anyone = auth_newstate("anyone");

        any_rights = cyrus_acl_myrights(auth_anyone, mbentry->acl);
        auth_freestate(auth_anyone);
    }
    if ((any_rights & DACL_READ) == DACL_READ) {
        addr->flags |= ADDR_IS_PUBLIC;
    }

    lrock->len++;

done:
    buf_free(&displayname);

    return 0;
}

static int addr_compare(const void *a, const void *b)
{
    struct addr_info *c1 = (struct addr_info *) a;
    struct addr_info *c2 = (struct addr_info *) b;

    return strcmp(c1->displayname, c2->displayname);
}


/* Create a HTML document listing all addressbooks available to the user */
static int list_addressbooks(struct transaction_t *txn)
{
    int ret = 0, precond, rights;
    char mboxlist[MAX_MAILBOX_PATH+1];
    struct stat sbuf;
    time_t lastmod;
    const char *etag, *base_path = txn->req_tgt.path;
    unsigned level = 0, i;
    struct buf *body = &txn->resp_body.payload;
    struct list_addr_rock lrock;
    const char *proto = NULL;
    const char *host = NULL;
#include "imap/http_carddav_js.h"

    /* stat() mailboxes.db for Last-Modified and ETag */
    snprintf(mboxlist, MAX_MAILBOX_PATH, "%s%s", config_dir, FNAME_MBOXLIST);
    stat(mboxlist, &sbuf);
    lastmod = MAX(compile_time, sbuf.st_mtime);
    assert(!buf_len(&txn->buf));
    buf_printf(&txn->buf, "%ld-%ld-%ld",
               compile_time, sbuf.st_mtime, sbuf.st_size);

    /* stat() config file for Last-Modified and ETag */
    stat(config_filename, &sbuf);
    lastmod = MAX(lastmod, sbuf.st_mtime);
    buf_printf(&txn->buf, "-%ld-%ld", sbuf.st_mtime, sbuf.st_size);
    etag = buf_cstring(&txn->buf);

    /* Check any preconditions */
    precond = check_precond(txn, etag, lastmod);

    switch (precond) {
    case HTTP_OK:
    case HTTP_NOT_MODIFIED:
        /* Fill in ETag, Last-Modified, and Expires */
        txn->resp_body.etag = etag;
        txn->resp_body.lastmod = lastmod;
        txn->flags.cc |= CC_REVALIDATE;

        if (precond != HTTP_NOT_MODIFIED) break;

        GCC_FALLTHROUGH

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

    /* Setup for chunked response */
    txn->flags.te |= TE_CHUNKED;
    txn->resp_body.type = "text/html; charset=utf-8";

    /* Short-circuit for HEAD request */
    if (txn->meth == METH_HEAD) {
        response_header(HTTP_OK, txn);
        goto done;
    }

    /* Send HTML header */
    buf_reset(body);
    buf_printf_markup(body, level, HTML_DOCTYPE);
    buf_printf_markup(body, level++, "<html>");
    buf_printf_markup(body, level++, "<head>");
    buf_printf_markup(body, level, "<title>%s</title>", "Available Addressbooks");
    buf_printf_markup(body, level++, "<script type=\"text/javascript\">");
    buf_appendcstr(body, "//<![CDATA[\n");
    buf_printf(body, (const char *) http_carddav_js,
               CYRUS_VERSION, http_carddav_js_len);
    buf_appendcstr(body, "//]]>\n");
    buf_printf_markup(body, --level, "</script>");
    buf_printf_markup(body, level++, "<noscript>");
    buf_printf_markup(body, level, "<i>*** %s ***</i>",
                      "JavaScript required to create/modify/delete addressbooks");
    buf_printf_markup(body, --level, "</noscript>");
    buf_printf_markup(body, --level, "</head>");
    buf_printf_markup(body, level++, "<body>");

    write_body(HTTP_OK, txn, buf_cstring(body), buf_len(body));
    buf_reset(body);

    /* Check ACL for current user */
    rights = httpd_myrights(httpd_authstate, txn->req_tgt.mbentry);

    if (rights & DACL_MKCOL) {
        /* Add "create" form */
        buf_printf_markup(body, level, "<h2>%s</h2>", "Create New Addressbook");
        buf_printf_markup(body, level++, "<form name='create'>");
        buf_printf_markup(body, level++, "<table cellpadding=5>");
        buf_printf_markup(body, level++, "<tr>");
        buf_printf_markup(body, level, "<td align=right>Name:</td>");
        buf_printf_markup(body, level,
                          "<td><input name=name size=30 maxlength=40></td>");
        buf_printf_markup(body, --level, "</tr>");

        buf_printf_markup(body, level++, "<tr>");
        buf_printf_markup(body, level, "<td align=right>Description:</td>");
        buf_printf_markup(body, level,
                          "<td><input name=desc size=75 maxlength=120></td>");
        buf_printf_markup(body, --level, "</tr>");

        buf_printf_markup(body, level++, "<tr>");
        buf_printf_markup(body, level, "<td></td>");
        buf_printf_markup(body, level,
                          "<td><br><input type=button value='Create'"
                          " onclick=\"createAddressbook('%s')\">"
                          " <input type=reset></td>",
                          base_path);
        buf_printf_markup(body, --level, "</tr>");

        buf_printf_markup(body, --level, "</table>");
        buf_printf_markup(body, --level, "</form>");

        buf_printf_markup(body, level, "<br><hr><br>");

        write_body(0, txn, buf_cstring(body), buf_len(body));
        buf_reset(body);
    }

    buf_printf_markup(body, level, "<h2>%s</h2>", "Available Addressbooks");
    buf_printf_markup(body, level++, "<table border cellpadding=5>");

    /* Create base URL for addressbooks */
    http_proto_host(txn->req_hdrs, &proto, &host);
    buf_reset(&txn->buf);
    buf_printf(&txn->buf, "%s://%s%s", proto, host, txn->req_tgt.path);

    memset(&lrock, 0, sizeof(struct list_addr_rock));
    mboxlist_mboxtree(txn->req_tgt.mbentry->name,
                      list_addr_cb, &lrock, MBOXTREE_SKIP_ROOT);

    /* Sort addressbooks by displayname */
    qsort(lrock.addr, lrock.len, sizeof(struct addr_info), &addr_compare);

    /* Add available addressbooks with action items */
    for (i = 0; i < lrock.len; i++) {
        struct addr_info *addr = &lrock.addr[i];

        /* Send a body chunk once in a while */
        if (buf_len(body) > PROT_BUFSIZE) {
            write_body(0, txn, buf_cstring(body), buf_len(body));
            buf_reset(body);
        }

        /* Addressbook name */
        buf_printf_markup(body, level++, "<tr>");
        buf_printf_markup(body, level, "<td>%s%s%s",
                          (addr->flags & ADDR_IS_DEFAULT) ? "<b>" : "",
                          addr->displayname,
                          (addr->flags & ADDR_IS_DEFAULT) ? "</b>" : "");

        /* Download link */
        buf_printf_markup(body, level, "<td><a href=\"%s%s\">Download</a></td>",
                          base_path, addr->shortname);

        /* Delete button */
        buf_printf_markup(body, level,
                          "<td><input type=button%s value='Delete'"
                          " onclick=\"deleteAddressbook('%s%s', '%s')\"></td>",
                          !(addr->flags & ADDR_CAN_DELETE) ? " disabled" : "",
                          base_path, addr->shortname, addr->displayname);

        /* Public (shared) checkbox */
        buf_printf_markup(body, level,
                          "<td><input type=checkbox%s%s name=share"
                          " onclick=\"shareAddressbook('%s%s', this.checked)\">"
                          "Public</td>",
                          !(addr->flags & ADDR_CAN_ADMIN) ? " disabled" : "",
                          (addr->flags & ADDR_IS_PUBLIC) ? " checked" : "",
                          base_path, addr->shortname);

        buf_printf_markup(body, --level, "</tr>");
    }

    free(lrock.addr);

    /* Finish list */
    buf_printf_markup(body, --level, "</table>");

    /* Finish HTML */
    buf_printf_markup(body, --level, "</body>");
    buf_printf_markup(body, --level, "</html>");
    write_body(0, txn, buf_cstring(body), buf_len(body));

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

  done:
    return ret;
}


/* Perform a GET/HEAD request on a CardDAV resource */
static int carddav_get(struct transaction_t *txn,
                       struct mailbox *mailbox __attribute__((unused)),
                       struct index_record *record,
                       void *data __attribute__((unused)),
                       void **obj __attribute__((unused)))
{
    int rights;

    if (!(txn->req_tgt.collection || txn->req_tgt.userid))
        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 (record && record->uid) {
        /* GET on a resource */
        return HTTP_CONTINUE;
    }

    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.collection) {
        /* Download an entire addressbook collection */
        return export_addressbook(txn);
    }
    else if (txn->req_tgt.userid &&
             config_getswitch(IMAPOPT_CARDDAV_ALLOWADDRESSBOOKADMIN)) {
        /* GET a list of addressbook under addressbook-home-set */
        return list_addressbooks(txn);
    }

    /* Unknown action */
    return HTTP_NO_CONTENT;
}


static int carddav_put(struct transaction_t *txn, void *obj,
                       struct mailbox *mailbox, const char *resource,
                       void *destdb, unsigned flags __attribute__((unused)))
{
    struct carddav_db *db = (struct carddav_db *)destdb;
    struct vparse_card *vcard = (struct vparse_card *)obj;

    if (!(vcard && vcard->objects &&
          vparse_restriction_check(vcard->objects))) {
        txn->error.precond = CARDDAV_VALID_DATA;
        return HTTP_FORBIDDEN;
    }

    return store_resource(txn, vcard, mailbox, resource, db, /*dupcheck*/1);
}


/* Perform a bulk import */
static int carddav_import(struct transaction_t *txn, void *obj,
                          struct mailbox *mailbox, void *destdb,
                          xmlNodePtr root, xmlNsPtr *ns, unsigned flags)
{
    struct vparse_card *vcard = obj;
    xmlBufferPtr xmlbuf = NULL;
    size_t baselen;

    if (!root) {
        /* Validate the vCard data */
        if (!vcard ||
            !vcard->objects ||
            !vcard->objects->type ||
            strcasecmp(vcard->objects->type, "vcard")) {
            txn->error.precond = CARDDAV_VALID_DATA;
            return HTTP_FORBIDDEN;
        }

        return 0;
    }


    /* Setup for appending resource name to request path */
    baselen = strlen(txn->req_tgt.path);
    txn->req_tgt.resource = txn->req_tgt.path + baselen;

    /* Import vCards */
    while (vcard->objects) {
        struct vparse_card *this, *next;
        xmlNodePtr resp, node;
        struct vparse_entry *entry;
        const char *resource = makeuuid(), *uid, *myuid = NULL;
        int r;

        /* Create DAV:response element */
        resp = xmlNewChild(root, ns[NS_DAV], BAD_CAST "response", NULL);
        if (!resp) {
            syslog(LOG_ERR,
                   "import_resource()): Unable to add response XML element");
            fatal("import_resource()): Unable to add response XML element",
                  EX_SOFTWARE);
        }

        /* Isolate this card */
        this = vcard->objects;
        next = this->next;
        this->next = NULL;

        /* Get/create UID property */
        entry = vparse_get_entry(this, NULL, "UID");
        if (entry) {
            uid = entry->v.value;
        }
        else {
            myuid = uid = resource;
            vparse_add_entry(this, NULL, "UID", uid);
        }

        /* Append a unique resource name to URL and perform a PUT */
        txn->req_tgt.reslen =
            snprintf(txn->req_tgt.resource, MAX_MAILBOX_PATH - baselen,
                     "%s.vcf", resource);

        r = carddav_put(txn, vcard, mailbox,
                       txn->req_tgt.resource, destdb, flags);

        switch (r) {
        case HTTP_OK:
        case HTTP_CREATED:
        case HTTP_NO_CONTENT:
            /* Success: Add DAV:href and DAV:propstat elements */
            xml_add_href(resp, NULL, txn->req_tgt.path);

            node = xmlNewChild(resp, ns[NS_DAV], BAD_CAST "propstat", NULL);
            xmlNewChild(node, ns[NS_DAV], BAD_CAST "status",
                        BAD_CAST http_statusline(VER_1_1, HTTP_OK));

            node = xmlNewChild(node, ns[NS_DAV], BAD_CAST "prop", NULL);

            if (txn->resp_body.etag) {
                /* Add DAV:getetag property */
                xmlNewTextChild(node, ns[NS_DAV], BAD_CAST "getetag",
                                BAD_CAST txn->resp_body.etag);
            }

            if ((flags & PREFER_REP) && myuid /* we added a UID */) {
                /* Add CARDDAV:addressbook-data property */
                struct buf *vcardbuf = vcard_as_buf(this);
                xmlNodePtr cdata = xmlNewChild(node, ns[NS_CARDDAV],
                                               BAD_CAST "addressbook-data", NULL);

                xmlAddChild(cdata, xmlNewCDataBlock(root->doc,
                                                    BAD_CAST buf_cstring(vcardbuf),
                                                    buf_len(vcardbuf)));
                buf_free(vcardbuf);
            }

            break;

        default:
            /* Failure: Add DAV:href, DAV:status, and DAV:error elements */
            xml_add_href(resp, NULL, NULL);

            xmlNewChild(resp, ns[NS_DAV], BAD_CAST "status",
                        BAD_CAST http_statusline(VER_1_1, r));

            node = xml_add_error(resp, &txn->error, ns);
            break;
        }

        /* Add CS:uid property */
        xmlNewTextChild(node, ns[NS_CS], BAD_CAST "uid", BAD_CAST uid);

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

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

        /* Remove this vcard from the head of the list */
        vparse_free_card(this);
        vcard->objects = next;

        /* Clear the buffer used for constructing href */
        buf_reset(&txn->buf);
    }

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

    return 0;
}


/* Callback to fetch DAV:getcontenttype */
static int propfind_getcontenttype(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_setcstr(&fctx->buf, "text/vcard; charset=utf-8");

    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:resourcetype */
static int propfind_restype(const xmlChar *name, xmlNsPtr ns,
                            struct propfind_ctx *fctx,
                            xmlNodePtr prop __attribute__((unused)),
                            xmlNodePtr resp,
                            struct propstat propstat[],
                            void *rock __attribute__((unused)))
{
    if (!propstat) {
        /* Prescreen "property" request */
        if (fctx->req_tgt->collection ||
            (fctx->req_tgt->userid && fctx->depth >= 1) || fctx->depth >= 2) {
            /* Add namespaces for possible resource types */
            ensure_ns(fctx->ns, NS_CARDDAV, fctx->root, XML_NS_CARDDAV, "C");
        }

        return 0;
    }

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

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

        if (fctx->req_tgt->collection) {
            ensure_ns(fctx->ns, NS_CARDDAV,
                      resp ? resp->parent: node, XML_NS_CARDDAV, "C");
            xmlNewChild(node, fctx->ns[NS_CARDDAV],
                        BAD_CAST "addressbook", NULL);
        }
    }

    return 0;
}


static void prune_properties(struct vparse_card *vcard, strarray_t *partial)
{
    struct vparse_entry **entryp = &vcard->properties;

    while (*entryp) {
        struct vparse_entry *entry = *entryp;

        if (strarray_find_case(partial, entry->name, 0) < 0) {
            *entryp = entry->next;
            entry->next = NULL; /* so free doesn't walk the chain */
            vparse_free_entry(entry);
        }
        else {
            entryp = &((*entryp)->next);
        }
    }
}

/* Callback to prescreen/fetch CARDDAV:address-data */
static int propfind_addrdata(const xmlChar *name, xmlNsPtr ns,
                             struct propfind_ctx *fctx,
                             xmlNodePtr prop,
                             xmlNodePtr resp __attribute__((unused)),
                             struct propstat propstat[],
                             void *rock __attribute__((unused)))
{
    static struct mime_type_t *out_type = carddav_mime_types;
    static strarray_t partial_addrdata = STRARRAY_INITIALIZER;
    strarray_t *partial = &partial_addrdata;
    const char *data = NULL;
    size_t datalen = 0;

    if (!fctx) {
        /* Cleanup "property" request - free partial property array */
        strarray_fini(partial);

        return 0;
    }

    if (!propstat) {
        /* Prescreen "property" request - read partial properties */
        xmlNodePtr node;

        /* Initialize partial property array to be empty */
        strarray_init(partial);

        /* Check for and parse child elements of CARDDAV:address-data */
        for (node = xmlFirstElementChild(prop); node;
             node = xmlNextElementSibling(node)) {

            if (!xmlStrcmp(node->name, BAD_CAST "prop")) {
                xmlChar *name = xmlGetProp(node, BAD_CAST "name");
                if (name) {
                    strarray_add_case(partial, (const char *) name);
                    xmlFree(name);
                }
            }
        }
    }
    else {
        if (fctx->txn->meth != METH_REPORT) return HTTP_FORBIDDEN;

        if (!out_type->content_type) return HTTP_BAD_MEDIATYPE;

        if (!fctx->record) return HTTP_NOT_FOUND;

        if (!fctx->msg_buf.len)
            mailbox_map_record(fctx->mailbox, fctx->record, &fctx->msg_buf);
        if (!fctx->msg_buf.len) return HTTP_SERVER_ERROR;

        data = fctx->msg_buf.s + fctx->record->header_size;
        datalen = fctx->record->size - fctx->record->header_size;

        if (strarray_size(partial)) {
            /* Limit returned properties */
            struct vparse_card *vcard = fctx->obj;

            if (!vcard) vcard = fctx->obj = vcard_parse_string(data, 1);
            prune_properties(vcard->objects, partial);

            /* Create vCard data from new vcard component */
            buf_reset(&fctx->msg_buf);
            vparse_tobuf(vcard, &fctx->msg_buf);
            data = buf_cstring(&fctx->msg_buf);
            datalen = buf_len(&fctx->msg_buf);
        }
    }

    return propfind_getdata(name, ns, fctx, prop, propstat, carddav_mime_types,
                            &out_type, data, datalen);
}


/* Callback to fetch CARDDAV:addressbook-home-set */
int propfind_abookhome(const xmlChar *name, xmlNsPtr ns,
                       struct propfind_ctx *fctx,
                       xmlNodePtr prop,
                       xmlNodePtr resp __attribute__((unused)),
                       struct propstat propstat[],
                       void *rock __attribute__((unused)))
{
    xmlNodePtr node;
    // XXX - should we be using httpd_userid here?
    const char *userid = fctx->req_tgt->userid;

    if (!(namespace_addressbook.enabled && userid))
        return HTTP_NOT_FOUND;

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

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

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

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

    return 0;
}


/* Callback to fetch CARDDAV:supported-address-data */
static int propfind_suppaddrdata(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;
    struct mime_type_t *mime;

    if (!fctx->req_tgt->collection) return HTTP_NOT_FOUND;

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

    for (mime = carddav_mime_types; mime->content_type; mime++) {
        xmlNodePtr type = xmlNewChild(node, fctx->ns[NS_CARDDAV],
                                      BAD_CAST "address-data-type", NULL);

        /* Trim any charset from content-type */
        buf_reset(&fctx->buf);
        buf_printf(&fctx->buf, "%.*s",
                   (int) strcspn(mime->content_type, ";"), mime->content_type);

        xmlNewProp(type, BAD_CAST "content-type",
                   BAD_CAST buf_cstring(&fctx->buf));

        if (mime->version)
            xmlNewProp(type, BAD_CAST "version", BAD_CAST mime->version);
    }

    buf_reset(&fctx->buf);

    return 0;
}

/* Callback to fetch CY:address-groups */
int propfind_addrgroups(const xmlChar *name, xmlNsPtr ns,
                        struct propfind_ctx *fctx,
                        xmlNodePtr prop __attribute__((unused)),
                        xmlNodePtr resp __attribute__((unused)),
                        struct propstat propstat[],
                        void *rock __attribute__((unused)))
{
    int r = 0;
    struct carddav_db *davdb = NULL;
    struct carddav_data *cdata = NULL;
    strarray_t *groups;
    xmlNodePtr node;
    int i;

    if (!fctx->req_tgt->collection) return HTTP_NOT_FOUND;

    /* If we're here via report_sync_col then we don't have a db handle yet, so
     * lets just manage this ourselves */
    davdb = carddav_open_mailbox(fctx->mailbox);
    if (davdb == NULL) {
        r = HTTP_SERVER_ERROR;
        goto done;
    }

    r = carddav_lookup_resource(davdb, fctx->req_tgt->mbentry->name,
                                fctx->req_tgt->resource, &cdata, 0);
    if (r)
        goto done;

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

    groups = carddav_getuid_groups(davdb, cdata->vcard_uid);
    if (groups == NULL)
        goto done;

    for (i = 0; i < strarray_size(groups); i++) {
        const char *group_uid = strarray_nth(groups, i);

        xmlNodePtr group = xmlNewChild(node, fctx->ns[NS_CYRUS],
                                       BAD_CAST "address-group", NULL);
        xmlAddChild(group,
                    xmlNewCDataBlock(fctx->root->doc,
                                     BAD_CAST group_uid, strlen(group_uid)));
    }

    strarray_free(groups);

done:
    carddav_close(davdb);
    return r;
}


typedef enum vcardproperty_kind {
    VCARD_ANY_PROPERTY = 0,
    VCARD_FN_PROPERTY = 1,
    VCARD_N_PROPERTY = 2,
    VCARD_NICKNAME_PROPERTY = 3,
    VCARD_UID_PROPERTY = 4,
    VCARD_NO_PROPERTY = 1000
} vcardproperty_kind;

struct cardquery_filter {
    unsigned allof : 1;
    struct prop_filter *prop;
};

static unsigned vcardproperty_string_to_kind(const char *str)
{
    if (!strcasecmp(str, "FN")) return VCARD_FN_PROPERTY;
    else if (!strcasecmp(str, "N")) return VCARD_N_PROPERTY;
    else if (!strcasecmp(str, "NICKNAME")) return VCARD_NICKNAME_PROPERTY;
    else if (!strcasecmp(str, "UID")) return VCARD_UID_PROPERTY;
    else return VCARD_ANY_PROPERTY;
}

static int parse_cardfilter(xmlNodePtr root, struct cardquery_filter *filter,
                            struct error_t *error)
{
    xmlChar *attr;
    xmlNodePtr node;
    struct filter_profile_t profile =
        { 0 /* anyof */, COLLATION_UNICODE,
          CARDDAV_SUPP_FILTER, CARDDAV_SUPP_COLLATION,
          vcardproperty_string_to_kind, VCARD_NO_PROPERTY,
          NULL /* param_string_to_kind */, 0 /* no_param_value */,
          NULL /* parse_propfilter */ };

    /* Parse elements of filter */
    attr = xmlGetProp(root, BAD_CAST "test");
    if (attr) {
        if (!xmlStrcmp(attr, BAD_CAST "allof")) filter->allof = 1;
        else if (xmlStrcmp(attr, BAD_CAST "anyof")) {
            error->precond = CARDDAV_SUPP_FILTER;
            error->desc = "Unsupported test";
            error->node = xmlCopyNode(root, 2);
        }
        xmlFree(attr);
    }

    for (node = xmlFirstElementChild(root); node && !error->precond;
         node = xmlNextElementSibling(node)) {

        if (!xmlStrcmp(node->name, BAD_CAST "prop-filter")) {
            struct prop_filter *prop = NULL;

            dav_parse_propfilter(node, &prop, &profile, error);
            if (prop) {
                if (filter->prop) prop->next = filter->prop;
                filter->prop = prop;
            }
        }
        else {
            error->precond = CARDDAV_SUPP_FILTER;
            error->desc = "Unsupported element in filter";
            error->node = xmlCopyNode(root, 1);
        }
    }

    return error->precond ? HTTP_FORBIDDEN : 0;
}


static int apply_paramfilter(struct param_filter *paramfilter,
                             struct vparse_entry *prop)
{
    struct vparse_param *param =
        vparse_get_param(prop, (char *) paramfilter->name);

    if (!param) return paramfilter->not_defined;
    if (paramfilter->not_defined) return 0;
    if (!paramfilter->match) return 1;

    return dav_apply_textmatch(BAD_CAST param->value, paramfilter->match);
}

static int apply_propfilter(struct prop_filter *propfilter,
                            struct carddav_data *cdata,
                            struct propfind_ctx *fctx)
{
    int pass = 1;
    struct vparse_card *vcard = fctx->obj;
    struct vparse_entry myprop, *prop = NULL;

    memset(&myprop, 0, sizeof(struct vparse_entry));

    if (!propfilter->param) {
        switch (propfilter->kind) {
        case VCARD_FN_PROPERTY:
            if (cdata->fullname) myprop.v.value = (char *) cdata->fullname;
            break;

        case VCARD_N_PROPERTY:
            if (cdata->name) myprop.v.value = (char *) cdata->name;
            break;

        case VCARD_NICKNAME_PROPERTY:
            if (cdata->nickname) {
                if (propfilter->match) {
                    myprop.multivaluesep = ',';
                    myprop.v.values =
                        strarray_split(cdata->nickname, ",", STRARRAY_TRIM);
                }
                else myprop.v.value = (char *) cdata->nickname;
            }
            break;

        case VCARD_UID_PROPERTY:
            if (cdata->vcard_uid) myprop.v.value = (char *) cdata->vcard_uid;
            break;

        default:
            break;
        }

        if (myprop.v.value) prop = &myprop;
    }

    if (propfilter->param || (propfilter->kind == VCARD_ANY_PROPERTY)) {
        /* Load message containing the resource and parse vcard data */
        if (!vcard) {
            if (!fctx->msg_buf.len) {
                mailbox_map_record(fctx->mailbox, fctx->record, &fctx->msg_buf);
            }
            if (fctx->msg_buf.len) {
                vcard = fctx->obj =
                    vcard_parse_string(buf_cstring(&fctx->msg_buf) +
                                       fctx->record->header_size, 1);
            }
            if (!vcard) return 0;
        }

        prop = vparse_get_entry(vcard->objects, NULL, (char *) propfilter->name);
    }

    if (!prop) return propfilter->not_defined;
    if (propfilter->not_defined) return 0;
    if (!(propfilter->match || propfilter->param)) return 1;

    /* Test each instance of this property (logical OR) */
    do {
        struct text_match_t *match;
        struct param_filter *paramfilter;

        if (!pass && strcasecmpsafe(prop->name, (char *) propfilter->name)) {
            /* Skip property if name doesn't match */
            continue;
        }
    
        pass = propfilter->allof;

        /* Apply each text-match, breaking if allof fails or anyof succeeds */
        for (match = propfilter->match;
             match && (pass == propfilter->allof);
             match = match->next) {

            int n = 0;
            const char *text = prop->multivaluesep ?
                strarray_nth(prop->v.values, n) : prop->v.value;

            /* Test each value of this property (logical OR) */
            do {
                pass = dav_apply_textmatch(BAD_CAST text, match);

            } while (!pass && prop->multivaluesep &&
                     (text = strarray_nth(prop->v.values, ++n)));
        }

        /* Apply each param-filter, breaking if allof fails or anyof succeeds */
        for (paramfilter = propfilter->param;
             paramfilter && (pass == propfilter->allof);
             paramfilter = paramfilter->next) {

            pass = apply_paramfilter(paramfilter, prop);
        }

    } while (!pass && (prop = prop->next));  /* XXX  No API to fetch next prop */

    if (myprop.multivaluesep) strarray_free(myprop.v.values);

    return pass;
}

/* See if the current resource matches the specified filter.
 * Returns 1 if match, 0 otherwise.
 */
static int apply_cardfilter(struct propfind_ctx *fctx, void *data)
{
    struct cardquery_filter *cardfilter =
        (struct cardquery_filter *) fctx->filter_crit;
    struct carddav_data *cdata = (struct carddav_data *) data;
    struct prop_filter *propfilter;
    int pass = 1;

    for (propfilter = cardfilter->prop; propfilter;
         propfilter = propfilter->next) {

        pass = apply_propfilter(propfilter, cdata, fctx);
        /* If allof fails or anyof succeeds, we're done */
        if (pass != cardfilter->allof) break;
    }

    return pass;
}

static void free_cardfilter(struct cardquery_filter *cardfilter)
{
    struct prop_filter *prop, *next;

    for (prop = cardfilter->prop; prop; prop = next) {
        next = prop->next;

        dav_free_propfilter(prop);
    }
}

static int report_card_query(struct transaction_t *txn,
                             struct meth_params *rparams __attribute__((unused)),
                             xmlNodePtr inroot, struct propfind_ctx *fctx)
{
    int ret = 0;
    xmlNodePtr node;
    struct cardquery_filter cardfilter;

    memset(&cardfilter, 0, sizeof(struct cardquery_filter));

    fctx->filter_crit = &cardfilter;
    fctx->open_db = (db_open_proc_t) &carddav_open_mailbox;
    fctx->close_db = (db_close_proc_t) &carddav_close;
    fctx->lookup_resource = (db_lookup_proc_t) &carddav_lookup_resource;
    fctx->foreach_resource = (db_foreach_proc_t) &carddav_foreach;
    fctx->proc_by_resource = &propfind_by_resource;
    fctx->davdb = NULL;

    /* Parse children element of report */
    for (node = inroot->children; node; node = node->next) {
        if (node->type == XML_ELEMENT_NODE) {
            if (!xmlStrcmp(node->name, BAD_CAST "filter")) {
                ret = parse_cardfilter(node, &cardfilter, &txn->error);
                if (ret) goto done;
                else fctx->filter = apply_cardfilter;
            }
        }
    }

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

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

    if (fctx->depth++ > 0) {
        /* Addressbook collection(s) */
        if (txn->req_tgt.collection) {
            /* Add response for target addressbook collection */
            propfind_by_collection(txn->req_tgt.mbentry, fctx);
        }
        else {
            /* Add responses for all contained addressbook collections */
            mboxlist_mboxtree(txn->req_tgt.mbentry->name,
                              propfind_by_collection, fctx,
                              MBOXTREE_SKIP_ROOT);

            /* Add responses for all shared addressbook collections */
            mboxlist_usersubs(txn->req_tgt.userid,
                              propfind_by_collection, fctx,
                              MBOXTREE_SKIP_PERSONAL);
        }
    }

    /* 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:
    /* Free filter structure */
    free_cardfilter(&cardfilter);

    if (fctx->davdb) {
        fctx->close_db(fctx->davdb);
        fctx->davdb = NULL;
    }

    return ret;
}
