/*
* transform.c - written by ale in milano on 09 Sep 2020
* functions to undo MLM transformations

Copyright (C) 2020-2023 Alessandro Vesely

This file is part of zdkimfilter

zdkimfilter is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

zdkimfilter is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License version 3
along with zdkimfilter.  If not, see <http://www.gnu.org/licenses/>.

Additional permission under GNU GPLv3 section 7:

If you modify zdkimfilter, or any covered part of it, by linking or combining
it with OpenSSL, OpenDKIM, Sendmail, or any software developed by The Trusted
Domain Project or Sendmail Inc., containing parts covered by the applicable
licence, the licensor of zdkimfilter grants you additional permission to convey
the resulting work.
*/

#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <stdint.h>

#include <syslog.h> // for LOG_DEBUG,... constants

#include "transform.h"
#include "filterlib.h" // for fl_report()
#include "util.h"
#include <libopendkim/dkim-mailparse.h>
#include <libopendkim/dkim-arparse.h>

#include <assert.h>

char const *retry_mode_string(transform_retry_mode mode)
{
	switch (mode)
	{
		case transform_retry_none:
			return "no retry at all";
		case transform_retry_header:
			return "retry header only";
		case transform_retry_body:
			return "retry body only";
		case transform_retry_all:
			return "retry header and body";
		default:
			assert(0);
			return "ERROR";
	}
}


void clear_dkim_transform(dkim_transform *dt)
{
	if (dt->dkim)
	{
		dkim_free(dt->dkim);
		dt->dkim = NULL;
	}
	if (dt->vc)
	{
		clear_transform_stack(dt->vc);
		dt->vc = NULL;
	}
	if (dt->tw)
	{
		clear_treewalk(dt->tw);
		dt->tw = NULL;
	}

	free(dt->boundary);
	dt->boundary = NULL;
	free(dt->first_content_type);
	dt->first_content_type = NULL;
	free(dt->sender_domain);
	dt->sender_domain = NULL;
	free(dt->list_domain);
	dt->list_domain = NULL;
	for (int i = 0; i < MAX_SAVE_SUBJECT_ALTERNATIVES; ++i)
	{
		free(dt->save_subject[i]);
		dt->save_subject[i] = NULL;
	}
	// dmarc_domains cleared by caller
	clear_original_header(dt->oh_base);
	dt->oh_base = NULL;
	for (int i = 0; i < MAX_FROM_CANDIDATES; ++i)
		clear_candidate_from(&dt->cf[i]);
	if (dt->save_fp)
	{
		fclose(dt->save_fp);
		dt->save_fp = NULL;
	}
	free(dt->save_file);
	dt->save_file = NULL;

	dt->retry_mode = transform_retry_none;
	dt->cleared = 1;
}

void clear_candidate_from(candidate_from **dest)
{
	assert(dest);

	candidate_from *cf = *dest;
	if (cf)
	{
		free(cf->mailbox);
		free(cf->domain);
		free(cf);
		*dest = NULL;
	}
}

int add_candidate_from(candidate_from **dest, char const*space_mailbox, candidate_is is)
{
	assert(dest);

	/*
	* Skip leading whitespace eliminates any possible newline inserted
	* by rewriting.  This shouldn't disturb, if canonicalized relaxed.
	*/
	char const *mailbox = skip_fws(space_mailbox);
	if (mailbox == NULL)
		return 0;

	char *p = strdup(mailbox);
	if (p == NULL)
	{
		fl_report(LOG_ALERT, "MEMORY FAULT");
		return -1;
	}

	char *user, *domain, *cont, *start = p;
	if (my_mail_parse_c(p, &user, &domain, &cont))
		domain = NULL;

	// consider only the last mailbox in some of the fields
	else if (is == candidate_is_reply_to || is == candidate_is_cc)
	{
		char *next;
		while (cont && *cont && (next = skip_cfws(cont)) != NULL && *next)
		{
			start = cont;
			if (my_mail_parse_c(p, &user, &domain, &cont))
			{
				domain = NULL;
				break;
			}
		}
	}

	if (domain == NULL)
	{
		free(p);
		return 0;
	}

	candidate_from *n = malloc(sizeof *n);
	if (n == NULL)
	{
		fl_report(LOG_ALERT, "MEMORY FAULT");
		return -1;
	}

	memset(n, 0, sizeof *n);
	n->mailbox = strdup(mailbox);
	n->domain = strdup(domain);
	n->part = start == p? 0: start - p -1; // point to "," if added
	n->is = is;
	free(p);

	if (n->mailbox == NULL || n->domain == NULL)
	{
		free(n);
		fl_report(LOG_ALERT, "MEMORY FAULT");
		return -1;
	}

	*dest = n;
	return 0;
}

char const *cte_transform_string(cte_transform cte)
{
	switch (cte)
	{
		case cte_transform_identity: return "identity";
		case cte_transform_base64: return "base64";
		case cte_transform_quoted_printable: return "quoted-printable";
		case cte_transform_identity_multipart: return "multipart";
		default: return "--UNKNOWN--";
	}
}


void parse_content_transfer_encoding(dkim_transform *dt, char const *s, int is_original)
/*
* s is the value of a Content-Transfer-Encoding header field.
* 
* If an entity is of type "multipart" the Content-Transfer-Encoding is
* not permitted to have any value other than "7bit", "8bit" or "binary".
*/
{
	assert(dt);
	assert(s);

	static const char id1[] = "7bit";
	static const char id2[] = "8bit";
	static const char id3[] = "binary";
	static const char base64[] = "base64";
	static const char quoted_printable[] = "quoted-printable";

	if ((s = skip_cfws(s)) == NULL)
		return;

	char const *tok = s;
	int ch;
	while (isalnum(ch = *(unsigned char*)s) || ch == '-')
		++s;

	unsigned len = s - tok;
	cte_transform *cte = is_original? &dt->original_cte: &dt->cte;

	if (*cte == 0)  // no double setting
	{
		if (len == sizeof id1 -1 && strincmp(tok, id1, len) == 0)
			*cte = cte_transform_identity;
		else if (len == sizeof id2 -1 && strincmp(tok, id2, len) == 0)
			*cte = cte_transform_identity;
		else if (len == sizeof id3 -1 && strincmp(tok, id3, len) == 0)
			*cte = cte_transform_identity;
		else if (len == sizeof base64 -1 && strincmp(tok, base64, len) == 0)
			*cte = cte_transform_base64;
		else if (len == sizeof quoted_printable -1 && strincmp(tok, quoted_printable, len) == 0)
			*cte = cte_transform_quoted_printable;
	}

	if (is_original && (s = skip_cfws(s)) != NULL && *s == ';')
	/*
	* for example:
	* Original-Content-Transfer-Encoding: base64; column-width=76
	*/
	{
		static const char column_width[] = "column-width";
		static const char nocr[] = "nocr";
		tok = s = skip_cfws(s + 1);
		if (tok)
		{
			while (isalnum(ch = *(unsigned char*)s) || ch == '-')
				++s;
			len = s - tok;
			if (len == sizeof column_width -1 && strincmp(tok, column_width, len) == 0)
			{
				if ((s = skip_cfws(s)) != NULL)
				{
					if (*s == '=')
					{
						char *sval = skip_cfws(s + 1);
						if (sval)
						{
							long l = strtol(sval, NULL, 10);
							if (l > 0 && l < 1024)
								dt->b64.column_width = l;
						}
					}
				}
			}
			else if (len == sizeof nocr -1 && strincmp(tok, nocr, len) == 0)
				dt->b64.nocr = 1;
		}
	}
}

cte_transform parse_content_transfer_encoding_simple(char const *s)
{
	dkim_transform dt;
	dt.cte = 0;
	parse_content_transfer_encoding(&dt, s, 0);
	return dt.cte;
}

int content_type_is_text_plain(char const *s)
/*
* s is the value of a Content-Type header field.
* 
* Only care of checking whether it is text/plain
*/
{
	static const char text[] = "text";
	static const char plain[] = "plain";

	if ((s = skip_cfws(s)) == NULL)
		return -1;

	char const *tok = s;
	int ch;
	while (isalnum(ch = *(unsigned char*)s) || ch == '-')
		++s;

	unsigned len = s - tok;
	if (len == sizeof text - 1 && strincmp(tok, text, len) == 0 &&
		(s = skip_cfws(s)) != NULL && *s == '/' &&
		(s = skip_cfws(s + 1)) != NULL)
	{
		tok = s;
		while (isalnum(ch = *(unsigned char*)s) || ch == '-')
			++s;

		len = s - tok;
		if (len == sizeof plain - 1 && strincmp(tok, plain, len) == 0)
			return 1;
	}

	return 0;
}

static char *skip_cfws_sep(char const *s, int sep)
{
	char *p = skip_cfws(s);
	if (*(unsigned char *)p != sep)
		return NULL;

	return skip_cfws(p+1);
}

void parse_content_type(dkim_transform *dt, char const *s, int is_original)
/*
* s is the value of a Content-Type header field.
* 
* Only care of checking whether it is multipart;
*/
{
	static const char multipart[] = "multipart";
	static const char boundary[] = "boundary";

	if ((s = skip_cfws(s)) == NULL)
		return;

	char const *tok = s;
	int ch;
	while (isalnum(ch = *(unsigned char*)s) || ch == '-')
		++s;

	unsigned len = s - tok;
	cte_transform *cte = is_original? &dt->original_cte: &dt->cte;

	if (*cte == 0)  // no double setting
	{
		if (len == sizeof multipart -1 && strincmp(tok, multipart, len) == 0)
			*cte = cte_transform_identity_multipart;
	}

	if (!is_original &&
		dt->cte == cte_transform_identity_multipart &&
		dt->boundary == NULL)
	{
		if ((s = skip_cfws(s)) != NULL && *s == '/' &&
			(s = skip_token(s + 1)) != NULL)
		{
			while ((s = skip_cfws_sep(s, ';')) != NULL)
			{
				tok = s;
				s = skip_token(tok);
				len = s - tok;
				if ((s = skip_cfws_sep(s, '=')) != NULL)
				{
					char const *e;
					int quote = *(unsigned char*)s == '"';
					if (quote)
					{
						++s;
						e = my_mail_matching_paren(s, s + strlen(s), '\0', '"');
					}
					else
						e = skip_token(s);

					if (len == sizeof boundary -1 && strincmp(tok, boundary, len) == 0)
					{
						unsigned blen = e - s;
						if ((dt->boundary = malloc(blen + 1)) != NULL)
						{
							memcpy(dt->boundary, s, blen);
							dt->boundary[blen] = 0;
						}
						break;
					}
					s = e;
				}
				else
					break;
			}
		}
	}
}

int content_type_is_multipart(char const *s)
{
	dkim_transform dt;
	dt.original_cte = 0;
	parse_content_type(&dt, s, 1);
	return dt.original_cte == cte_transform_identity_multipart;
}

static original_header OH_INVALID = {NULL, 0, 0, 0, 0, 0, 0, ""};

void clear_original_header(original_header *oh)
{
	while (oh != NULL)
	{
		original_header* const next = oh->next;
		free(oh);
		oh = next;
	}
}

typedef enum get_oh_mode
{
	oh_dont_add, oh_must_be_new, oh_never_mind, oh_remove_it
} get_oh_mode;

static original_header *
get_original_header(original_header **base, char const *hdr, get_oh_mode mode)
/*
* hdr points to the header name after the dash in "Original-".
* When inserting a new field, a duplicate makes an invalid transform.
* When retrieving, just want to know if found or not.  In this case,
* mode == oh_dont_add, hdr contains just the field name, without column.
*/
{
	assert(base);
	assert(hdr);

	char const *colon = strchr(hdr, ':');
	if (colon == NULL && mode != oh_dont_add && mode != oh_remove_it)
	{
		fl_report(LOG_WARNING, "Original header without colon: %s", hdr);
		return &OH_INVALID;
	}

	unsigned name_length, colon_ndx;
	if (colon)
	{
		colon_ndx = colon - hdr;
		while (colon > hdr && isspace(*(unsigned char*)(colon - 1)))
			--colon;

		name_length = colon - hdr;
	}
	else
	{
		colon_ndx = name_length = strlen(hdr);
	}

	original_header**oh = base;
	for (; *oh != NULL; oh = &(*oh)->next)
	{
		if (name_length < (*oh)->name_length)
			continue;

		if (name_length == (*oh)->name_length)
		{
			int const cmp = strincmp(hdr, (*oh)->field, name_length);
			if (cmp < 0)
				continue;

			if (cmp == 0) // found
			{
				if (mode == oh_must_be_new)
				/*
				* Repeated Original-* are invalid.
				*/
				{
					// fl_report(LOG_WARNING, "Original header repeated: %s", hdr);
					return &OH_INVALID;
				}

				if (mode == oh_remove_it)
				{
					original_header *del = *oh;
					*oh = del->next;
					free(del);
					return NULL;
				}
				return *oh;
			}
		}

		break;
	}

	// not found
	if (mode == oh_dont_add || mode == oh_remove_it)
		return NULL;

	size_t const len = sizeof(original_header);
	size_t const len2 = strlen(hdr) + 1;
	original_header *new_oh = malloc(len + len2);
	if (new_oh)
	{
		memset(new_oh, 0, len);
		new_oh->name_length = name_length;
		new_oh->colon_ndx = colon_ndx;
		new_oh->next = *oh;
		*oh = new_oh;
		char *start = &new_oh->field[0];
		memcpy(start, hdr, len2);
		char *e = start + len2 - 1; // point to the trailing 0
		for (start += colon_ndx + 1; start < e; ++start)
			if (!isspace(*(unsigned char*)start))
				break;
		new_oh->is_empty = start == e;
		assert(new_oh->field[len2-2] != '\n');
	}
	else
		fl_report(LOG_ALERT, "MEMORY FAULT");

	return new_oh;
}

int check_original_subject(dkim_transform *dt)
/*
* If an Original-Subject: was given in the message, it takes precedence.
* If an Original-Subject: exists already and is equal to the signed one,
* remove it, as it is not a reason to redo the header.
*
* Note: It seems that "RE: [subject tag] blah..." can be replaced by
* "Re: [subject tag] blah...".
*
* Return -1 on fatal error, 0 otherwise.
*/
{
	static const char subject[] = "Subject:";
	char *s = dt->save_subject[0];
	if (s == NULL)  // ??
		return 0;

	original_header *oh =
		get_original_header(&dt->oh_base, subject, oh_dont_add);
	if (oh) // exists already
	{
		if (strcmp(s, oh->field + oh->colon_ndx + 1) == 0)
			get_original_header(&dt->oh_base, subject, oh_remove_it);
		return 0;
	}

	/*
	* If the original subject was saved somewhere else, use it.
	* Note that they're saved without header field name.
	*/

	if (dt->is_reply && dt->save_subject[1] != NULL)
	/*
	* dt->save_subject[1] is "Thread-Topic" (check verify_headers()).
	* It saves the subject of the first message in a thread.
	*/
	{
		free(dt->save_subject[1]);
		dt->save_subject[1] = NULL;
	}

	for (int i = 1; i < MAX_SAVE_SUBJECT_ALTERNATIVES; ++i)
	{
		char *si;
		if ((si = dt->save_subject[i]) != NULL)
		{
			unsigned len = strlen(si);
			char buf[len + sizeof subject];
			strcat(strcpy(buf, subject), si);
			oh = get_original_header(&dt->oh_base, buf, oh_must_be_new);
			return oh == NULL? -1: 0;
		}
	}

	/*
	* Assume that any subject tag was added by the MLM.
	*/
	unsigned len = strlen(s);
	char buf[len + sizeof subject];
	strcat(strcpy(buf, subject), s);

	char *p = &buf[sizeof subject - 1];
	s = p;
	while (isspace(*(unsigned char*)p))
		++p;
	if (*p == '[')
	{
		char const *e = my_mail_matching_paren(p + 1, s + len, '[', ']');
		if (*e++ != 0)
		{
			/*
			* TODO: enforce max taglen
			*/
			unsigned taglen = e - s;
			assert(len >= taglen);
			len -= taglen;
			memmove(s, e, len);
			s[len] = 0;
			oh = get_original_header(&dt->oh_base, buf, oh_must_be_new);
			if (oh == NULL)
				return -1;
		}
	}

	return 0;
}

char *replace_original_header(dkim_transform *dt, char const *hdr)
/*
* Return NULL if the replacement doesn't exist.
*/
{
	assert(dt);
	assert(dt->dkim);
	assert(hdr);

	original_header *oh =
		get_original_header(&dt->oh_base, hdr, oh_dont_add);
	if (oh)
	{
		oh->is_replaced = 1;
		if (oh->is_empty)
			return "";
		return &oh->field[0];
	}

	return NULL;
}

char *peek_original_header(dkim_transform *dt, char const *hdr)
/*
* Return NULL if the replacement doesn't exist.
*/
{
	assert(dt);
	assert(hdr);

	original_header *oh =
		get_original_header(&dt->oh_base, hdr, oh_dont_add);
	if (oh)
	{
		assert(oh->field[oh->colon_ndx] == ':');
		return &oh->field[oh->colon_ndx + 1];
	}

	return NULL;
}

int add_to_original(dkim_transform *dt, char const *hdr)
/*
* Add these header fields even if dt->dkim is not open yet.
*/
{
	assert(dt);

	if (! dt->cleared)
	{
		original_header *oh = get_original_header(&dt->oh_base, hdr, oh_must_be_new);
		if (oh == &OH_INVALID)
		{
			clear_dkim_transform(dt);
			return 0;
		}

		if (oh == NULL)
			return -1;
	}

	return 0;
}

int add_to_original_signed(original_header **oh_base, char const *hdr,
	original_h_flag original_h)
/*
* Add these header fields, on signing messages.
*/
{
	assert(oh_base);

	original_header *oh = get_original_header(oh_base, hdr, oh_never_mind);
	if (oh == NULL)
		return -1;

	oh->only_if_signed = (original_h & original_h_only_if_signed) != 0;
	oh->is_author = (original_h & original_h_is_author) != 0;

	return 0;
}

int is_any_original_signed(original_header *oh, DKIM_SIGINFO *sig)
{
	for (; oh != NULL; oh = oh->next)
	{
		int name_length = oh->name_length;
		char *field = &oh->field[0];
		int savech = field[name_length];
		field[name_length] = 0;
		int rtc = dkim_sig_hdrsigned(sig, field);
		field[name_length] = savech;
		if (rtc)
			return rtc;
	}

	return 0;
}

/*
* In principle, reversing the transformation shouldn't be more 
* complicated than verifying a signature with l=.  Skipping the subject 
* tag while canonicalizing the header is not much more work than adding 
* \r to each line (at mine, lines are terminated by a bare \n.)  The 
* addition of an entity may require to skip an initial part of the body, 
* and then stop at the corresponding boundary.  It can be considered a 
* refined canonicalization, negligible w.r.t. cryptography.
*/

typedef struct skip_one_entity_parm
{
	char *boundary;
	unsigned boundary_length;
	unsigned skipped:   1; // skipped already
	unsigned complete:  1; // found beginning of footer
	unsigned in_header: 1; // found starting boundary
} skip_one_entity_parm;

int skip_one_entity(char *in, unsigned len, transform_stack *vc)
{
	assert(vc);

	skip_one_entity_parm *ss = (skip_one_entity_parm *)vc->fn_parm;
	assert(ss);
	assert(ss->skipped == 0 || ss->boundary != NULL);

	vc = vc->next;
	if (in == NULL) // flush or cleanup call
	{
		if (len == 1) // cleanup
		{
			free(ss->boundary);
			ss->boundary = NULL;
		}
		return (*vc->fn)(NULL, len, vc);
	}

	if (ss->skipped)
	{
		if (len >= ss->boundary_length &&
			strncmp(in, ss->boundary, ss->boundary_length) == 0)
		// the previous CRLF and this line are not part of the original body
		{
			ss->complete = 1;
		}

		if (ss->complete)
			return 0;

		return (*vc->fn)(in, len, vc);
	}

	if (ss->in_header)
	{
		if (len == 2 && in[0] == '\r' && in[1] == '\n')
			ss->skipped = 1;

		return 0;
	}

	if (len > 2 && in[0] == '-' && in[1] == '-')
	{
		char *e = &in[len];
		while (isspace(*(unsigned char*)--e))
			continue;

		++e;
		ss->boundary_length = e - &in[0];
		size_t const blen = ss->boundary_length + 1;
		ss->boundary = malloc(blen);
		if (ss->boundary == NULL)
			return -1;

		memcpy(ss->boundary, &in[0], blen);
		ss->in_header = 1;
	}

	return 0;
}

// base64

static const char base64tab[]=
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";

typedef struct decode_base64_parm
{
	unsigned char rest;
	char tail[4];
	char prev;
} decode_base64_parm;

static int decode_base64_adjust_buf(char *, unsigned, transform_stack *);
int decode_base64(char *in, unsigned len, transform_stack *vc)
{
	assert(vc);
	assert(vc->next);

	if (in == NULL) // flush or cleanup call
	{
		// if bp->rest encoding was bad
		vc = vc->next;
		return (*vc->fn)(NULL, len, vc);
	}

	decode_base64_parm *bp = (decode_base64_parm*)vc->fn_parm;
	if (bp->rest)
		return decode_base64_adjust_buf(in, len, vc);

	char out[3 * (len + 1)/2]; // account for added CRs
	union fourbytes
	{
		unsigned char four[4];
		uint32_t all;
	} un;
	unsigned outlen = 0;
	int i = 0, ch = 0;
	char prev = bp->prev;

	for (;;)
	{
		un.all = 0x55555555;
		for (i = 0; i < 4;)
		{
			ch = *(unsigned char*)in++;
			if (ch == 0)
				break;

			char *s = strchr(base64tab, ch);
			if (s == NULL)
				continue;

			un.four[i++] = s - base64tab;
		}

		if (i < 4)
		{
			assert(ch == 0);
			bp->rest = i;
			for (int j = 0; j < i; ++j)
				bp->tail[j] = base64tab[un.four[j]];
		}
		else
		{
			char a = (un.four[0] << 2) | (un.four[1] >> 4);
			char b = (un.four[1] << 4) | (un.four[2] >> 2);
			char c = (un.four[2] << 6) | un.four[3];
			if (a == '\n' && prev != '\r')
				out[outlen++] = '\r';
			out[outlen++] = prev = a;

			if (un.four[2] < 64)
			{
				if (b == '\n' && prev != '\r')
					out[outlen++] = '\r';
				out[outlen++] = prev = b;
				if (un.four[3] < 64)
				{
					if (c == '\n' && prev != '\r')
						out[outlen++] = '\r';
					out[outlen++] = prev = c;
					if (ch)
						continue;
				}
			}
		}

		break;
	}

	bp->prev = prev;
	vc = vc->next;
	out[outlen] = 0;
	return (*vc->fn)(out, outlen, vc);
}

static int
decode_base64_adjust_buf(char *in, unsigned len, transform_stack *vc)
{
	assert(vc);
	assert(vc->next);

	decode_base64_parm *bp = (decode_base64_parm*)vc->fn_parm;
	assert(bp->rest < 4);

	unsigned size = bp->rest + len + 1;
	char my_in[size];

	memcpy(my_in, bp->tail, bp->rest);
	memcpy(&my_in[bp->rest], in, len);
	my_in[size - 1] = 0;
	bp->rest = 0;
	return decode_base64(my_in, size, vc);
}

typedef struct encode_base64_parm
{
	unsigned column_width, column;
	char nocr, nooutcr;
	unsigned char rest;
	char tail[4];
} encode_base64_parm;

static void encode_base64_init(transform_stack *vc, void *vcw)
{
	assert(vc);

	encode_base64_parm *bp = (encode_base64_parm*)vc->fn_parm;
	base64_encode_parm *b64 = (base64_encode_parm*)vcw;

	bp->column_width = b64 && b64->column_width > 0? b64->column_width: 76;
	if (b64)
	{
		bp->nocr = b64->nocr;
		bp->nooutcr = b64->nooutcr;
	}
}

int encode_base64(char *in, unsigned len, transform_stack *vc)
{
	assert(vc);
	assert(vc->next);

	encode_base64_parm *bp = (encode_base64_parm*)vc->fn_parm;
	assert(bp->rest < 3);
	assert(bp->column_width > 0);

	unsigned outlen = 0, column = bp->column;
	int nocr = bp->nocr, nooutcr = bp->nooutcr;

#define CHECK_COLUMN_WIDTH(nooutcr) \
	do \
	{ \
		if (++column >= bp->column_width) \
		{ \
			if (nooutcr == 0) \
				out[outlen++] = '\r'; \
			out[outlen++] = '\n'; \
			column = 0; \
		} \
	} while(0)

	if (in == NULL) // flush or cleanup call
	{
		vc = vc->next;
		if (len == 0) // flush
		{
			char out[16];

			if (bp->rest)
			{
				int ch = (unsigned char)bp->tail[0],
					ch1 = (unsigned char)bp->tail[1];

				out[outlen++] = base64tab[(ch >> 2) & 0x3F];
				CHECK_COLUMN_WIDTH(nooutcr);
				if (bp->rest == 1)
				{
					out[outlen++] = base64tab[((ch & 0x3) << 4)];
					CHECK_COLUMN_WIDTH(nooutcr);
					out[outlen++] = '=';
				}
				else
				{
					out[outlen++] = base64tab[((ch & 0x3) << 4) | ((ch1 & 0xF0) >> 4)];
					CHECK_COLUMN_WIDTH(nooutcr);
					out[outlen++] = base64tab[((ch1 & 0xF) << 2)];
				}
				CHECK_COLUMN_WIDTH(nooutcr);
				out[outlen++] = '=';
			}
			if (nooutcr == 0)
				out[outlen++] = '\r';
			out[outlen++] = '\n';
			out[outlen] = 0;
			int rtc = (*vc->fn)(out, outlen, vc);
			if (rtc)
				return rtc;
		}
		return (*vc->fn)(NULL, len, vc);
	}

	char buf[len + 4], *p, *end;
	if (bp->rest)
	{
		p = &buf[0];
		memcpy(p, bp->tail, bp->rest);
		memcpy(&buf[bp->rest], in, len);
		len += bp->rest;
		end = &buf[len];
		*end = 0;
		bp->rest = 0;
	}
	else
	{
		p = &in[0];
		end = p + len;
	}

	size_t outsize = ((len + 2) / 3 * 4) + 1;
	outsize += 2 + 2*outsize/bp->column_width;

	char out[outsize];

	while (p + 2 < end)
	{
		int ch = *(unsigned char*)p++;
		if (nocr && ch == '\r') ch = *(unsigned char*)p++;
		int ch1 = *(unsigned char*)p++;
		if (nocr && ch1 == '\r') ch1 = *(unsigned char*)p++;
		int ch2 = *(unsigned char*)p++;
		if (nocr && ch2 == '\r') ch2 = *(unsigned char*)p++;
		out[outlen++] = base64tab[(ch >> 2) & 0x3F];
		CHECK_COLUMN_WIDTH(nooutcr);
		out[outlen++] = base64tab[((ch & 0x3) << 4) | ((ch1 & 0xF0) >> 4)];
		CHECK_COLUMN_WIDTH(nooutcr);
		out[outlen++] = base64tab[((ch1 & 0xF) << 2) | ((ch2 & 0xC0) >> 6)];
		CHECK_COLUMN_WIDTH(nooutcr);
		out[outlen++] = base64tab[ch2 & 0x3F];
		CHECK_COLUMN_WIDTH(nooutcr);
	}

	bp->column = column;
	while (p < end)
		bp->tail[bp->rest++] = *p++;

#undef CHECK_COLUMN_WIDTH

	vc = vc->next;
	out[outlen] = 0;
	return (*vc->fn)(out, outlen, vc);
}

typedef struct encode_quoted_printable_parm
{
	unsigned column_width, column;
	int anticipated_n;
} encode_quoted_printable_parm;

static void encode_quoted_printable_init(transform_stack *vc, void *vcw)
{
	assert(vc);

	encode_quoted_printable_parm *bp = (encode_quoted_printable_parm*)vc->fn_parm;
	base64_encode_parm *cw = (base64_encode_parm*)vcw;

	bp->column_width = cw && cw->column_width > 0? cw->column_width: 76;
}

int encode_quoted_printable(char *in, unsigned len, transform_stack *vc)
{
	assert(vc);
	assert(vc->next);

	encode_quoted_printable_parm *bp = (encode_quoted_printable_parm*)vc->fn_parm;
	assert(bp->column_width > 0);

	unsigned column = bp->column;

	if (in == NULL) // flush or cleanup call
	{
		vc = vc->next;
		return (*vc->fn)(NULL, len, vc);
	}

	size_t outsize = 3*len;
	outsize += 2 + 2*outsize/bp->column_width;
	char out[outsize + 1];
	unsigned outlen = 0;
	int ch;

#define CHECK_COLUMN_WIDTH(INC) \
	do \
	{ \
		if ((column + INC) >= bp->column_width) \
		{ \
			out[outlen++] = '='; \
			out[outlen++] = '\r'; \
			out[outlen++] = '\n'; \
			column = 0; \
		} \
	} while(0)

	while ((ch = *(unsigned char*)in++) != 0 && len--> 0)
	{
		if ((ch >= 33 && ch <= 60) || (ch >= 62 && ch <= 126))
		{
			out[outlen++] = ch;
			CHECK_COLUMN_WIDTH(1);
			++column;
		}
		else if (ch == 9 || ch == 32)
		{
			CHECK_COLUMN_WIDTH(2);
			out[outlen++] = ch;
			++column;
		}
		else if (ch == 13)
		{
			out[outlen++] = '\r';
			out[outlen++] = '\n';
			column = 0;
			bp->anticipated_n = 1;
		}
		else if (ch == 10)
		{
			if (bp->anticipated_n)
				bp->anticipated_n = 0;
			else
			{
				out[outlen++] = '\r';
				out[outlen++] = '\n';
				column = 0;
			}
		}
		else
		{
			static const char xdigit[] = "0123456789ABCDEF";
			CHECK_COLUMN_WIDTH(3);
			out[outlen++] = '=';
			out[outlen++] = xdigit[ch >> 4];
			out[outlen++] = xdigit[ch & 0xf];
			column += 3;
		}
	}

	bp->column = column;

#undef CHECK_COLUMN_WIDTH

	vc = vc->next;
	out[outlen] = 0;
	return (*vc->fn)(out, outlen, vc);
}

int decode_quoted_printable(char *in, unsigned len, transform_stack *vc)
{
	assert(vc);
	assert(vc->next);

	vc = vc->next;

	if (in == NULL) // flush or cleanup call
		return (*vc->fn)(NULL, len, vc);

	if (len == 0)
		return 0;

	char out[len + 1];
	unsigned outlen = 0;
	int ch;

	while ((ch = *(unsigned char*)in++) != 0)
	{
		if (ch != '=')
		{
			out[outlen++] = ch;
			continue;
		}

		char buf[4];
		if ((buf[0] = *in) != 0)
		{
			buf[1] = *++in;
			if (buf[1] != 0)
				++in;

			// ignore soft line breaks
			if (buf[0] == '\r' && buf[1] == '\n')
				continue;
			if (buf[0] == '\r' || buf[0] == '\n')
			{
				--in;
				continue;
			}
		}
		else
			buf[1] = 0; // bad encoding
		buf[2] = 0;
		out[outlen++] = (char)strtol(buf, NULL, 16);
	}

	out[outlen] = 0;
	return (*vc->fn)(out, outlen, vc);
}

typedef struct limit_size_parm
{
	unsigned long length;
} limit_size_parm;

static void limit_size_init(transform_stack *vc, void* v)
{
	assert(vc);

	limit_size_parm *ls = (limit_size_parm*)vc->fn_parm;
	unsigned long *length = (unsigned long*)v;

	if (length && *length > 0)
		ls->length = *length;
}

int limit_size(char *in, unsigned len, transform_stack *vc)
/*
* Limit the output to the given length
*/
{
	assert(vc);
	assert(vc->next);

	limit_size_parm *ls = (limit_size_parm*)vc->fn_parm;
	vc = vc->next;

	if (in == NULL) // flush or cleanup call
		return (*vc->fn)(NULL, len, vc);

	if (ls->length < len)
	{
		len = ls->length;
		in[len] = 0;
	}

	int rtc = 0;
	if (len)
	{
		rtc = (*vc->fn)(in, len, vc);
		ls->length -= len;
	}

	return rtc;
}

typedef struct skip_last_entity_parm
{
	unsigned long length[2];
} skip_last_entity_parm;

static void skip_last_entity_init(transform_stack *vc, void* v)
{
	assert(vc);
	assert(v);

	skip_last_entity_parm *parm = (skip_last_entity_parm*)vc->fn_parm;
	unsigned long *length = (unsigned long *)v;
	if (length && *length)
	{
		parm->length[0] = length[0];
		parm->length[1] = length[1];
	}
}

int skip_last_entity(char *in, unsigned len, transform_stack *vc)
{
	assert(vc);
	assert(vc->next);

	skip_last_entity_parm *parm = (skip_last_entity_parm*)vc->fn_parm;

	vc = vc->next;

	if (in == NULL) // flush or cleanup call
		return (*vc->fn)(NULL, len, vc);

	int rtc = 0;

	unsigned olen = len;
	if (parm->length[0] < len)
		olen = parm->length[0];

	if (olen > 0)
	{
		rtc = (*vc->fn)(in, olen, vc);
		parm->length[0] -= olen;
		parm->length[1] -= olen;
		len -= olen;
		in += olen;
	}

	if (len > parm->length[1])
	{
		len -= parm->length[1];
		in += parm->length[1];
		parm->length[1] = 0;
		if (rtc == 0)
			rtc = (*vc->fn)(in, len, vc);
	}
	else
		parm->length[1] -= len;

	return rtc;
}

typedef struct rectify_lines_parm
{
	char line[256]; // Footer lines longer than that are ignored
	unsigned in_line;
} rectify_lines_parm;

static int send_available(rectify_lines_parm *rl, transform_stack *vc)
{
	assert(rl);
	assert(vc);

	int rtc = 0;
	if (rl->in_line)
	{
		rl->line[rl->in_line] = 0;
		rtc = (*vc->fn)(rl->line, rl->in_line, vc);
		rl->in_line = 0;
	}

	return rtc;
}

int rectify_lines(char *in, unsigned len, transform_stack *vc)
/*
* Buffer decoded content and feed one line at a time.  Discard CRs but
* count them.  Note that although they are often not encoded, both
* copy_body() and decode_base64() force CRs.  So, LF's not preceded by
* CR can only appear if they're encoded as =0A in quoted-printable, a
* circumstance not encountered thus far.
*/
{
	assert(vc);
	assert(vc->next);

	rectify_lines_parm *rl = (rectify_lines_parm*)vc->fn_parm;
	vc = vc->next;

	if (in == NULL) // flush or cleanup call
	{
		if (len == 0) // flush
			if (send_available(rl, vc))
				return 1;
		return (*vc->fn)(NULL, len, vc);
	}

	char *end = in + len;
	while (in < end)
	{
		if (rl->in_line >= sizeof rl->line -2) // have space for '\n', 0
			if (send_available(rl, vc))
				return 1;

		int ch = *(unsigned char*)in++;
		if (ch != '\n')
			rl->line[rl->in_line++] = ch;
		if (ch == '\r')
		{
			rl->line[rl->in_line++] = '\n';
			if (send_available(rl, vc))
				return 1;
		}
	}

	return 0;
}

typedef void (*parm_init_fn)(transform_stack *vc, void*src);

static const struct param_table
{
	transform_fn fn;
	size_t param_size;
	parm_init_fn init;
} param_table[] =
{
	{&skip_one_entity, sizeof(skip_one_entity_parm), NULL},
	{&decode_base64, sizeof(decode_base64_parm), NULL},
	{&encode_base64, sizeof(encode_base64_parm), &encode_base64_init},
	{&encode_quoted_printable, sizeof(encode_quoted_printable_parm),
		&encode_quoted_printable_init},
	{&limit_size, sizeof(limit_size_parm), &limit_size_init},
	{&decode_quoted_printable, 0, NULL},
	{&rectify_lines, sizeof(rectify_lines_parm), NULL},
	{&skip_last_entity, sizeof(skip_last_entity_parm),
		&skip_last_entity_init}
};

#define PARAM_SIZE_ENTRIES sizeof param_table/sizeof param_table[0]

void clear_transform_stack(transform_stack *vc)
{
	if (vc && vc->fn)
		(*vc->fn)(NULL, 1, vc); // cleanup
	while (vc)
	{
		transform_stack *tmp = vc->next;
		vc->next = NULL;
		free(vc);
		vc = tmp;
	}
}

#if 0
transform_stack *
push_transform_stack(transform_stack *vc, transform_fn fn, void *parm)
/*
* The last called function, which is the first pushed, has vc == NULL,
* parm != NULL, and is not an internal tranform function.  Intermediate
* functions have vc != NULL, parm according to the function being pushed,
* and are internal functions.
*
* If vc is NULL for internal functions, assume a memory failure on a
* previous push, and return NULL without even attempting to allocate
* a new structure.
*/
{
	size_t param_size = 0;
	parm_init_fn init = NULL;

	// For known functions, override the parameter passed
	unsigned fn_i;
	for (fn_i = 0; fn_i < PARAM_SIZE_ENTRIES; ++fn_i)
		if (fn == param_table[fn_i].fn)
		{
			if (vc == NULL)
				return NULL;

			param_size = param_table[fn_i].param_size;
			init = param_table[fn_i].init;
			break;
		}

	size_t size = param_size + sizeof(transform_stack);
	transform_stack *new = malloc(size);
	if (new)
	{
		memset(new, 0, size);
		new->next = vc;
		new->fn = fn;
		if (fn_i < PARAM_SIZE_ENTRIES)
		{
			new->fn_parm = ((char*)new) + sizeof(transform_stack);
			if (init)
				(*init)(new, parm);
		}
		else
			new->fn_parm = parm;

	}
	return new;
}
#endif

transform_stack *
append_transform_stack(transform_stack *vc, transform_fn fn, void *parm)
/*
* The last called function, which is the first pushed, has vc == NULL,
* parm != NULL, and is not an internal tranform function.  Intermediate
* functions have vc != NULL, parm according to the function being pushed,
* and are internal functions.
*
* On memory failure return NULL, and destroy the existing vc, if given.
* Otherwise, return the top structure.
*/
{
	size_t param_size = 0;
	parm_init_fn init = NULL;

	// For known functions, override the parameter passed
	unsigned fn_i;
	for (fn_i = 0; fn_i < PARAM_SIZE_ENTRIES; ++fn_i)
		if (fn == param_table[fn_i].fn)
		{
			param_size = param_table[fn_i].param_size;
			init = param_table[fn_i].init;
			break;
		}

	size_t size = param_size + sizeof(transform_stack);
	transform_stack *new = malloc(size);
	if (new == NULL)
	{
		clear_transform_stack(vc);
		return NULL;
	}

	for (transform_stack *last = vc; last; last = last->next)
		if (last->next == NULL)
		{
			last->next = new;
			break;
		}

	memset(new, 0, size);
	new->fn = fn;
	if (fn_i < PARAM_SIZE_ENTRIES)
	{
		new->fn_parm = ((char*)new) + sizeof(transform_stack);
		if (init)
			(*init)(new, parm);
	}
	else
		new->fn_parm = parm;

	return vc? vc: new;
}

#if TEST_TRANSFORM
#include <stdio.h>
#include <errno.h>

static int verbose = 0, lineno = 0;

int debug_output(char *in, unsigned len, transform_stack *vc)
{
	assert(vc);
	assert(vc->next == NULL);

	if (verbose && in)
		fprintf(stdout, "line %d, len %d\n", lineno++, len);
	if (in)
		fprintf(stdout, "%.*s", len, in);
	fflush(stdout);
	return 0;
}

int main(int argc, char *argv[])
{
	transform_stack *vc = NULL;
	FILE *fp = stdin;
	unsigned column = 0;
	unsigned long length[2] = {0, 0};

	for (int i = 1; i < argc; ++i)
	{
		char *a = argv[i];
		if (strcmp(a, "--in") == 0)
		{
			if (++i >= argc || (fp = fopen(argv[i], "r")) == NULL)
			{
				fprintf(stderr, "cannot open %s: %s\n",
					i < argc? argv[i]: "missing argument", strerror(errno));
				return 1;
			}
		}
		else if (strcmp(a, "--verbose") == 0)
			++verbose;
		else if (strcmp(a, "--column") == 0)
		{
			if (++i >= argc)
			{
				fprintf(stderr, "missing argument");
				return 1;
			}
			column = atoi(argv[i]);
		}
		else if (strcmp(a, "--length") == 0)
		{
			if (++i >= argc)
			{
				fprintf(stderr, "missing argument");
				return 1;
			}
			char *t;
			length[0] = strtoul(argv[i], &t, 0);
			if (*t)
				length[1] = strtoul(t + 1, &t, 0);
		}
		else if (strcmp(a, "skip_one_entity") == 0)
			vc = append_transform_stack(vc, &skip_one_entity, NULL);
		else if (strcmp(a, "decode_base64") == 0)
			vc = append_transform_stack(vc, &decode_base64, NULL);
		else if (strcmp(a, "encode_base64") == 0)
			vc = append_transform_stack(vc, &encode_base64, &column);
		else if (strcmp(a, "decode_quoted_printable") == 0)
			vc = append_transform_stack(vc, &decode_quoted_printable, NULL);
		else if (strcmp(a, "encode_quoted_printable") == 0)
			vc = append_transform_stack(vc, &encode_quoted_printable, &column);
		else if (strcmp(a, "limit_size") == 0)
			vc = append_transform_stack(vc, &limit_size, &length[0]);
		else if (strcmp(a, "skip_last_entity") == 0)
			vc = append_transform_stack(vc, &skip_last_entity, length);
		else
		{
			fprintf(stderr, "Unknown function %s\n", a);
			a = NULL;
		}
	}

	vc = append_transform_stack(vc, &debug_output, NULL);
	if (vc == NULL)
	{
		fputs("MEMORY FAULT!", stderr);
		return 1;
	}

	char buf[8192], *s;
	int rtc = 0;
	while ((s = fgets(buf, sizeof buf - 1, fp)) != NULL)
	{
		unsigned len = strlen(s);
		if (s[len - 1] != '\n')
			break;

		s[len-1] = '\r';
		s[len] = '\n';
		s[++len] = 0;
		rtc = (*vc->fn)(buf, len, vc);
		if (rtc)
			break;
	}

	if (rtc == 0) // flush
	{
		rtc = (*vc->fn)(NULL, 0, vc);
	}

	if (fp != stdin)
		fclose(fp);
	clear_transform_stack(vc);
	return rtc? 1: 0;
}
#endif // TEST_TRANSFORM
