EXPORTED void response_header(long code, struct transaction_t *txn)
{
    time_t now;
    char datestr[30];
    unsigned keepalive;
    const char **hdr;
    struct auth_challenge_t *auth_chal;
    struct resp_body_t *resp_body;
    static struct buf log = BUF_INITIALIZER;

    /* Stop method processing alarm */
    keepalive = alarm(0);


    /* Status-Line */
    prot_printf(httpd_out, "%s\r\n", http_statusline(code));


    /* Connection Management */
    switch (code) {
    case HTTP_SWITCH_PROT:
	keepalive = 0;  /* No alarm during TLS negotiation */

	prot_printf(httpd_out, "Upgrade: %s\r\n", TLS_VERSION);
	prot_puts(httpd_out, "Connection: Upgrade\r\n");

	/* Fall through as provisional response */

    case HTTP_CONTINUE:
    case HTTP_PROCESSING:
	/* Provisional response - nothing else needed */

	/* CRLF terminating the header block */
	prot_puts(httpd_out, "\r\n");

	/* Force the response to the client immediately */
	prot_flush(httpd_out);

	/* Reset method processing alarm */
	alarm(keepalive);

	return;

    case HTTP_UPGRADE:
	txn->flags.conn |= CONN_UPGRADE;
	prot_printf(httpd_out, "Upgrade: %s\r\n", TLS_VERSION);

	/* Fall through as final response */

    default:
	/* Final response */
	if (txn->flags.conn) {
	    /* Construct Connection header */
	    const char *conn_tokens[] =
		{ "close", "Upgrade", "Keep-Alive", NULL };

	    if (txn->flags.conn & CONN_KEEPALIVE) {
		prot_printf(httpd_out, "Keep-Alive: timeout=%d\r\n",
			    httpd_timeout);
	    }

	    comma_list_hdr("Connection", conn_tokens, txn->flags.conn);
	}

	auth_chal = &txn->auth_chal;
	resp_body = &txn->resp_body;
    }


    /* Control Data */
    now = time(0);
    httpdate_gen(datestr, sizeof(datestr), now);
    prot_printf(httpd_out, "Date: %s\r\n", datestr);

    if (httpd_tls_done) {
	prot_puts(httpd_out, "Strict-Transport-Security: max-age=600\r\n");
    }
    if (txn->location) {
	prot_printf(httpd_out, "Location: %s\r\n", txn->location);
    }
    if (txn->flags.cc) {
	/* Construct Cache-Control header */
	const char *cc_dirs[] =
	    { "must-revalidate", "no-cache", "no-store", "no-transform",
	      "public", "private", "max-age=%d", NULL };

	comma_list_hdr("Cache-Control", cc_dirs, txn->flags.cc,
		       resp_body->maxage);

	if (txn->flags.cc & CC_MAXAGE) {
	    httpdate_gen(datestr, sizeof(datestr), now + resp_body->maxage);
	    prot_printf(httpd_out, "Expires: %s\r\n", datestr);
	}
    }
    if (txn->flags.cors) {
	/* Construct Cross-Origin Resource Sharing headers */
	prot_printf(httpd_out, "Access-Control-Allow-Origin: %s\r\n",
		    *spool_getheader(txn->req_hdrs, "Origin"));
	prot_puts(httpd_out, "Access-Control-Allow-Credentials: true\r\n");

	if (txn->flags.cors == CORS_PREFLIGHT) {
	    allow_hdr("Access-Control-Allow-Methods", txn->req_tgt.allow);

	    for (hdr = spool_getheader(txn->req_hdrs,
				       "Access-Control-Request-Headers");
		 hdr && *hdr; hdr++) {
		prot_printf(httpd_out,
			    "Access-Control-Allow-Headers: %s\r\n", *hdr);
	    }
	    prot_puts(httpd_out, "Access-Control-Max-Age: 3600\r\n");
	}
    }
    if (txn->flags.vary) {
	/* Construct Vary header */
	const char *vary_hdrs[] =
	    { "Accept", "Accept-Encoding", "Brief", "Prefer", NULL };

	comma_list_hdr("Vary", vary_hdrs, txn->flags.vary);
    }


    /* Response Context */
    if (txn->flags.mime) {
	prot_puts(httpd_out, "MIME-Version: 1.0\r\n");
    }
    if (txn->req_tgt.allow & ALLOW_ISCHEDULE) {
	prot_puts(httpd_out, "iSchedule-Version: 1.0\r\n");
	if (resp_body->iserial) {
	    prot_printf(httpd_out, "iSchedule-Capabilities: %ld\r\n",
			resp_body->iserial);
	}
    }
    if (resp_body->prefs) {
	/* Construct Preference-Applied header */
	const char *prefs[] =
	    { "return=minimal", "return=representation", "depth-noroot", NULL };

	comma_list_hdr("Preference-Applied", prefs, resp_body->prefs);
	if (txn->flags.cors) Access_Control_Expose("Preference-Applied");
    }

    switch (code) {
    case HTTP_OK:
	switch (txn->meth) {
	case METH_GET:
	case METH_HEAD:
	    /* Construct Accept-Ranges header for GET and HEAD responses */
	    prot_printf(httpd_out, "Accept-Ranges: %s\r\n",
			txn->flags.ranges ? "bytes" : "none");
	    break;

	case METH_OPTIONS:
	    if (config_serverinfo == IMAP_ENUM_SERVERINFO_ON) {
		prot_printf(httpd_out, "Server: %s\r\n",
			    buf_cstring(&serverinfo));
	    }

	    if (txn->req_tgt.allow & ALLOW_DAV) {
		/* Construct DAV header(s) based on namespace of request URL */
		prot_printf(httpd_out, "DAV: 1,%s 3, access-control%s\r\n",
			    (txn->req_tgt.allow & ALLOW_WRITE) ? " 2," : "",
			    (txn->req_tgt.allow & ALLOW_WRITECOL) ?
			    ", extended-mkcol" : "");
		if (txn->req_tgt.allow & ALLOW_CAL) {
		    prot_printf(httpd_out, "DAV: calendar-access%s%s%s\r\n",
				(txn->req_tgt.allow & ALLOW_CAL_AVAIL) ?
				", calendar-availability" : "",
				(txn->req_tgt.allow & ALLOW_CAL_SCHED) ?
				", calendar-auto-schedule" : "",
				(txn->req_tgt.allow & ALLOW_CAL_NOTZ) ?
				", calendar-no-timezone" : "");

		    /* Backwards compatibility with older Apple VAV clients */
		    if ((txn->req_tgt.allow &
			 (ALLOW_CAL_AVAIL | ALLOW_CAL_SCHED)) ==
			(ALLOW_CAL_AVAIL | ALLOW_CAL_SCHED)) {
			if ((hdr = spool_getheader(txn->req_hdrs, "User-Agent"))
			    && strstr(hdr[0], "CalendarAgent/")) {
			    prot_printf(httpd_out,
					"DAV: inbox-availability\r\n");
			}
		    }
		}
		if (txn->req_tgt.allow & ALLOW_CARD) {
		    prot_puts(httpd_out, "DAV: addressbook\r\n");
		}
	    }

	    if (txn->flags.cors == CORS_PREFLIGHT) {
		/* Access-Control-Allow-Methods supersedes Allow */
		break;
	    }
	    else goto allow;
	}
	goto authorized;

    case HTTP_NOT_ALLOWED:
    allow:
	/* Construct Allow header(s) for OPTIONS and 405 response */
	allow_hdr("Allow", txn->req_tgt.allow);
	goto authorized;

    case HTTP_BAD_MEDIATYPE:
	if (txn->req_body.te == TE_UNKNOWN) {
	    /* Construct Allow-Encoding header for 415 response */
#ifdef HAVE_ZLIB
	    prot_puts(httpd_out, "Allow-Encoding: gzip, deflate\r\n");
#else
	    prot_puts(httpd_out, "Allow-Encoding: identity\r\n");
#endif
	}
	goto authorized;

    case HTTP_UNAUTHORIZED:
	/* Authentication Challenges */
	if (!auth_chal->scheme) {
	    /* Require authentication by advertising all possible schemes */
	    struct auth_scheme_t *scheme;

	    for (scheme = auth_schemes; scheme->name; scheme++) {
		/* Only advertise what is available and
		   can work with the type of connection */
		if ((avail_auth_schemes & (1 << scheme->idx)) &&
		    !((txn->flags.conn & CONN_CLOSE) &&
		      (scheme->flags & AUTH_NEED_PERSIST))) {
		    auth_chal->param = NULL;

		    if (scheme->flags & AUTH_SERVER_FIRST) {
			/* Generate the initial challenge */
			http_auth(scheme->name, txn);

			if (!auth_chal->param) continue;  /* If fail, skip it */
		    }
		    WWW_Authenticate(scheme->name, auth_chal->param);
		}
	    }
	}
	else {
	    /* Continue with current authentication exchange */ 
	    WWW_Authenticate(auth_chal->scheme->name, auth_chal->param);
	}
	break;

    default:
    authorized:
	/* Authentication completed/unnecessary */
	if (auth_chal->param) {
	    /* Authentication completed with success data */
	    if (auth_chal->scheme->send_success) {
		/* Special handling of success data for this scheme */
		auth_chal->scheme->send_success(auth_chal->scheme->name,
						auth_chal->param);
	    }
	    else {
		/* Default handling of success data */
		WWW_Authenticate(auth_chal->scheme->name, auth_chal->param);
	    }
	}
    }


    /* Validators */
    if (resp_body->lock) {
	prot_printf(httpd_out, "Lock-Token: <%s>\r\n", resp_body->lock);
	if (txn->flags.cors) Access_Control_Expose("Lock-Token");
    }
    if (resp_body->stag) {
	prot_printf(httpd_out, "Schedule-Tag: \"%s\"\r\n", resp_body->stag);
	if (txn->flags.cors) Access_Control_Expose("Schedule-Tag");
    }
    if (resp_body->etag) {
	prot_printf(httpd_out, "ETag: %s\"%s\"\r\n",
		    resp_body->enc ? "W/" : "", resp_body->etag);
	if (txn->flags.cors) Access_Control_Expose("ETag");
    }
    if (resp_body->lastmod) {
	/* Last-Modified MUST NOT be in the future */
	resp_body->lastmod = MIN(resp_body->lastmod, now);
	httpdate_gen(datestr, sizeof(datestr), resp_body->lastmod);
	prot_printf(httpd_out, "Last-Modified: %s\r\n", datestr);
    }


    /* Representation Metadata */
    if (resp_body->type) {
	prot_printf(httpd_out, "Content-Type: %s\r\n", resp_body->type);

	if (resp_body->fname) {
	    prot_printf(httpd_out,
			"Content-Disposition: inline; filename=\"%s\"\r\n",
			resp_body->fname);
	}
	if (txn->resp_body.enc) {
	    /* Construct Content-Encoding header */
	    const char *ce[] =
		{ "deflate", "gzip", NULL };

	    comma_list_hdr("Content-Encoding", ce, txn->resp_body.enc);
	}
	if (resp_body->lang) {
	    prot_printf(httpd_out, "Content-Language: %s\r\n", resp_body->lang);
	}
	if (resp_body->loc) {
	    prot_printf(httpd_out, "Content-Location: %s\r\n", resp_body->loc);
	    if (txn->flags.cors) Access_Control_Expose("Content-Location");
	}
	if (resp_body->md5) {
	    Content_MD5(resp_body->md5);
	}
    }


    /* Payload */
    switch (code) {
    case HTTP_NO_CONTENT:
    case HTTP_NOT_MODIFIED:
	/* MUST NOT include a body */
	break;

    case HTTP_UNSAT_RANGE:
	prot_printf(httpd_out, "Content-Range: bytes */%lu\r\n",
		    resp_body->len);
	resp_body->len = 0;  /* No content */

	/* Fall through and specify framing */

    case HTTP_PARTIAL:
	if (resp_body->range) {
	    prot_printf(httpd_out, "Content-Range: bytes %lu-%lu/%lu\r\n",
			resp_body->range->first, resp_body->range->last,
			resp_body->len);

	    /* Set actual content length of range */
	    resp_body->len = resp_body->range->last -
		resp_body->range->first + 1;

	    free(resp_body->range);
	}

	/* Fall through and specify framing */

    default:
	if (txn->flags.te) {
	    /* HTTP/1.1+ only - we use close-delimiting for HTTP/1.0 */
	    if (!txn->flags.ver1_0) {
		/* Construct Transfer-Encoding header */
		const char *te[] =
		    { "deflate", "gzip", "chunked", NULL };

		comma_list_hdr("Transfer-Encoding", te, txn->flags.te);

		if (txn->flags.trailer) {
		    /* Construct Trailer header */
		    const char *trailer_hdrs[] =
			{ "Content-MD5", NULL };

		    comma_list_hdr("Trailer", trailer_hdrs, txn->flags.trailer);
		}
	    }
	}
	else if (resp_body->len || txn->meth != METH_HEAD) {
	    prot_printf(httpd_out, "Content-Length: %lu\r\n", resp_body->len);
	}
    }


    /* CRLF terminating the header block */
    prot_puts(httpd_out, "\r\n");


    /* Log the client request and our response */
    buf_reset(&log);
    /* Add client data */
    buf_printf(&log, "%s", httpd_clienthost);
    if (proxy_userid) buf_printf(&log, " as \"%s\"", proxy_userid);
    if (txn->req_hdrs &&
	(hdr = spool_getheader(txn->req_hdrs, "User-Agent"))) {
	buf_printf(&log, " with \"%s\"", hdr[0]);
	if ((hdr = spool_getheader(txn->req_hdrs, "X-Client")))
	    buf_printf(&log, " by \"%s\"", hdr[0]);
	else if ((hdr = spool_getheader(txn->req_hdrs, "X-Requested-With")))
	    buf_printf(&log, " by \"%s\"", hdr[0]);
    }
    /* Add request-line */
    buf_appendcstr(&log, "; \"");
    if (txn->req_line.meth) {
	buf_printf(&log, "%s",
		   txn->flags.override ? "POST" : txn->req_line.meth);
	if (txn->req_line.uri) {
	    buf_printf(&log, " %s", txn->req_line.uri);
	    if (txn->req_line.ver) {
		buf_printf(&log, " %s", txn->req_line.ver);
		if (code != HTTP_TOO_LONG) {
		    char *p = txn->req_line.ver + strlen(txn->req_line.ver) + 1;
		    if (*p) buf_printf(&log, " %s", p);
		}
	    }
	}
    }
    buf_appendcstr(&log, "\"");
    if (txn->req_hdrs) {
	/* Add any request modifying headers */
	const char *sep = " (";

	if (txn->flags.override) {
	    buf_printf(&log, "%smethod-override=%s", sep, txn->req_line.meth);
	    sep = "; ";
	}
	if ((hdr = spool_getheader(txn->req_hdrs, "Origin"))) {
	    buf_printf(&log, "%sorigin=%s", sep, hdr[0]);
	    sep = "; ";
	}
	if ((hdr = spool_getheader(txn->req_hdrs, "Referer"))) {
	    buf_printf(&log, "%sreferer=%s", sep, hdr[0]);
	    sep = "; ";
	}
	if ((hdr = spool_getheader(txn->req_hdrs, "Destination"))) {
	    buf_printf(&log, "%sdestination=%s", sep, hdr[0]);
	    sep = "; ";
	}
	if ((hdr = spool_getheader(txn->req_hdrs, ":type"))) {
	    buf_printf(&log, "%stype=%s", sep, hdr[0]);
	    sep = "; ";
	}
	if ((hdr = spool_getheader(txn->req_hdrs, "Depth"))) {
	    buf_printf(&log, "%sdepth=%s", sep, hdr[0]);
	    sep = "; ";
	}
	if (*sep == ';') buf_appendcstr(&log, ")");
    }
    buf_printf(&log, " => \"%s\"", error_message(code));
    /* Add any auxiliary response data */
    if (txn->location) {
	buf_printf(&log, " (location=%s)", txn->location);
    }
    else if (txn->flags.cors) {
	buf_appendcstr(&log, " (allow-origin)");
    }
    else if (txn->error.desc) {
	buf_printf(&log, " (error=%s)", txn->error.desc);
    }
    syslog(LOG_INFO, "%s", buf_cstring(&log));
}