EXPORTED void write_body(long code, struct transaction_t *txn,
		const char *buf, unsigned len)
{
    unsigned is_dynamic = code ? (txn->flags.te & TE_CHUNKED) : 1;
    unsigned outlen = len, offset = 0;
    int do_md5 = config_getswitch(IMAPOPT_HTTPCONTENTMD5);
    static MD5_CTX ctx;
    static unsigned char md5[MD5_DIGEST_LENGTH];

    if (!is_dynamic && len < GZIP_MIN_LEN) {
	/* Don't compress small static content */
	txn->resp_body.enc = CE_IDENTITY;
	txn->flags.te = TE_NONE;
    }

    /* Compress data */
    if (txn->resp_body.enc || txn->flags.te & ~TE_CHUNKED) {
#ifdef HAVE_ZLIB
	/* Only flush for static content or on last (zero-length) chunk */
	unsigned flush = (is_dynamic && len) ? Z_NO_FLUSH : Z_FINISH;

	if (code) deflateReset(&txn->zstrm);

	txn->zstrm.next_in = (Bytef *) buf;
	txn->zstrm.avail_in = len;
	buf_reset(&txn->zbuf);

	do {
	    buf_ensure(&txn->zbuf,
		       deflateBound(&txn->zstrm, txn->zstrm.avail_in));

	    txn->zstrm.next_out = (Bytef *) txn->zbuf.s + txn->zbuf.len;
	    txn->zstrm.avail_out = txn->zbuf.alloc - txn->zbuf.len;

	    deflate(&txn->zstrm, flush);
	    txn->zbuf.len = txn->zbuf.alloc - txn->zstrm.avail_out;

	} while (!txn->zstrm.avail_out);

	buf = txn->zbuf.s;
	outlen = txn->zbuf.len;
#else
	/* XXX should never get here */
	fatal("Compression requested, but no zlib", EC_SOFTWARE);
#endif /* HAVE_ZLIB */
    }

    if (code) {
	/* Initial call - prepare response header based on CE, TE and version */
	if (do_md5) MD5Init(&ctx);

	if (txn->flags.te & ~TE_CHUNKED) {
	    /* Transfer-Encoded content MUST be chunked */
	    txn->flags.te |= TE_CHUNKED;

	    if (!is_dynamic) {
		/* Handle static content as last chunk */
		len = 0;
	    }
	}

	if (!(txn->flags.te & TE_CHUNKED)) {
	    /* Full/partial body (no encoding).
	     *
	     * In all cases, 'resp_body.len' is used to specify complete-length
	     * In the case of a 206 or 416 response, Content-Length will be
	     * set accordingly in response_header().
	     */
	    txn->resp_body.len = outlen;

	    if (code == HTTP_PARTIAL) {
		/* check_precond() tells us that this is a range request */
		code = parse_ranges(*spool_getheader(txn->req_hdrs, "Range"),
				    outlen, &txn->resp_body.range);

		switch (code) {
		case HTTP_OK:
		    /* Full body (unknown range-unit) */
		    break;

		case HTTP_PARTIAL:
		    /* One or more range request(s) */
		    txn->resp_body.len = outlen;

		    if (txn->resp_body.range->next) {
			/* Multiple ranges */
			multipart_byteranges(txn, buf);
			return;
		    }
		    else {
			/* Single range - set data parameters accordingly */
			offset += txn->resp_body.range->first;
			outlen = txn->resp_body.range->last -
			    txn->resp_body.range->first + 1;
		    }
		    break;

		case HTTP_UNSAT_RANGE:
		    /* No valid ranges */
		    outlen = 0;
		    break;
		}
	    }

	    if (outlen && do_md5) {
		MD5Update(&ctx, buf+offset, outlen);
		MD5Final(md5, &ctx);
		txn->resp_body.md5 = md5;
	    }
	}
	else if (txn->flags.ver1_0) {
	    /* HTTP/1.0 doesn't support chunked - close-delimit the body */
	    txn->flags.conn = CONN_CLOSE;
	}
	else if (do_md5) txn->flags.trailer = TRAILER_CMD5;

	response_header(code, txn);

	/* MUST NOT send a body for 1xx/204/304 response or any HEAD response */
	switch (code) {
	case HTTP_CONTINUE:
	case HTTP_SWITCH_PROT:
	case HTTP_PROCESSING:
	case HTTP_NO_CONTENT:
	case HTTP_NOT_MODIFIED:
	    return;

	default:
	    if (txn->meth == METH_HEAD) return;
	}
    }

    /* Output data */
    if ((txn->flags.te & TE_CHUNKED) && !txn->flags.ver1_0) {
	/* HTTP/1.1 chunk */
	if (outlen) {
	    prot_printf(httpd_out, "%x\r\n", outlen);
	    prot_write(httpd_out, buf, outlen);
	    prot_puts(httpd_out, "\r\n");

	    if (do_md5) MD5Update(&ctx, buf, outlen);	    
	}
	if (!len) {
	    /* Terminate the HTTP/1.1 body with a zero-length chunk */
	    prot_puts(httpd_out, "0\r\n");

	    /* Trailer */
	    if (do_md5) {
		MD5Final(md5, &ctx);
		Content_MD5(md5);
	    }

	    prot_puts(httpd_out, "\r\n");
	}
    }
    else {
	/* Full body or HTTP/1.0 close-delimited body */
	prot_write(httpd_out, buf + offset, outlen);
    }
}