/* This file is part of Mailfromd.
   Copyright (C) 2022-2025 Sergey Poznyakoff

   This program 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, or (at your option)
   any later version.

   This program 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
   along with this program.  If not, see <http://www.gnu.org/licenses/>. */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include <mailutils/mailutils.h>
#include <string.h>
#include "libmf.h"

static char *address_header_names[] = {
	"resent-sender",
	"resent-from",
	"resent-reply-to",
	"sender",
	"from",
	"reply-to",
	"errors-to",
	"disposition-notification-to",
	"to",
	"resent-to",
	"cc",
	"resent-cc",
	"bcc",
	"resent-bcc",
	"apparently-to",
	NULL
};

static int
is_address_header(char const *name)
{
	int i;

	for (i = 0; address_header_names[i]; i++) {
		if (mu_c_strcasecmp(address_header_names[i], name) == 0)
			return 1;
	}
	return 0;
}

static size_t max_col = 78;
	
struct outbuffer {
	char *buf_base;             // Buffer storage.
	size_t buf_size;            // Size of buf_base.
	size_t buf_pos;             // Current position in buf_base.
	size_t buf_line_start;      // Position past the last \n in buffer.
	size_t buf_value_start;     // Start of the actual value.
};

static int
outbuffer_realloc(struct outbuffer *op, size_t more)
{
	while (op->buf_pos + more >= op->buf_size) {
		char *p = mu_2nrealloc(op->buf_base, &op->buf_size, 1);
		if (!p)
			return ENOMEM;
		op->buf_base = p;
	}
	return 0;
}

static int
outbuffer_append_chr(struct outbuffer *op, int c)
{
	int rc;

	if ((rc = outbuffer_realloc(op, 1)) == 0) {
		op->buf_base[op->buf_pos++] = c;
		if (c == '\n')
			op->buf_line_start = op->buf_pos;
	}
	return rc;
}

static int
outbuffer_append(struct outbuffer *op, char const *str, size_t len)
{
	int rc;
	
	if ((rc = outbuffer_realloc(op, len)) == 0) {
		size_t i;
		
		memcpy(op->buf_base + op->buf_pos, str, len);
		op->buf_pos += len;
		for (i = op->buf_pos-1; i > op->buf_line_start; i--)
			if (op->buf_base[i] == '\n') {
				op->buf_line_start = i;
				break;
			}
	}
	return rc;
}

static int
outbuffer_append_ml(struct outbuffer *op, char const *str, size_t len)
{
	int rc = 0;
	
	while (len > 0) {
		char *nlp;

		if ((nlp = memchr(str, '\n', len)) != NULL) {
			size_t n = nlp - str + 1;
			if ((rc = outbuffer_append(op, str, n)) != 0)
				break;
			str += n;
			len -= n;
			if (*str != ' ' && *str != '\t')
				if ((rc = outbuffer_append_chr(op, ' ')) != 0)
					return rc;
		} else {
			if ((rc = outbuffer_append(op, str, len)) != 0)
				break;
			len = 0;
		}
	}
	return rc;
}

static void
outbuffer_init(struct outbuffer *op)
{
	memset(op, 0, sizeof(*op));
}

static int
outbuffer_setup(struct outbuffer *op, char const *hname)
{
	int rc;

	op->buf_pos = 0;
	op->buf_line_start = 0;
	if ((rc = outbuffer_append(op, hname, strlen(hname))) == 0 &&
	    (rc = outbuffer_append(op, ": ", 2)) == 0) {
		op->buf_value_start = op->buf_pos;
	}
	return rc;
}

static void
outbuffer_free(struct outbuffer *op)
{
	free(op->buf_base);
}

static char const *
outbuffer_value_ptr(struct outbuffer *op)
{
	return op->buf_base + op->buf_value_start;
}

static size_t
outbuffer_line_length(struct outbuffer *op)
{
	return op->buf_pos - op->buf_line_start;
}

static int
copy_regular_header(struct outbuffer *op, char const *hname,
		    char const *hvalue)
{
	int rc;

	if ((rc = outbuffer_setup(op, hname)) == 0 &&
	    (rc = outbuffer_append_ml(op, hvalue, strlen(hvalue))) == 0)
		rc = outbuffer_append_chr(op, 0);
	return rc;
}

/* States and symbols */
enum {
	END,    /* end of input */
	OPR,	/* operator */
	ATM,    /* atom */
	DLM,    /* delimiter */
	QST,	/* in a quoted string */
	SPC,    /* eat whitespace */
	NSTATES /* number of machine states */
};

/* State transition table */
static int transtab[NSTATES][NSTATES] = {
   /*   oldst   chtype> END   OPR   ATM   DLM   QST   SPC  */
	/*END*/ {       END,  END,  END,  END,  END,  END  },
        /*OPR*/ {       END,  OPR,  ATM,  DLM,  QST,  SPC  },
        /*ATM*/ {       END,  OPR,  ATM,  DLM,  QST,  SPC  },
        /*DLM*/ {       END,  OPR,  ATM,  DLM,  QST,  SPC  },
        /*QST*/ {       END,  QST,  QST,  QST,  OPR,  QST  },
        /*SPC*/ {       END,  OPR,  ATM,  DLM,  QST,  SPC  },
};

/*
 * The alphabet table below is based on Sendmail's ExtTokenTab, with
 * the following assumptions:
 *
 *   Delimiter characters:  "()<>,;\r\n"
 *   Operators:             ".:%@!^/[]+"
 */ 
static unsigned char alphabet[256] =
{
    /*	nul soh stx etx eot enq ack bel  bs  ht  nl  vt  np  cr  so  si   */
	END,ATM,ATM,ATM,ATM,ATM,ATM,ATM, ATM,SPC,SPC,SPC,SPC,SPC,ATM,ATM,
    /*	dle dc1 dc2 dc3 dc4 nak syn etb  can em  sub esc fs  gs  rs  us   */
	ATM,ATM,ATM,ATM,ATM,ATM,ATM,ATM, ATM,ATM,ATM,ATM,ATM,ATM,ATM,ATM,
    /*  sp  !   "   #   $   %   &   '    (   )   *   +   ,   -   .   /    */
	SPC,OPR,QST,ATM,ATM,OPR,ATM,ATM, SPC,SPC,ATM,OPR,DLM,ATM,OPR,OPR,
    /*	0   1   2   3   4   5   6   7    8   9   :   ;   <   =   >   ?    */
	ATM,ATM,ATM,ATM,ATM,ATM,ATM,ATM, ATM,ATM,OPR,OPR,OPR,ATM,OPR,ATM,
    /*	@   A   B   C   D   E   F   G    H   I   J   K   L   M   N   O    */
	OPR,ATM,ATM,ATM,ATM,ATM,ATM,ATM, ATM,ATM,ATM,ATM,ATM,ATM,ATM,ATM,
    /*  P   Q   R   S   T   U   V   W    X   Y   Z   [   \   ]   ^   _    */
	ATM,ATM,ATM,ATM,ATM,ATM,ATM,ATM, ATM,ATM,ATM,OPR,ATM,OPR,OPR,ATM,
    /*	`   a   b   c   d   e   f   g    h   i   j   k   l   m   n   o    */
	ATM,ATM,ATM,ATM,ATM,ATM,ATM,ATM, ATM,ATM,ATM,ATM,ATM,ATM,ATM,ATM,
    /*  p   q   r   s   t   u   v   w    x   y   z   {   |   }   ~   del  */
	ATM,ATM,ATM,ATM,ATM,ATM,ATM,ATM, ATM,ATM,ATM,ATM,ATM,ATM,ATM,ATM,

    /* Upper 128 characters */ 
	ATM,ATM,ATM,ATM,ATM,ATM,ATM,ATM, ATM,ATM,ATM,ATM,ATM,ATM,ATM,ATM,
	ATM,ATM,ATM,ATM,ATM,ATM,ATM,ATM, ATM,ATM,ATM,ATM,ATM,ATM,ATM,ATM,
	ATM,ATM,ATM,ATM,ATM,ATM,ATM,ATM, ATM,ATM,ATM,ATM,ATM,ATM,ATM,ATM,
	ATM,ATM,ATM,ATM,ATM,ATM,ATM,ATM, ATM,ATM,ATM,ATM,ATM,ATM,ATM,ATM,
	ATM,ATM,ATM,ATM,ATM,ATM,ATM,ATM, ATM,ATM,ATM,ATM,ATM,ATM,ATM,ATM,
	ATM,ATM,ATM,ATM,ATM,ATM,ATM,ATM, ATM,ATM,ATM,ATM,ATM,ATM,ATM,ATM,
	ATM,ATM,ATM,ATM,ATM,ATM,ATM,ATM, ATM,ATM,ATM,ATM,ATM,ATM,ATM,ATM,
	ATM,ATM,ATM,ATM,ATM,ATM,ATM,ATM, ATM,ATM,ATM,ATM,ATM,ATM,ATM,ATM
};

struct scanner {
	const unsigned char *input;    /* Input string. */
	const unsigned char *cur;      /* Current position in the input. */
	int state;                     /* Scanner state. */
	int rdlm;                      /* True (1) if SCAN_DLM should be
					  returned on the next call. */
	               /* Output fields: */
	const unsigned char *tok_ptr;  /* Start of token. */
	size_t tok_len;                /* Token length. */
};

/* Scanner return codes */
enum scan_code {
	SCAN_OK,    /* OK, next address scanned successfully */
	SCAN_DLM,   /* A delimiter (",") is returned */
	SCAN_END    /* End of input */
};

static int
scan_next(struct scanner *scn)
{
	int backslash = 0;
	int route_syntax = 0;
	int cmntcnt = 0;
	int anglecnt = 0;
	int la;
	
	if (scn->state == END)
		return SCAN_END;

	if (scn->state == DLM && scn->rdlm) {
		scn->tok_ptr = scn->cur - 1;
		scn->tok_len = 1;
		scn->rdlm = 0;
		return SCAN_DLM;
	}

	/* Skip leading whitespace */
	for (;;) {
		if (*scn->cur == 0) {
			scn->state = END;
			return SCAN_END;
		} else if (mu_isspace(*scn->cur))
			scn->cur++;
		else
			break;
	}

	scn->tok_ptr = scn->cur;

	for (;;) {
		if ((la = *scn->cur) == 0) {
			if (scn->state == QST) {
				// Unbalanced '\"'
				la = '"';
			} else if (cmntcnt > 0)	{
				// Unbalanced '('
				la = ')';
			} else if (anglecnt > 0) {
				// Unbalanced '<'
				la = '>';
			}
		} else
			scn->cur++;
			
		if (alphabet[la] == DLM && scn->state != QST && cmntcnt == 0 && anglecnt > 0) {
			/* special case for better error management */
			if (!route_syntax) {
				// Unbalanced '<'
				la = '>';
				scn->cur--;
			}
		}

		if (backslash) {
			backslash = 0;
			
			if (cmntcnt > 0 || la != '!' || scn->state == QST) {
				continue;
			}
		}

		if (la == '\\') {
			backslash = 1;
		} else if (scn->state == QST) {
			/* nothing */;
		} else if (la == '(') {
			cmntcnt++;
			continue;
		} else if (la == ')') {
			if (cmntcnt <= 0) {
				// Unbalanced ')'
				continue;
			} else
				cmntcnt--;
		} else if (cmntcnt > 0)	{
			continue;
		} else if (la == '<') {
			const char *ptr = (const char *)scn->cur;

			anglecnt++;
			while (mu_isascii(*ptr) && mu_isspace(*ptr))
				ptr++;
			if (*ptr == '@')
				route_syntax = 1;
		} else if (la == '>') {
			if (anglecnt <= 0) {
				// Unbalanced '>'
				continue;
			} else
				anglecnt--;
			route_syntax = 0;
		}
		
		scn->state = transtab[scn->state][alphabet[la]];
		scn->rdlm = (scn->state == DLM);
		if (scn->state == END || (scn->state == DLM && anglecnt == 0))
			break;
	}

	scn->tok_len = scn->cur - scn->tok_ptr;
	if (scn->state != END)
		scn->tok_len--;
	/* Can't use alphabet[], because ( and ) are marked as SPC */
	while (scn->tok_len > 0 && mu_isspace(scn->tok_ptr[scn->tok_len-1])) {
		scn->tok_len--;
	}	
	
	return SCAN_OK;
}

static int
scan_first(struct scanner *scn, char const *input)
{
	memset(scn, 0, sizeof(*scn));
	scn->cur = scn->input = (unsigned char *)input;
	scn->state = ATM;
	scn->rdlm = 1;
	return scan_next(scn);
}

static char const *
token_ptr(struct scanner *scn)
{
	return (char const*) scn->tok_ptr;
}

static size_t
token_length(struct scanner *scn)
{
	return scn->tok_len;
}

static int
copy_address_header(struct outbuffer *op, char const *hname,
		    char const *hvalue)
{
	struct scanner scn;
	int rc = 0;
	int res;

	if ((rc = outbuffer_setup(op, hname)) != 0)
		return rc;

	res = scan_first(&scn, hvalue);
	if (res == SCAN_OK) {
		if ((rc = outbuffer_append(op, token_ptr(&scn), token_length(&scn))) != 0)
			return rc;
		while ((res = scan_next(&scn)) == SCAN_DLM) {
			size_t col;
			
			if ((res = scan_next(&scn)) == SCAN_END)
				break;
			col = outbuffer_line_length(op) + token_length(&scn) + 2;
			if (col > max_col) {
				if ((rc = outbuffer_append(op, ",\n        ", 10)) != 0)
					return rc;
			} else {
				if ((rc = outbuffer_append(op, ", ", 2)) != 0)
					return rc;
			}
			if ((rc = outbuffer_append(op, token_ptr(&scn), token_length(&scn))) != 0)
				return rc;
		}
		if (res != SCAN_END)
			return MU_ERR_PARSE;
	} else if (res != SCAN_END)
		return MU_ERR_PARSE;

	return outbuffer_append_chr(op, 0);
}

static int
copy_body(mu_message_t imsg, mu_message_t omsg)
{
	int rc;
	mu_body_t b;
	mu_stream_t istr, ostr;

	if ((rc = mu_message_get_body(imsg, &b)) == 0 &&
	    (rc = mu_body_get_streamref(b, &istr)) == 0) {
		if ((rc = mu_message_get_body(omsg, &b)) == 0 &&
		    (rc = mu_body_get_streamref(b, &ostr)) == 0) {
			rc = mu_stream_copy(ostr, istr, 0, NULL);
			mu_stream_unref(ostr);
		}
		mu_stream_unref(istr);
	}
	return rc;
}

int
sendmail_address_normalize(mu_message_t imsg, mu_message_t *ret_msg)
{
	int rc;
	mu_message_t omsg;
	mu_header_t ihdr = NULL, ohdr = NULL;
	size_t nhdr, i;
	struct outbuffer obuf;

	if ((rc = mu_message_create(&omsg, NULL)) != 0)
		return rc;
	
	if ((rc = mu_message_get_header(imsg, &ihdr)) == 0 &&
	    (rc = mu_message_get_header(omsg, &ohdr)) == 0 &&
	    (rc = mu_header_get_field_count(ihdr, &nhdr)) == 0) {
		outbuffer_init(&obuf);
	
		for (i = 1; i <= nhdr; i++) {
			char const *hname, *hval;
			if ((rc = mu_header_sget_field_name(ihdr, i, &hname)) != 0)
				break;
			if ((rc = mu_header_sget_field_value(ihdr, i, &hval)) != 0)
				break;

			if (is_address_header(hname)) {
				rc = copy_address_header(&obuf, hname, hval);
			} else {
				rc = copy_regular_header(&obuf, hname, hval);
			}
			if (rc == 0)
				rc = mu_header_append(ohdr, hname, outbuffer_value_ptr(&obuf));
			if (rc)
				break;
		}
		outbuffer_free(&obuf);
	}
	
	if (rc == 0)
		rc = copy_body(imsg, omsg);

	if (rc == 0)
		*ret_msg = omsg;
	else
		mu_message_destroy (&omsg, NULL);
	return rc;
}

