/*
** spost.c -- feed messages to sendmail
**
** This is a simpler, faster, replacement for "post" for use
** when "sendmail" is the transport system.
**
** This code is Copyright (c) 2002, by the authors of nmh.  See the
** COPYRIGHT file in the root directory of the nmh distribution for
** complete copyright information.
*/

#include <h/mh.h>
#include <signal.h>
#include <h/addrsbr.h>
#include <h/aliasbr.h>
#include <h/dropsbr.h>
#include <h/tws.h>
#include <h/utils.h>
#include <unistd.h>
#include <locale.h>
#include <sysexits.h>
#include <errno.h>

#define MAX_SM_FIELD 1476  /* < largest hdr field sendmail will accept */

static struct swit switches[] = {
#define VERBSW  0
	{ "verbose", 0 },
#define NVERBSW  1
	{ "noverbose", 2 },
#define VERSIONSW  2
	{ "Version", 0 },
#define HELPSW  3
	{ "help", 0 },
#define DEBUGSW  4
	{ "debug", -5 },
#define DISTSW  5
	{ "dist", -4 },  /* interface from dist */
	{ NULL, 0 }
};

char *version=VERSION;

/* flags for headers->flags */
#define HNOP  0x0000  /* just used to keep .set around */
#define HBAD  0x0001  /* bad header - don't let it through */
#define HADR  0x0002  /* header has an address field */
#define HSUB  0x0004  /* Subject: header */
#define HTRY  0x0008  /* try to send to addrs on header */
#define HBCC  0x0010  /* don't output this header */
#define HFCC  0x0020  /* FCC: type header */
#define HIGN  0x0040  /* ignore this header */
#define HDCC  0x0080  /* DCC: type header */

/* flags for headers->set */
#define MFRM  0x0001  /* we've seen a From: */
#define MDAT  0x0002  /* we've seen a Date: */
#define MRFM  0x0004  /* we've seen a Resent-From: */
#define MVIS  0x0008  /* we've seen sighted addrs */
#define MINV  0x0010  /* we've seen blind addrs */
#define MRDT  0x0020  /* we've seen a Resent-Date: */
#define MFMM  0x0040  /* The Mail is From a Alternative-Mailbox Addresse */

struct headers {
	char *value;
	unsigned int flags;
	unsigned int set;
};

static struct headers NHeaders[] = {
	{ "Return-Path", HBAD, 0 },
	{ "Received", HBAD, 0 },
	{ "Reply-To", HADR, 0 },
	{ "From", HADR, MFRM },
	{ "Sender", HADR|HBAD, 0 },
	{ "Date", HNOP, MDAT },
	{ "Subject", HSUB, 0 },
	{ "To", HADR|HTRY, MVIS },
	{ "Cc", HADR|HTRY, MVIS },
	{ "Dcc", HADR|HTRY|HDCC, MINV },
	{ "Bcc", HADR|HTRY|HBCC, MINV },
	{ "Message-Id", HBAD, 0 },
	{ "Fcc", HFCC, 0 },
	{ "Envelope-From", HIGN, 0 },
	{ NULL, 0, 0 }
};

static struct headers RHeaders[] = {
	{ "Resent-Reply-To",   HADR, 0 },
	{ "Resent-From", HADR, MRFM },
	{ "Resent-Sender", HADR|HBAD, 0 },
	{ "Resent-Date", HNOP, MRDT },
	{ "Resent-Subject", HSUB, 0 },
	{ "Resent-To", HADR|HTRY, MVIS },
	{ "Resent-Cc", HADR|HTRY, MVIS },
	{ "Resent-Dcc", HADR|HTRY|HDCC, MINV },
	{ "Resent-Bcc", HADR|HTRY|HBCC, MINV },
	{ "Resent-Message-Id", HBAD, 0 },
	{ "Resent-Fcc", HFCC, 0 },
	{ "Reply-To", HADR, 0 },
	{ "Fcc", HIGN, 0 },
	{ "Envelope-From", HIGN, 0 },
	{ NULL, 0, 0 }
};


static int badmsg = 0;
static int verbose = 0;
static int debug = 0;
static int aliasflg = 0;  /* if going to process aliases */

static unsigned msgflags = 0;  /* what we've seen */

static enum {
	normal, resent
} msgstate = normal;

static char *tmpfil;

static char *subject = NULL;  /* the subject field for BCC'ing */
static struct mailname *from = NULL;  /* the from field for BCC'ing */
static char fccs[BUFSIZ] = "";
struct mailname *bccs = NULL;  /* list of the bcc recipients */
struct mailname *recipients = NULL;  /* list of the recipients */
size_t recipientsc = 0;
struct mailname *sender = NULL;

static struct headers *hdrtab;  /* table for the message we're doing */
static FILE *out;  /* output (temp) file */

/*
** static prototypes
*/
static void putfmt(char *, char *, FILE *);
static void finish_headers(FILE *);
static int get_header(char *, struct headers *);
static void putadr(char *, struct mailname *);
static int putone(char *, int, int);
static void process_fcc(char *);
static void fcc(char *, char *);
static void process_bccs(char *);
static size_t do_aliasing(struct mailname *, struct mailname **);


int
main(int argc, char **argv)
{
	enum state state;
	struct field f = {{0}};
	int compnum;
	char *cp, *msg = NULL, **argp, **arguments;
	char **sargv, buf[BUFSIZ];
	FILE *in;

	setlocale(LC_ALL, "");
	invo_name = mhbasename(argv[0]);

	context_read();

	arguments = getarguments(invo_name, argc, argv, 0);
	argp = arguments;

	while ((cp = *argp++)) {
		if (*cp == '-') {
			switch (smatch(++cp, switches)) {
			case AMBIGSW:
				ambigsw(cp, switches);
				exit(EX_USAGE);
			case UNKWNSW:
				adios(EX_USAGE, NULL, "-%s unknown", cp);

			case HELPSW:
				snprintf(buf, sizeof(buf),
						"%s [switches] file",
						invo_name);
				print_help(buf, switches, 1);
				exit(argc == 2 ? EX_OK : EX_USAGE);
			case VERSIONSW:
				print_version(invo_name);
				exit(argc == 2 ? EX_OK : EX_USAGE);

			case DEBUGSW:
				debug++;
				continue;

			case DISTSW:
				msgstate = resent;
				continue;

			case VERBSW:
				verbose++;
				continue;
			case NVERBSW:
				verbose = 0;
				continue;
			}
		}
		if (msg) {
			adios(EX_USAGE, NULL, "only one message at a time!");
		} else {
			msg = cp;
		}
	}

	if (!msg) {
		adios(EX_USAGE, NULL, "usage: %s [switches] file", invo_name);
	}

	if ((in = fopen(msg, "r")) == NULL) {
		adios(EX_IOERR, msg, "unable to open");
	}

	if (debug) {
		verbose++;
		out = stdout;
	} else {
		tmpfil = mh_xstrdup(m_mktemp2("/tmp/", invo_name, NULL, &out));
	}

	/* check for "Aliasfile:" profile entry */
	if ((cp = context_find("Aliasfile"))) {
		char *dp, **ap; 

		aliasflg = 1;
		for (ap=brkstring(dp=mh_xstrdup(cp), " ", "\n"); ap && *ap;
				ap++) {
			if ((state = alias(etcpath(*ap))) != AK_OK) {
				adios(EX_IOERR, NULL, "aliasing error in file %s: %s",
						*ap, akerror(state));
			}
		}
	}


	hdrtab = (msgstate == normal) ? NHeaders : RHeaders;

	for (compnum = 1, state = FLD2;;) {
		switch (state = m_getfld2(state, &f, in)) {
		case FLD2:
			compnum++;
			putfmt(f.name, f.value, out);
			continue;

		case BODY2:
			finish_headers(out);
			fprintf(out, "\n%s", f.value);
			while ((state = m_getfld2(state, &f, in)) == BODY2) {
				if (f.valuelen > NAMESZ+1 || (!f.crlf && f.valuelen > NAMESZ)) {
					adios(EX_DATAERR, NULL, "Body contains a to long line");
				}
				fputs(f.value, out);
			}
			break;

		case FILEEOF2:
			finish_headers(out);
			break;

		case LENERR2:
		case FMTERR2:
		case IOERR2:
			adios(EX_DATAERR, NULL, "message format error in component #%d",
					compnum);

		default:
			adios(EX_SOFTWARE, NULL, "getfld() returned %d", state);
		}
		break;
	}
	fclose(in);

	if (state != FILEEOF2) {
		adios(EX_IOERR, "m_getfld2", "Error while reading body");
	}

	if (debug) {
		struct mailname *i = recipients;
		/* stop here */
		puts("----EOM----");
		while (i) {
			fputs(i->m_mbox, stdout);
			if (i->m_host) {
				fputs("@", stdout);
				fputs(i->m_host, stdout);
			}
			fputs("\n", stdout);
			i = i->m_next;
			mnfree(recipients);
			recipients = i;
		}
		exit(EX_OK);
	}

	fclose(out);

	/* process Fcc */
	if (*fccs) {
		fcc(tmpfil, fccs);
	}

	if (bccs) {
		process_bccs(tmpfil);
		if (!(msgflags & MVIS)) {
			/* only Bcc rcpts: we're finished here */
			unlink(tmpfil);
			exit(EX_OK);
		}
	}

	/*
	** re-open the temp file, unlink it and exec sendmail, giving it
	** the msg temp file as std in.
	*/
	if (!freopen(tmpfil, "r", stdin)) {
		adios(EX_IOERR, tmpfil, "can't reopen for sendmail");
	}
	unlink(tmpfil);

	if (recipientsc == 0) {
		adios(EX_DATAERR, NULL, "message has no recipients");
	}

	sargv = mh_xcalloc(recipientsc + 4, sizeof(char **));

	argp = sargv;
	*argp++ = "send-mail";
	*argp++ = "-i";  /* don't stop on "." */
	if (verbose) {
		*argp++ = "-v";
	}

	while (recipients != NULL) {
		cp = mh_xstrdup(recipients->m_mbox);
		if (recipients->m_host) {
			cp = add("@", cp);
			cp = add(recipients->m_host, cp);
		}
		*argp++ = cp;
		cp = NULL;
		recipients = recipients->m_next;
	}
	*argp = NULL;
	execvp(sendmail, sargv);

	if (errno == E2BIG) {
		adios(EX_DATAERR, sendmail, "too much arguments, probably to much recipients");
	}

	adios(EX_OSERR, sendmail, "can't exec");
	return -1;
}


/* DRAFT GENERATION */

static void
putfmt(char *name, char *str, FILE *out)
{
	int i;
	struct headers *hdr;
	struct mailname addr_start, *addr_end;
	size_t addrc = 0;
	ssize_t ret;

	addr_end = &addr_start;
	addr_end->m_next = NULL;

	/* remove leading whitespace */
	while (*str==' ' || *str=='\t') {
		str++;
	}

	if ((i = get_header(name, hdrtab)) == NOTOK) {
		/* no header we would care for */
		if (mh_strcasecmp(name, attach_hdr)==0) {
			return;
		}
		if (mh_strcasecmp(name, sign_hdr)==0) {
			return;
		}
		if (mh_strcasecmp(name, enc_hdr)==0) {
			return;
		}
		/* push it through */
		fprintf(out, "%s: %s", name, str);
		return;
	}
	/* it's one of the interesting headers */
	hdr = &hdrtab[i];

	if (hdr->flags & HIGN || strcmp(str, "\n")==0) {
		return;
	}

	if (hdr->flags & HBAD) {
		advise(NULL, "illegal header line -- %s:", name);
		badmsg++;
		return;
	}

	msgflags |= hdr->set;

	if (hdr->flags & HFCC) {
		process_fcc(str);
		return;
	}

	if (hdr->flags & HSUB) {
		subject = mh_xstrdup(str);
	}

	if (!(hdr->flags & HADR)) {
		fprintf(out, "%s: %s", name, str);
		return;
	}

	if ((ret = getmboxes(str, &addr_end)) < 0) {
		adios(EX_DATAERR, NULL, "can't parse address: %s", str);
	}

	addrc += ret;

	if (aliasflg) {
		addrc += do_aliasing(&addr_start, &addr_end);
	}

	if (hdr->flags & HBCC) {
		addr_end->m_next = bccs;
		bccs = addr_start.m_next;
		return;
	}

	if (hdr->set & MFRM) {
		struct mailname *mp = NULL;
		struct mailname *my = NULL;

		/* needed because the address parser holds global state */
		ismymbox(NULL);

		for (mp = addr_start.m_next; mp; mp = mp->m_next) {
			if (ismymbox(mp)) {
				msgflags |= MFMM;
				if (my == NULL) {
					from = my = mp;
				}
			}
		}

		if (addrc > 1) {
			sender = my;
		}
	}

	if (!(hdr->flags & HDCC)) {
		putadr(name, addr_start.m_next);
	}

	if (hdr->flags & HTRY) {
		addr_end->m_next = recipients;
		recipients = addr_start.m_next;
		recipientsc += i;
	}
}


/*
** Add yet missing headers.
*/
static void
finish_headers(FILE *out)
{
	char *cp;
	char from[BUFSIZ];  /* my network address */
	char signature[BUFSIZ];  /* my signature */
	char *resentstr = (msgstate == resent) ? "Resent-" : "";

	if (!(msgflags & MDAT)) {
		fprintf(out, "%sDate: %s\n", resentstr, dtimenow());
	}

	if (sender != NULL) {
		snprintf(signature, sizeof(signature), "%s", sender->m_text);
	} else if ((cp = context_find("Default-From")) != NULL) {
		snprintf(signature, sizeof(signature), "%s", cp);
	} else {
		snprintf(from, sizeof(from), "%s@%s", getusername(), LocalName());
		if ((cp = getfullname()) && *cp) {
			snprintf(signature, sizeof(signature), "%s <%s>", cp, from);
		} else {
			snprintf(signature, sizeof(signature), "%s", from);
		}
	}
	if (!(msgflags & MFRM)) {
		fprintf(out, "%sFrom: %s\n", resentstr, signature);
	} else {
		/*
		** Add a Sender: header because the From: header could
		** be fake or contain multiple addresses.
		*/
		if (!(msgflags & MFMM) || sender != NULL) {
			fprintf(out, "%sSender: %s\n", resentstr, signature);
		}
	}
	if (!(msgflags & MVIS)) {
		fprintf(out, "%sBcc: undisclosed-recipients:;\n", resentstr);
	}
	if (badmsg) {
		unlink(tmpfil);
		adios(EX_DATAERR, NULL, "re-format message and try again");
	}
}


/*
** Return index of the requested header in the table, or NOTOK if missing.
*/
static int
get_header(char *header, struct headers *table)
{
	struct headers *h;

	for (h=table; h->value; h++) {
		if (mh_strcasecmp(header, h->value)==0) {
			return (h - table);
		}
	}
	return NOTOK;
}


/*
** output the address list for header "name".  The address list
** is a linked list of mailname structs.  "nl" points to the head
** of the list.  Alias substitution should be done on nl.
*/
static void
putadr(char *name, struct mailname *nl)
{
	struct mailname *mp;
	char *cp;
	int linepos;
	int namelen;

	fprintf(out, "%s: ", name);
	namelen = strlen(name) + 2;
	linepos = namelen;

	for (mp = nl; mp; ) {
		if (linepos > MAX_SM_FIELD) {
			fprintf(out, "\n%s: ", name);
			linepos = namelen;
		}
		if (mp->m_ingrp) {
			if (mp->m_gname != NULL) {
				cp = mh_xstrdup(mp->m_gname);
				cp = add(";", cp);
				linepos = putone(cp, linepos, namelen);
				mh_free0(&cp);
				cp = NULL;
			}
		} else {
			linepos = putone(mp->m_text, linepos, namelen);
		}
		mp = mp->m_next;
	}
	putc('\n', out);
}

static int
putone(char *adr, int pos, int indent)
{
	int len;
	static int linepos;

	len = strlen(adr);
	if (pos == indent) {
		linepos = pos;
	} else if (linepos+len > OUTPUTLINELEN) {
		fprintf(out, ",\n%*s", indent, "");
		linepos = indent;
		pos += indent + 2;
	} else {
		fputs(", ", out);
		linepos += 2;
		pos += 2;
	}
	fputs(adr, out);

	linepos += len;
	return (pos+len);
}


static void
process_fcc(char *str)
{
	char *cp, *pp;
	int state = 0;

	if (strlen(str)+strlen(fccs) > sizeof fccs /2) {
		adios(EX_DATAERR, NULL, "Too much Fcc data");
	}
	/* todo: better have three states: SEPARATOR, PLUS, WORD */
	for (cp=pp=str; *cp; cp++) {
		switch (*cp) {
		case ' ':
		case '\t':
		case '\n':
		case ',':
			if (state != 0) {
				state = 0;
				*cp = '\0';
				if (*pp=='+' || *pp=='@') {
					strcat(fccs, " ");
				} else {
					strcat(fccs, " +");
				}
				strcat(fccs, pp);
			}
			break;
		default:
			if (state == 0) {
				state = 1;
				pp = cp;
			}
			break;
		}
	}
}


static void
fcc(char *file, char *folders)
{
	int status;
	char cmd[BUFSIZ];

	if (verbose) {
		printf("%sFcc: %s\n", msgstate == resent ? "Resent-" : "",
				folders);
		fflush(stdout);
	}
	if (100+strlen(file)+strlen(folders) > sizeof cmd) {
		adios(EX_DATAERR, NULL, "Too much Fcc data");
	}
	/* hack: read from /dev/null and refile(1) won't question us */
	snprintf(cmd, sizeof cmd, "</dev/null refile -link -file '%s' %s",
			file, folders);
	status = system(cmd);
	if (status == -1) {
		fprintf(stderr, "Skipped %sFcc %s: unable to system().\n",
				msgstate == resent ? "Resent-" : "", folders);
	} else if (status != 0) {
		fprintf(stderr, "%sFcc %s: Problems occurred.\n",
				msgstate == resent ? "Resent-" : "", folders);
	}
}


/* BCC GENERATION */

static void
process_bccs(char *origmsg)
{
	char *bccdraft = NULL;
	struct mailname *mp = NULL;
	FILE *out = NULL;

	for (mp=bccs; mp; mp=mp->m_next) {
		bccdraft = mh_xstrdup(m_mktemp2("/tmp/", invo_name, NULL, &out));
		fprintf(out, "To: %s\n", mp->m_text);
		if (from) {
			fprintf(out, "From: %s\n", from->m_text);
		}
		fprintf(out, "Subject: [BCC] %s", subject ? subject : "");
		fprintf(out, "%s: %s\n", attach_hdr, origmsg);
		fprintf(out, "------------\n");
		fclose(out);

		if (execprogl("send", "send", bccdraft, (char *)NULL) != 0) {
			admonish(invo_name, "Problems to send Bcc to %s",
					mp->m_text);
			unlink(bccdraft);
		}
	}
}

/*
 * Do aliasing on a mailname linked list
 * Begin at start->m_next
 * End if m_next == NULL
 * **end is set to the new end.
 * Return the number of new mainames in the list
 */

static size_t
do_aliasing(struct mailname *start, struct mailname **end)
{
	struct mailname *prev, *cur;
	char *cp;
	size_t i = 0;
	ssize_t e;

	prev = start;
	cur = prev->m_next;

	while (cur != NULL) {
		if (cur->m_nohost) {
			cp = akvalue(cur->m_mbox);
			if (strcmp(cp, cur->m_mbox) != 0) {
				prev->m_next = cur->m_next;
				if ((e = getmboxes(cp, &prev)) < 0) {
					goto error;
				}
				i += e;
				i -= 1;
				mnfree(cur);
			} else {
				prev = cur;
			}
		} else {
			prev = cur;
		}
		cur = prev->m_next;
	}
	*end = prev;
	return i;
error:
	adios(EX_CONFIG, NULL, "can't parse alias %s: %s", cur->m_mbox, cp);
	return 0; /* not reached */
}
