static int action_list(struct transaction_t *txn, int rights)
{
    int ret = 0, precond;
    char mboxlist[MAX_MAILBOX_PATH+1];
    struct stat sbuf;
    time_t lastmod;
    const char *etag;
    unsigned level = 0;
    struct buf *body = &txn->resp_body.payload;
    struct list_cal_rock lrock;

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

    /* 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 = caldav_check_precond(txn, NULL, 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;

    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 Calendars");
    buf_printf_markup(body, level++, "<script type=\"text/javascript\">");
    buf_printf_markup(body, level, "function httpGet(url)");
    buf_printf_markup(body, level++, "{");
    buf_printf_markup(body, level, "var req = new XMLHttpRequest();");
    buf_printf_markup(body, level, "req.open('GET', url, true);");
    buf_printf_markup(body, level, "req.send(null);");
    buf_printf_markup(body, --level, "}");
    buf_printf_markup(body, --level, "</script>");
    buf_printf_markup(body, --level, "</head>");
    buf_printf_markup(body, level++, "<body>");
    buf_printf_markup(body, level, "<h2>%s</h2>", "Available Calendars");
    buf_printf_markup(body, level++, "<table border cellpadding=5>");
    write_body(HTTP_OK, txn, buf_cstring(body), buf_len(body));
    buf_reset(body);

    /* Populate list callback rock */
    http_proto_host(txn->req_hdrs, &lrock.proto, &lrock.host);
    lrock.txn = txn;
    lrock.level = &level;

    /* Generate list of calendars */
    strlcat(txn->req_tgt.mboxname, ".%", sizeof(txn->req_tgt.mboxname));

    mboxlist_findall(NULL, txn->req_tgt.mboxname, 1, httpd_userid,
		     httpd_authstate, list_cal_cb, &lrock);

    if (buf_len(body)) write_body(0, txn, buf_cstring(body), buf_len(body));

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

    if (rights & DACL_MKCOL) {
	/* Add "create" form */
	const struct cal_comp_t *comp;
	struct list_tzid_rock tzrock = { body, &level };

	buf_printf_markup(body, level, "<p><hr>");
	buf_printf_markup(body, level, "<h3>%s</h3>", "Create New Calendar");
	buf_printf_markup(body, level++,
			  "<form method=GET action=\"%s\">",
			  txn->req_tgt.path);
	buf_printf_markup(body, level,
			  "<input type=hidden name=action value=create>");
	buf_printf_markup(body, level++, "<table cellpadding=5>");
	buf_printf_markup(body, level++, "<tr>");
	buf_printf_markup(body, level, "<td align=right>Name:");
	buf_printf_markup(body, level--,
			  "<td><input name=name size=30 maxlength=40>");
	buf_printf_markup(body, level++, "<tr>");
	buf_printf_markup(body, level, "<td align=right>Description:");
	buf_printf_markup(body, level--,
			  "<td><input name=desc size=75 maxlength=120>");

	buf_printf_markup(body, level++, "<tr>");
	buf_printf_markup(body, level, "<td align=right>Components:"
			  "<br><sub>(default = ALL)</sub>");
	buf_printf_markup(body, level++, "<td>");
	for (comp = cal_comps; comp->name; comp++) {
	    buf_printf_markup(body, level,
			      "<input type=checkbox name=comp value=%s>%s",
			      comp->name, comp->name);
	}
	level -= 2;

	if (namespace_calendar.allow & ALLOW_CAL_NOTZ) {
	    buf_printf_markup(body, level++, "<tr>");
	    buf_printf_markup(body, level, "<td align=right>Time Zone:");
	    buf_printf_markup(body, level++, "<td>");
	    buf_printf_markup(body, level++, "<select name=tzid>");
	    buf_printf_markup(body, level, "<option></option>");
	    zoneinfo_find(NULL, 1, 0, &list_tzid_cb, &tzrock);
	    buf_printf_markup(body, --level, "</select>");
	    level -= 2;
	}

	buf_printf_markup(body, level++, "<tr>");
	buf_printf_markup(body, level, "<td>");
	buf_printf_markup(body, level--,
			  "<td><input type=submit value=Create>");
	buf_printf_markup(body, --level, "</table>");
	buf_printf_markup(body, --level, "</form>");
    }

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