static void cmdloop(void)
{
    int gzip_enabled = 0;
    struct transaction_t txn;

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

    /* Pre-allocate our working buffer */
    buf_ensure(&txn.buf, 1024);

#ifdef HAVE_ZLIB
    /* Always use gzip format because IE incorrectly uses raw deflate */
    if (config_getswitch(IMAPOPT_HTTPALLOWCOMPRESS) &&
	deflateInit2(&txn.zstrm, Z_DEFAULT_COMPRESSION, Z_DEFLATED,
		     16+MAX_WBITS, MAX_MEM_LEVEL, Z_DEFAULT_STRATEGY) == Z_OK) {
	gzip_enabled = 1;
    }
#endif

    for (;;) {
	int ret, empty, r, i, c;
	char *p;
	tok_t tok;
	const char **hdr, *query;
	const struct namespace_t *namespace;
	const struct method_t *meth_t;
	struct request_line_t *req_line = &txn.req_line;

	/* Reset txn state */
	txn.meth = METH_UNKNOWN;
	memset(&txn.flags, 0, sizeof(struct txn_flags_t));
	txn.flags.conn = 0;
	txn.flags.vary = VARY_AE;
	memset(req_line, 0, sizeof(struct request_line_t));
	memset(&txn.req_tgt, 0, sizeof(struct request_target_t));
	construct_hash_table(&txn.req_qparams, 10, 1);
	txn.req_uri = NULL;
	txn.auth_chal.param = NULL;
	txn.req_hdrs = NULL;
	txn.req_body.flags = 0;
	buf_reset(&txn.req_body.payload);
	txn.location = NULL;
	memset(&txn.error, 0, sizeof(struct error_t));
	memset(&txn.resp_body, 0,  /* Don't zero the response payload buffer */
	       sizeof(struct resp_body_t) - sizeof(struct buf));
	buf_reset(&txn.resp_body.payload);
	buf_reset(&txn.buf);
	ret = empty = 0;

	/* Create header cache */
	if (!(txn.req_hdrs = spool_new_hdrcache())) {
	    txn.error.desc = "Unable to create header cache";
	    ret = HTTP_SERVER_ERROR;
	}

      req_line:
	do {
	    /* Flush any buffered output */
	    prot_flush(httpd_out);
	    if (backend_current) prot_flush(backend_current->out);

	    /* Check for shutdown file */
	    if (shutdown_file(txn.buf.s, txn.buf.alloc) ||
		(httpd_userid &&
		 userdeny(httpd_userid, config_ident, txn.buf.s, txn.buf.alloc))) {
		txn.error.desc = txn.buf.s;
		ret = HTTP_UNAVAILABLE;
		break;
	    }

	    signals_poll();

	} while (!proxy_check_input(protin, httpd_in, httpd_out,
				    backend_current ? backend_current->in : NULL,
				    NULL, 0));
	if (ret) {
	    txn.flags.conn = CONN_CLOSE;
	    error_response(ret, &txn);
	    protgroup_free(protin);
	    shut_down(0);
	}


	/* Read request-line */
	syslog(LOG_DEBUG, "read & parse request-line");
	if (!prot_fgets(req_line->buf, MAX_REQ_LINE+1, httpd_in)) {
	    txn.error.desc = prot_error(httpd_in);
	    if (txn.error.desc && strcmp(txn.error.desc, PROT_EOF_STRING)) {
		/* client timed out */
		syslog(LOG_WARNING, "%s, closing connection", txn.error.desc);
		ret = HTTP_TIMEOUT;
	    }
	    else {
		/* client closed connection */
	    }

	    txn.flags.conn = CONN_CLOSE;
	    goto done;
	}

	/* Trim CRLF from request-line */
	p = req_line->buf + strlen(req_line->buf);
	if (p[-1] == '\n') *--p = '\0';
	if (p[-1] == '\r') *--p = '\0';

	/* Ignore 1 empty line before request-line per HTTPbis Part 1 Sec 3.5 */
	if (!empty++ && !*req_line->buf) goto req_line;

	/* Parse request-line = method SP request-target SP HTTP-version CRLF */
	tok_initm(&tok, req_line->buf, " ", 0);
	if (!(req_line->meth = tok_next(&tok))) {
	    ret = HTTP_BAD_REQUEST;
	    txn.error.desc = "Missing method in request-line";
	}
	else if (!(req_line->uri = tok_next(&tok))) {
	    ret = HTTP_BAD_REQUEST;
	    txn.error.desc = "Missing request-target in request-line";
	}
	else if ((size_t) (p - req_line->buf) > MAX_REQ_LINE - 2) {
	    /* request-line overran the size of our buffer */
	    ret = HTTP_TOO_LONG;
	    buf_printf(&txn.buf,
		       "Length of request-line MUST be less than %u octets",
		       MAX_REQ_LINE);
	    txn.error.desc = buf_cstring(&txn.buf);
	}
	else if (!(req_line->ver = tok_next(&tok))) {
	    ret = HTTP_BAD_REQUEST;
	    txn.error.desc = "Missing HTTP-version in request-line";
	}
	else if (tok_next(&tok)) {
	    ret = HTTP_BAD_REQUEST;
	    txn.error.desc = "Unexpected extra argument(s) in request-line";
	}

	/* Check HTTP-Version - MUST be HTTP/1.x */
	else if (strlen(req_line->ver) != HTTP_VERSION_LEN
		 || strncmp(req_line->ver, HTTP_VERSION, HTTP_VERSION_LEN-1)
		 || !isdigit(req_line->ver[HTTP_VERSION_LEN-1])) {
	    ret = HTTP_BAD_VERSION;
	    buf_printf(&txn.buf,
		     "This server only speaks %.*sx",
		       HTTP_VERSION_LEN-1, HTTP_VERSION);
	    txn.error.desc = buf_cstring(&txn.buf);
	}
	else if (req_line->ver[HTTP_VERSION_LEN-1] == '0') {
	    /* HTTP/1.0 connection */
	    txn.flags.ver1_0 = 1;
	}
	tok_fini(&tok);

	if (ret) {
	    txn.flags.conn = CONN_CLOSE;
	    goto done;
	}

	/* Read and parse headers */
	syslog(LOG_DEBUG, "read & parse headers");
	if ((r = spool_fill_hdrcache(httpd_in, NULL, txn.req_hdrs, NULL))) {
	    ret = HTTP_BAD_REQUEST;
	    txn.error.desc = error_message(r);
	}
	else if ((txn.error.desc = prot_error(httpd_in)) &&
		 strcmp(txn.error.desc, PROT_EOF_STRING)) {
	    /* client timed out */
	    syslog(LOG_WARNING, "%s, closing connection", txn.error.desc);
	    ret = HTTP_TIMEOUT;
	}

	/* Read CRLF separating headers and body */
	else if ((c = prot_getc(httpd_in)) != '\r' ||
		 (c = prot_getc(httpd_in)) != '\n') {
	    ret = HTTP_BAD_REQUEST;
	    txn.error.desc = error_message(IMAP_MESSAGE_NOBLANKLINE);
	}

	if (ret) {
	    txn.flags.conn = CONN_CLOSE;
	    goto done;
	}

	/* Check for Connection options */
	parse_connection(&txn);
	if (txn.flags.conn & CONN_UPGRADE) {
	    starttls(0);
	    txn.flags.conn &= ~CONN_UPGRADE;
	}

	/* Check for HTTP method override */
	if (!strcmp(req_line->meth, "POST") &&
	    (hdr = spool_getheader(txn.req_hdrs, "X-HTTP-Method-Override"))) {
	    txn.flags.override = 1;
	    req_line->meth = (char *) hdr[0];
	}

	/* Check Method against our list of known methods */
	for (txn.meth = 0; (txn.meth < METH_UNKNOWN) &&
		 strcmp(http_methods[txn.meth].name, req_line->meth);
	     txn.meth++);

	if (txn.meth == METH_UNKNOWN) ret = HTTP_NOT_IMPLEMENTED;

	/* Parse request-target URI */
	else if (!(txn.req_uri = parse_uri(txn.meth, req_line->uri, 1,
					   &txn.error.desc))) {
	    ret = HTTP_BAD_REQUEST;
	}

	/* Check message framing */
	else if ((r = http_parse_framing(txn.req_hdrs, &txn.req_body,
					 &txn.error.desc))) {
	    ret = r;
	}

	/* Check for Expectations */
	else if ((r = parse_expect(&txn))) {
	    ret = r;
	}

	/* Check for mandatory Host header (HTTP/1.1+ only) */
	else if ((hdr = spool_getheader(txn.req_hdrs, "Host")) && hdr[1]) {
	    ret = HTTP_BAD_REQUEST;
	    txn.error.desc = "Too many Host headers";
	}
	else if (!hdr) {
	    if (txn.flags.ver1_0) {
		/* HTTP/1.0 - create a Host header from URI */
		if (txn.req_uri->server) {
		    buf_setcstr(&txn.buf, txn.req_uri->server);
		    if (txn.req_uri->port)
			buf_printf(&txn.buf, ":%d", txn.req_uri->port);
		}
		else buf_setcstr(&txn.buf, config_servername);

		spool_cache_header(xstrdup("Host"),
				   xstrdup(buf_cstring(&txn.buf)),
				   txn.req_hdrs);
		buf_reset(&txn.buf);
	    }
	    else {
		ret = HTTP_BAD_REQUEST;
		txn.error.desc = "Missing Host header";
	    }
	}

	if (ret) goto done;

	query = URI_QUERY(txn.req_uri);

	/* Find the namespace of the requested resource */
	for (i = 0; namespaces[i]; i++) {
	    const char *path = txn.req_uri->path;
	    size_t len;

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

	    /* Handle any /.well-known/ bootstrapping */
	    if (namespaces[i]->well_known) {
		len = strlen(namespaces[i]->well_known);
		if (!strncmp(path, namespaces[i]->well_known, len) &&
		    (!path[len] || path[len] == '/')) {

		    hdr = spool_getheader(txn.req_hdrs, "Host");
		    buf_reset(&txn.buf);
		    buf_printf(&txn.buf, "%s://%s",
			       https? "https" : "http", hdr[0]);
		    buf_appendcstr(&txn.buf, namespaces[i]->prefix);
		    buf_appendcstr(&txn.buf, path + len);
		    if (query) buf_printf(&txn.buf, "?%s", query);
		    txn.location = buf_cstring(&txn.buf);

		    ret = HTTP_MOVED;
		    goto done;
		}
	    }

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

	    /* Check if method is supported in this namespace */
	    meth_t = &namespace->methods[txn.meth];
	    if (!meth_t->proc) ret = HTTP_NOT_ALLOWED;

	    /* Check if method expects a body */
	    else if ((http_methods[txn.meth].flags & METH_NOBODY) &&
		     (txn.req_body.framing != FRAMING_LENGTH ||
		      /* XXX  Will break if client sends just a last-chunk */
		      txn.req_body.len)) {
		ret = HTTP_BAD_MEDIATYPE;
	    }
	} else {
	    /* XXX  Should never get here */
	    ret = HTTP_SERVER_ERROR;
	}

	if (ret) goto done;

	/* Perform authentication, if necessary */
	if ((hdr = spool_getheader(txn.req_hdrs, "Authorization"))) {
	    if (httpd_userid) {
		/* Reauth - reinitialize */
		syslog(LOG_DEBUG, "reauth - reinit");
		reset_saslconn(&httpd_saslconn);
		txn.auth_chal.scheme = NULL;
	    }

            if (httpd_tls_required) {
                /* TLS required - redirect handled below */
                ret = HTTP_UNAUTHORIZED;
            }
            else {
		/* Check the auth credentials */
		r = http_auth(hdr[0], &txn);
		if ((r < 0) || !txn.auth_chal.scheme) {
		    /* Auth failed - reinitialize */
		    syslog(LOG_DEBUG, "auth failed - reinit");
		    reset_saslconn(&httpd_saslconn);
		    txn.auth_chal.scheme = NULL;
		    ret = HTTP_UNAUTHORIZED;
		}
	    }
	}
	else if (!httpd_userid && txn.auth_chal.scheme) {
	    /* Started auth exchange, but client didn't engage - reinit */
	    syslog(LOG_DEBUG, "client didn't complete auth - reinit");
	    reset_saslconn(&httpd_saslconn);
	    txn.auth_chal.scheme = NULL;
	}

	/* Perform proxy authorization, if necessary */
	else if (saslprops.authid &&
		 (hdr = spool_getheader(txn.req_hdrs, "Authorize-As")) &&
		 *hdr[0]) {
	    const char *authzid = hdr[0];

	    r = proxy_authz(&authzid, &txn);
	    if (r) {
		/* Proxy authz failed - reinitialize */
		syslog(LOG_DEBUG, "proxy authz failed - reinit");
		reset_saslconn(&httpd_saslconn);
		txn.auth_chal.scheme = NULL;
		ret = HTTP_UNAUTHORIZED;
	    }
	    else {
		httpd_userid = xstrdup(authzid);
		auth_success(&txn);
	    }
	}

	/* Request authentication, if necessary */
	switch (txn.meth) {
	case METH_GET:
	case METH_HEAD:
	case METH_OPTIONS:
	    /* Let method processing function decide if auth is needed */
	    break;

	default:
	    if (!httpd_userid && namespace->need_auth) {
		/* Authentication required */
		ret = HTTP_UNAUTHORIZED;
	    }
	}

	if (ret) goto need_auth;

	/* Check if this is a Cross-Origin Resource Sharing request */
	if (allow_cors && (hdr = spool_getheader(txn.req_hdrs, "Origin"))) {
	    const char *err = NULL;
	    xmlURIPtr uri = parse_uri(METH_UNKNOWN, hdr[0], 0, &err);

	    if (uri && uri->scheme && uri->server) {
		int o_https = !strcasecmp(uri->scheme, "https");

		if ((https == o_https) &&
		    !strcasecmp(uri->server,
				*spool_getheader(txn.req_hdrs, "Host"))) {
		    txn.flags.cors = CORS_SIMPLE;
		}
		else {
		    struct wildmat *wild;

		    /* Create URI w/o path or default port */
		    assert(!buf_len(&txn.buf));
		    buf_printf(&txn.buf, "%s://%s",
			       lcase(uri->scheme), lcase(uri->server));
		    if (uri->port &&
			((o_https && uri->port != 443) ||
			 (!o_https && uri->port != 80))) {
			buf_printf(&txn.buf, ":%d", uri->port);
		    }

		    /* Check Origin against the 'httpallowcors' wildmat */
		    for (wild = allow_cors; wild->pat; wild++) {
			if (wildmat(buf_cstring(&txn.buf), wild->pat)) {
			    /* If we have a non-negative match, allow request */
			    if (!wild->not) txn.flags.cors = CORS_SIMPLE;
			    break;
			}
		    }
		    buf_reset(&txn.buf);
		}
	    }
	    xmlFreeURI(uri);
	}

	/* Check if we should compress response body */
	if (gzip_enabled) {
	    /* XXX  Do we want to support deflate even though M$
	       doesn't implement it correctly (raw deflate vs. zlib)? */

	    if (!txn.flags.ver1_0 &&
		(hdr = spool_getheader(txn.req_hdrs, "TE"))) {
		struct accept *e, *enc = parse_accept(hdr);

		for (e = enc; e && e->token; e++) {
		    if (e->qual > 0.0 &&
			(!strcasecmp(e->token, "gzip") ||
			 !strcasecmp(e->token, "x-gzip"))) {
			txn.flags.te = TE_GZIP;
		    }
		    free(e->token);
		}
		if (enc) free(enc);
	    }
	    else if ((hdr = spool_getheader(txn.req_hdrs, "Accept-Encoding"))) {
		struct accept *e, *enc = parse_accept(hdr);

		for (e = enc; e && e->token; e++) {
		    if (e->qual > 0.0 &&
			(!strcasecmp(e->token, "gzip") ||
			 !strcasecmp(e->token, "x-gzip"))) {
			txn.resp_body.enc = CE_GZIP;
		    }
		    free(e->token);
		}
		if (enc) free(enc);
	    }
	}

	/* Parse any query parameters */
	if (query) {
	    /* Parse the query string and add param/value pairs to hash table */
	    tok_t tok;
	    char *param;

	    assert(!buf_len(&txn.buf));  /* Unescape buffer */

	    tok_init(&tok, (char *) query, ";&=",
		     TOK_TRIMLEFT|TOK_TRIMRIGHT|TOK_EMPTY);
	    while ((param = tok_next(&tok))) {
		struct strlist *vals;
		char *value = tok_next(&tok);
		size_t len;

		if (!value) value = "";
		len = strlen(value);
		buf_ensure(&txn.buf, len);

		vals = hash_lookup(param, &txn.req_qparams);
		appendstrlist(&vals,
			      xmlURIUnescapeString(value, len, txn.buf.s));
		hash_insert(param, vals, &txn.req_qparams);
	    }
	    tok_fini(&tok);

	    buf_reset(&txn.buf);
	}

	/* Start method processing alarm (HTTP/1.1+ only) */
	if (!txn.flags.ver1_0) alarm(httpd_keepalive);

	/* Process the requested method */
	ret = (*meth_t->proc)(&txn, meth_t->params);

      need_auth:
	if (ret == HTTP_UNAUTHORIZED) {
	    /* User must authenticate */

	    if (httpd_tls_required) {
		/* We only support TLS+Basic, so tell client to use TLS */
		ret = 0;

		/* Check which response is required */
		if ((hdr = spool_getheader(txn.req_hdrs, "Upgrade")) &&
		    !strncmp(hdr[0], TLS_VERSION, strcspn(hdr[0], " ,"))) {
		    /* Client (Murder proxy) supports RFC 2817 (TLS upgrade) */

		    response_header(HTTP_UPGRADE, &txn);
		}
		else {
		    /* All other clients use RFC 2818 (HTTPS) */
		    const char *path = txn.req_uri->path;
		    struct buf *html = &txn.resp_body.payload;

		    /* Create https URL */
		    hdr = spool_getheader(txn.req_hdrs, "Host");
		    buf_printf(&txn.buf, "https://%s", hdr[0]);
		    if (strcmp(path, "*")) {
			buf_appendcstr(&txn.buf, path);
			if (query) buf_printf(&txn.buf, "?%s", query);
		    }

		    txn.location = buf_cstring(&txn.buf);

		    /* Create HTML body */
		    buf_reset(html);
		    buf_printf(html, tls_message,
			       buf_cstring(&txn.buf), buf_cstring(&txn.buf));

		    /* Output our HTML response */
		    txn.resp_body.type = "text/html; charset=utf-8";
		    write_body(HTTP_MOVED, &txn,
			       buf_cstring(html), buf_len(html));
		}
	    }
	    else {
		/* Tell client to authenticate */
		if (r == SASL_CONTINUE)
		    txn.error.desc = "Continue authentication exchange";
		else if (r) txn.error.desc = "Authentication failed";
		else txn.error.desc =
			 "Must authenticate to access the specified target";
	    }
	}

      done:
	/* Handle errors (success responses handled by method functions) */
	if (ret) error_response(ret, &txn);

	/* Read and discard any unread request body */
	if (!(txn.flags.conn & CONN_CLOSE)) {
	    txn.req_body.flags |= BODY_DISCARD;
	    if (http_read_body(httpd_in, httpd_out,
			       txn.req_hdrs, &txn.req_body, &txn.error.desc)) {
		txn.flags.conn = CONN_CLOSE;
	    }
	}

	/* Memory cleanup */
	if (txn.req_uri) xmlFreeURI(txn.req_uri);
	if (txn.req_hdrs) spool_free_hdrcache(txn.req_hdrs);
	free_hash_table(&txn.req_qparams, (void (*)(void *)) &freestrlist);

	if (txn.flags.conn & CONN_CLOSE) {
	    buf_free(&txn.buf);
	    buf_free(&txn.req_body.payload);
	    buf_free(&txn.resp_body.payload);
#ifdef HAVE_ZLIB
	    deflateEnd(&txn.zstrm);
	    buf_free(&txn.zbuf);
#endif
	    return;
	}

	continue;
    }
}