/*
** mhbuild.c -- expand/translate MIME composition files
**
** 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.
*/

/*
** This code was originally part of mhn.c.  I split it into
** a separate program (mhbuild.c). But the code still has some of
** the mhn.c code in it.  This program needs additional
** streamlining and removal of unneeded code.
*/

#include <h/mh.h>
#include <fcntl.h>
#include <errno.h>
#include <h/tws.h>
#include <h/mime.h>
#include <h/mhparse.h>
#include <h/utils.h>
#include <unistd.h>
#include <ctype.h>
#include <sys/stat.h>
#include <locale.h>
#include <sysexits.h>

#ifdef HAVE_SYS_TIME_H
# include <sys/time.h>
#endif
#include <time.h>

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 },
	{ NULL, 0 }
};

char *version=VERSION;

/*
** Directory to place tmp files.  This must
** be set before these routines are called.
*/
char *tmp;

pid_t xpid = 0;

static char prefix[] = "----- =_aaaaaaaaaa";


/*
** prototypes
*/

/* mhmisc.c */
int make_intermediates(char *);
void content_error(char *, CT, char *, ...);

/* mhfree.c */
void free_content(CT);
void free_ctinfo(CT);
void free_encoding(CT, int);

/*
** static prototypes
*/
static int init_decoded_content(CT);
static char *fgetstr(char *, int, FILE *);
static int user_content(FILE *, char *, char *, CT *);
static void set_id(CT, int);
static int compose_content(CT);
static int scan_content(CT);
static int build_headers(CT);
static CT build_mime(char *);


int debugsw = 0;
int verbosw = 0;

/*
** Temporary files
*/
static char infile[BUFSIZ];
static int unlink_infile  = 0;

static char outfile[BUFSIZ];
static int unlink_outfile = 0;

void unlink_done();

/* mhoutsbr.c */
int output_message(CT, char *);
int output_message_fp(CT, FILE *, char*);

/* mhmisc.c */
void set_endian(void);

/* mhfree.c */
void free_content(CT);


int
main(int argc, char **argv)
{
	char *cp, buf[BUFSIZ];
	char buffer[BUFSIZ], *compfile = NULL;
	char **argp, **arguments;
	CT ct;
	FILE *fp = NULL;
	FILE *fp_out = NULL;

	if (atexit(unlink_done) != 0) {
		adios(EX_OSERR, NULL, "atexit failed");
	}

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

	/* read user profile/context */
	context_read();

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

	while ((cp = *argp++)) {
		if (cp[0] == '-' && cp[1] == '\0') {
			if (compfile)
				adios(EX_USAGE, NULL, "cannot specify both standard input and a file");
			else
				compfile = cp;
			verbosw = 0;  /* turn off -verbose listings */
			break;
		}
		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 VERBSW:
				verbosw++;
				continue;
			case NVERBSW:
				verbosw = 0;
				continue;
			case DEBUGSW:
				debugsw = 1;
				continue;
			}
		}
		if (compfile)
			adios(EX_USAGE, NULL, "only one composition file allowed");
		else
			compfile = cp;
	}

	set_endian();

	/*
	** Check if we've specified an additional profile
	*/
	if ((cp = getenv("MHBUILD"))) {
		if ((fp = fopen(cp, "r"))) {
			readconfig((struct node **) 0, fp, cp, 0);
			fclose(fp);
		} else {
			admonish("", "unable to read $MHBUILD profile (%s)",
					cp);
		}
	}

	/*
	** Read the standard profile setup
	*/
	if ((fp = fopen(cp = etcpath("mhn.defaults"), "r"))) {
		readconfig((struct node **) 0, fp, cp, 0);
		fclose(fp);
	}

	/*
	** Check for storage directory.  If defined, we
	** will store temporary files there.  Else we
	** store them in standard nmh directory.
	*/
	if ((cp = context_find(nmhstorage)) && *cp)
		tmp = concat(cp, "/", invo_name, NULL);
	else
		tmp = mh_xstrdup(toabsdir(invo_name));

	/* Check if we have a file to process */
	if (!compfile)
		adios(EX_USAGE, NULL, "need to specify a %s composition file",
				invo_name);

	/*
	** Process the composition file from standard input.
	*/
	if (compfile[0] == '-' && compfile[1] == '\0') {
		/* copy standard input to temporary file */
		strncpy(infile, m_mktemp(invo_name, NULL, &fp),
				sizeof(infile));
		while (fgets(buffer, BUFSIZ, stdin))
			fputs(buffer, fp);
		fclose(fp);
		unlink_infile = 1;

		/* build the content structures for MIME message */
		ct = build_mime(infile);

		/* output MIME message to this temporary file */
		strncpy(outfile, m_mktemp(invo_name, NULL, &fp_out),
				sizeof(outfile));
		unlink_outfile = 1;

		/* output the message */
		output_message_fp(ct, fp_out, outfile);
		fclose(fp_out);

		/* output the temp file to standard output */
		if ((fp = fopen(outfile, "r")) == NULL)
			adios(EX_IOERR, outfile, "unable to open");
		while (fgets(buffer, BUFSIZ, fp))
			fputs(buffer, stdout);
		fclose(fp);

		unlink(infile);
		unlink_infile = 0;

		unlink(outfile);
		unlink_outfile = 0;

		free_content(ct);
		exit(EX_OK);
	}

	/*
	** Process the composition file from a file.
	*/

	/* build the content structures for MIME message */
	ct = build_mime(compfile);

	/* output MIME message to this temporary file */
	strncpy(outfile, m_mktemp2(compfile, invo_name, NULL, &fp_out),
			sizeof(outfile));
	unlink_outfile = 1;

	/* output the message */
	output_message_fp(ct, fp_out, outfile);
	fclose(fp_out);

	/* Rename composition draft */
	snprintf(buffer, sizeof(buffer), "%s.orig", compfile);
	if (rename(compfile, buffer) == NOTOK) {
		adios(EX_IOERR, buffer, "unable to rename draft %s to", compfile);
	}

	/* Rename output file to take its place */
	if (rename(outfile, compfile) == NOTOK) {
		advise(compfile, "unable to rename output %s to", outfile);
		rename(buffer, compfile);
		exit(EX_IOERR);
	}
	unlink_outfile = 0;

	free_content(ct);
	return 0;
}


void
unlink_done()
{
	/*
	** Check if we need to remove stray temporary files.
	*/
	if (unlink_infile) {
		unlink(infile);
	}
	if (unlink_outfile) {
		unlink(outfile);
	}
}

/*
** Main routine for translating composition file
** into valid MIME message.  It translates the draft
** into a content structure (actually a tree of content
** structures).  This message then can be manipulated
** in various ways, including being output via
** output_message().
*/
static CT
build_mime(char *infile)
{
	enum state state;
	struct field f = {{0}};
	int compnum;
	char buf[BUFSIZ];
	char *cp, *np, *vp;
	struct multipart *m;
	struct part **pp;
	CT ct;
	FILE *in;
	HF hp;

	umask(~m_gmprot());

	/* open the composition draft */
	if ((in = fopen(infile, "r")) == NULL) {
		adios(EX_IOERR, infile, "unable to open for reading");
	}

	/*
	** Allocate space for primary (outside) content
	*/
	ct = mh_xcalloc(1, sizeof(*ct));

	/*
	** Allocate structure for handling decoded content
	** for this part.  We don't really need this, but
	** allocate it to remain consistent.
	*/
	init_decoded_content(ct);

	/*
	** Parse some of the header fields in the composition
	** draft into the linked list of header fields for
	** the new MIME message.
	*/
	for (compnum = 1, state = FLD2;;) {
		switch (state = m_getfld2(state, &f, in)) {
		case FLD2:
			compnum++;

			/* abort if draft has Mime-Version header field */
			if (!mh_strcasecmp(f.name, VRSN_FIELD)) {
				adios(EX_CONFIG, NULL, "draft shouldn't contain %s: field", VRSN_FIELD);
			}

			/*
			** abort if draft has Content-Transfer-Encoding
			** header field
			*/
			if (!mh_strcasecmp(f.name, ENCODING_FIELD)) {
				adios(EX_CONFIG, NULL, "draft shouldn't contain %s: field", ENCODING_FIELD);
			}

			/* ignore any Content-Type fields in the header */
			if (!mh_strcasecmp(f.name, TYPE_FIELD)) {
				continue;
			}

			/* add the header data to the list */
			add_header(ct, mh_xstrdup(f.name), mh_xstrdup(f.value));

			continue;

		case BODY2:
			fseek(in, (long) (-strlen(f.value)), SEEK_CUR);
			break;

		case FILEEOF2:
			adios(EX_CONFIG, NULL, "draft has empty body -- no directives!");
			/* NOTREACHED */

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

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

	/*
	** Iterate through the list of headers and call the function to
	** MIME-ify them if required.
	*/
	for (hp = ct->c_first_hf; hp != NULL; hp = hp->next) {
		if (encode_rfc2047(hp->name, &hp->value, NULL)) {
			adios(EX_DATAERR, NULL, "Unable to encode header \"%s\"", hp->name);
		}
	}

	/*
	** Now add the MIME-Version header field
	** to the list of header fields.
	*/
	np = mh_xstrdup(VRSN_FIELD);
	vp = concat(" ", VRSN_VALUE, "\n", NULL);
	add_header(ct, np, vp);

	/*
	** We initially assume we will find multiple contents in the
	** draft.  So create a multipart/mixed content to hold everything.
	** We can remove this later, if it is not needed.
	*/
	if (get_ctinfo("multipart/mixed", ct, 0) == NOTOK) {
		exit(EX_DATAERR);
	}
	ct->c_type = CT_MULTIPART;
	ct->c_subtype = MULTI_MIXED;
	ct->c_file = mh_xstrdup(infile);

	m = (struct multipart *) mh_xcalloc(1, sizeof(*m));
	ct->c_ctparams = (void *) m;
	pp = &m->mp_parts;

	/*
	** read and parse the composition file
	** and the directives it contains.
	*/
	while (fgetstr(buf, sizeof(buf) - 1, in)) {
		struct part *part;
		CT p;

		if (user_content(in, infile, buf, &p) == DONE) {
			admonish(NULL, "ignoring spurious #end");
			continue;
		}
		if (!p)
			continue;

		part = mh_xcalloc(1, sizeof(*part));
		*pp = part;
		pp = &part->mp_next;
		part->mp_part = p;
	}

	/*
	** close the composition draft since
	** it's not needed any longer.
	*/
	fclose(in);

	/* check if any contents were found */
	if (!m->mp_parts)
		adios(EX_OSERR, NULL, "no content directives found");

	/*
	** If only one content was found, then remove and
	** free the outer multipart content.
	*/
	if (!m->mp_parts->mp_next) {
		CT p;

		p = m->mp_parts->mp_part;
		m->mp_parts->mp_part = NULL;

		/* move header fields */
		p->c_first_hf = ct->c_first_hf;
		p->c_last_hf = ct->c_last_hf;
		ct->c_first_hf = NULL;
		ct->c_last_hf = NULL;

		free_content(ct);
		ct = p;
	} else {
		set_id(ct, 1);
	}

	/*
	** Fill out, or expand directives.  Parse and execute
	** commands specified by profile composition strings.
	*/
	compose_content(ct);

	if ((cp = strchr(prefix, 'a')) == NULL)
		adios(EX_SOFTWARE, NULL, "internal error(4)");

	/*
	** Scan the contents.  Choose a transfer encoding, and
	** check if prefix for multipart boundary clashes with
	** any of the contents.
	*/
	while (scan_content(ct) == NOTOK) {
		if (*cp < 'z') {
			(*cp)++;
		} else {
			if (*++cp == 0)
				adios(EX_SOFTWARE, NULL, "giving up trying to find a unique delimiter string");
				else
				(*cp)++;
		}
	}

	/* Build the rest of the header field structures */
	build_headers(ct);

	return ct;
}


/*
** Set up structures for placing unencoded
** content when building parts.
*/

static int
init_decoded_content(CT ct)
{
	CE ce;

	ce = mh_xcalloc(1, sizeof(*ce));

	ct->c_cefile     = ce;
	ct->c_ceopenfnx  = open7Bit;  /* since unencoded */
	ct->c_ceclosefnx = close_encoding;
	ct->c_cesizefnx  = NULL;  /* since unencoded */

	return OK;
}


static char *
fgetstr(char *s, int n, FILE *stream)
{
	char *cp, *ep;

	for (ep = (cp = s) + n; cp < ep; ) {
		int i;

		if (!fgets(cp, n, stream))
			return (cp != s ? s : NULL);
		if (cp == s && *cp != '#')
			return s;

		cp += (i = strlen(cp)) - 1;
		if (i <= 1 || *cp-- != '\n' || *cp != '\\')
			break;
		*cp = '\0';
		n -= (i - 2);
	}

	return s;
}


/*
** Parse the composition draft for text and directives.
** Do initial setup of Content structure.
*/

static int
user_content(FILE *in, char *file, char *buf, CT *ctp)
{
	int vrsn;
	unsigned char *cp;
	char **ap;
	char buffer[BUFSIZ];
	struct multipart *m;
	struct part **pp;
	struct str2init *s2i;
	CI ci;
	CT ct;
	CE ce;

	if (buf[0] == '\n' || strcmp(buf, "#\n") == 0) {
		*ctp = NULL;
		return OK;
	}

	/* allocate basic Content structure */
	ct = mh_xcalloc(1, sizeof(*ct));
	*ctp = ct;

	/* allocate basic structure for handling decoded content */
	init_decoded_content(ct);
	ce = ct->c_cefile;

	ci = &ct->c_ctinfo;
	set_id(ct, 0);

	/*
	** Handle inline text.  Check if line
	** is one of the following forms:
	**
	** 1) doesn't begin with '#'  (implicit directive)
	** 2) begins with "##"        (implicit directive)
	** 3) begins with "#<"
	*/
	if (buf[0] != '#' || buf[1] == '#' || buf[1] == '<') {
		int headers;
		int inlineD;
		long pos;
		char content[BUFSIZ];
		FILE *out;
		char *cp;

		cp = m_mktemp2(NULL, invo_name, NULL, &out);
		if (cp == NULL)
			adios(EX_CANTCREAT, "mhbuild", "unable to create temporary file");

		/* use a temp file to collect the plain text lines */
		ce->ce_file = mh_xstrdup(cp);
		ce->ce_unlink = 1;

		if (buf[0] == '#' && buf[1] == '<') {
			strncpy(content, buf + 2, sizeof(content));
			inlineD = 1;
			goto rock_and_roll;
		} else {
			inlineD = 0;
		}

		/* the directive is implicit */
		strncpy(content, "text/plain", sizeof(content));
		headers = 0;
		strncpy(buffer, buf[0] != '#' ? buf : buf + 1, sizeof(buffer));
		for (;;) {
			int i;

			if (headers >= 0 && uprf(buffer, DESCR_FIELD) &&
					buffer[i=strlen(DESCR_FIELD)] == ':') {
				headers = 1;

again_descr:
				ct->c_descr = add(buffer + i + 1, ct->c_descr);
				if (!fgetstr(buffer, sizeof(buffer) - 1, in))
					adios(EX_DATAERR, NULL, "end-of-file after %s: field in plaintext", DESCR_FIELD);
				switch (buffer[0]) {
				case ' ':
				case '\t':
					i = -1;
					goto again_descr;

				case '#':
					adios(EX_DATAERR, NULL, "#-directive after %s: field in plaintext", DESCR_FIELD);
					/* NOTREACHED */

				default:
					break;
				}
			}

			if (headers >= 0 && uprf(buffer, DISPO_FIELD)
				&& buffer[i = strlen(DISPO_FIELD)] == ':') {
				headers = 1;

again_dispo:
				ct->c_dispo = add(buffer + i + 1, ct->c_dispo);
				if (!fgetstr(buffer, sizeof(buffer) - 1, in))
					adios(EX_DATAERR, NULL, "end-of-file after %s: field in plaintext", DISPO_FIELD);
				switch (buffer[0]) {
				case ' ':
				case '\t':
					i = -1;
					goto again_dispo;

				case '#':
					adios(EX_DATAERR, NULL, "#-directive after %s: field in plaintext", DISPO_FIELD);
					/* NOTREACHED */

				default:
					break;
				}
			}

			if (headers != 1 || buffer[0] != '\n')
				fputs(buffer, out);

rock_and_roll:
			headers = -1;
			pos = ftell(in);
			if ((cp = fgetstr(buffer, sizeof(buffer) - 1, in))
					== NULL)
				break;
			if (buffer[0] == '#') {
				char *bp;

				if (buffer[1] != '#')
					break;
				for (cp = (bp = buffer) + 1; *cp; cp++)
					*bp++ = *cp;
				*bp = '\0';
			}
		}

		fclose(out);

		/* parse content type */
		if (get_ctinfo(content, ct, inlineD) == NOTOK)
			exit(EX_DATAERR);

		for (s2i = str2cts; s2i->si_key; s2i++)
			if (!mh_strcasecmp(ci->ci_type, s2i->si_key))
				break;
		if (!s2i->si_key && !uprf(ci->ci_type, "X-"))
			s2i++;

		/*
		** check type specified (possibly implicitly)
		*/
		switch (ct->c_type = s2i->si_val) {
		case CT_MESSAGE:
			if (!mh_strcasecmp(ci->ci_subtype, "rfc822")) {
				ct->c_encoding = CE_7BIT;
				goto call_init;
			}
			/* else fall... */
		case CT_MULTIPART:
			adios(EX_DATAERR, NULL, "it doesn't make sense to define an in-line %s content",
					ct->c_type == CT_MESSAGE ? "message" :
					"multipart");
			/* NOTREACHED */

		default:
call_init:
			if ((ct->c_ctinitfnx = s2i->si_init))
				(*ct->c_ctinitfnx) (ct);
			break;
		}

		if (cp)
			fseek(in, pos, SEEK_SET);
		return OK;
	}

	/*
	** If we've reached this point, the next line
	** must be some type of explicit directive.
	*/

	if (buf[1] == '@') {
		adios(EX_DATAERR, NULL, "The #@ directive i.e. message/external-body "
				"is not supported anymore.");
	}

	/* parse directive */
	if (get_ctinfo(buf+1, ct, 1) == NOTOK)
		exit(EX_DATAERR);

	/* check directive against the list of MIME types */
	for (s2i = str2cts; s2i->si_key; s2i++)
		if (!mh_strcasecmp(ci->ci_type, s2i->si_key))
			break;

	/*
	** Check if the directive specified a valid type.
	** This will happen if it was one of the following forms:
	**
	**    #type/subtype
	*/
	if (s2i->si_key) {
		if (!ci->ci_subtype)
			adios(EX_DATAERR, NULL, "missing subtype in \"#%s\"", ci->ci_type);

		switch (ct->c_type = s2i->si_val) {
		case CT_MULTIPART:
			adios(EX_DATAERR, NULL, "use \"#begin ... #end\" instead of \"#%s/%s\"", ci->ci_type, ci->ci_subtype);
			/* NOTREACHED */

		case CT_MESSAGE:
			if (!mh_strcasecmp(ci->ci_subtype, "partial") ||
					!mh_strcasecmp(ci->ci_subtype,
					"external-body")) {
				adios(EX_DATAERR, NULL, "sorry, \"#%s/%s\" isn't supported", ci->ci_type, ci->ci_subtype);
			}
use_forw:
			admonish(NULL, "use \"#forw [+folder] [msgs]\" instead of \"#%s/%s\"", ci->ci_type, ci->ci_subtype);
			/* FALL */

		default:
			if ((ct->c_ctinitfnx = s2i->si_init))
				(*ct->c_ctinitfnx) (ct);
			break;
		}

		/* Handle [file] argument */
		if (ci->ci_magic) {
			/* check if specifies command to execute */
			if (*ci->ci_magic == '|' || *ci->ci_magic == '!') {
				for (cp = ci->ci_magic + 1; isspace(*cp); cp++)
					continue;
				if (!*cp)
					adios(EX_DATAERR, NULL, "empty pipe command for #%s directive", ci->ci_type);
				cp = mh_xstrdup(cp);
				mh_free0(&(ci->ci_magic));
				ci->ci_magic = cp;
			} else {
				/* record filename of decoded contents */
				ce->ce_file = ci->ci_magic;
				if (access(ce->ce_file, R_OK) == NOTOK)
					adios(EX_IOERR, "reading", "unable to access %s for", ce->ce_file);
				ci->ci_magic = NULL;
			}
			return OK;
		}

		/*
		** No [file] argument, so check profile for
		** method to compose content.
		*/
		snprintf(buffer, sizeof(buffer), "%s-compose-%s/%s",
				invo_name, ci->ci_type, ci->ci_subtype);
		if ((cp = context_find(buffer)) == NULL || *cp == '\0') {
			snprintf(buffer, sizeof(buffer), "%s-compose-%s",
					invo_name, ci->ci_type);
			if ((cp = context_find(buffer)) == NULL ||
					*cp == '\0') {
				content_error(NULL, ct, "don't know how to compose content");
				exit(EX_CONFIG);
			}
		}
		ci->ci_magic = mh_xstrdup(cp);
		return OK;
	}

	/*
	** Message directive
	** #forw [+folder] [msgs]
	*/
	if (!mh_strcasecmp(ci->ci_type, "forw")) {
		int msgnum;
		char *folder, *arguments[MAXARGS];
		struct msgs *mp;

		if (ci->ci_magic) {
			int i;

			ap = brkstring(ci->ci_magic, " ", "\n");
			for (i=0; ap[i] && i<MAXARGS-1; i++) {
				arguments[i] = ap[i];
			}
			arguments[i] = NULL;

		} else {
			arguments[0] = seq_cur;
			arguments[1] = NULL;
		}
		folder = NULL;

		/* search the arguments for a folder name */
		for (ap = arguments; *ap; ap++) {
			cp = *ap;
			if (*cp == '+' || *cp == '@') {
				if (folder)
					adios(EX_USAGE, NULL, "only one folder per #forw directive");
				else
					folder = mh_xstrdup(expandfol(cp));
			}
		}

		/* else, use the current folder */
		if (!folder)
			folder = mh_xstrdup(getcurfol());

		if (!(mp = folder_read(folder)))
			adios(EX_IOERR, NULL, "unable to read folder %s", folder);
		for (ap = arguments; *ap; ap++) {
			cp = *ap;
			if (*cp != '+' && *cp != '@')
				if (!m_convert(mp, cp))
					exit(EX_USAGE);
		}
		mh_free0(&folder);
		free_ctinfo(ct);

		/*
		** If there is more than one message to include, make this
		** a content of type "multipart/digest" and insert each message
		** as a subpart.  If there is only one message, then make this
		** a content of type "message/rfc822".
		*/
		if (mp->numsel > 1) {
			/* we are forwarding multiple messages */
			if (get_ctinfo("multipart/digest", ct, 0) == NOTOK)
				exit(EX_DATAERR);
			ct->c_type = CT_MULTIPART;
			ct->c_subtype = MULTI_DIGEST;

			m = mh_xcalloc(1, sizeof(*m));
			ct->c_ctparams = (void *) m;
			pp = &m->mp_parts;

			for (msgnum = mp->lowsel; msgnum <= mp->hghsel; msgnum++) {
				if (is_selected(mp, msgnum)) {
					struct part *part;
					CT p;
					CE pe;

					p = mh_xcalloc(1, sizeof(*p));
					init_decoded_content(p);
					pe = p->c_cefile;
					if (get_ctinfo("message/rfc822", p, 0)
							== NOTOK)
						exit(EX_DATAERR);
					p->c_type = CT_MESSAGE;
					p->c_subtype = MESSAGE_RFC822;

					snprintf(buffer, sizeof(buffer),
							"%s/%d", mp->foldpath,
							msgnum);
					pe->ce_file = mh_xstrdup(buffer);

					part = mh_xcalloc(1, sizeof(*part));
					*pp = part;
					pp = &part->mp_next;
					part->mp_part = p;
				}
			}
		} else {
			/* we are forwarding one message */
			if (get_ctinfo("message/rfc822", ct, 0) == NOTOK)
				exit(EX_DATAERR);
			ct->c_type = CT_MESSAGE;
			ct->c_subtype = MESSAGE_RFC822;

			msgnum = mp->lowsel;
			snprintf(buffer, sizeof(buffer), "%s/%d",
					mp->foldpath, msgnum);
			ce->ce_file = mh_xstrdup(buffer);
		}

		folder_free(mp);  /* free folder/message structure */
		return OK;
	}

	/*
	** #end
	*/
	if (!mh_strcasecmp(ci->ci_type, "end")) {
		free_content(ct);
		*ctp = NULL;
		return DONE;
	}

	/*
	** #begin [ alternative | parallel ]
	*/
	if (!mh_strcasecmp(ci->ci_type, "begin")) {
		if (!ci->ci_magic) {
			vrsn = MULTI_MIXED;
			cp = SubMultiPart[vrsn - 1].kv_key;
		} else if (!mh_strcasecmp(ci->ci_magic, "alternative")) {
			vrsn = MULTI_ALTERNATE;
			cp = SubMultiPart[vrsn - 1].kv_key;
		} else if (!mh_strcasecmp(ci->ci_magic, "parallel")) {
			vrsn = MULTI_PARALLEL;
			cp = SubMultiPart[vrsn - 1].kv_key;
		} else if (uprf(ci->ci_magic, "digest")) {
			goto use_forw;
		} else {
			vrsn = MULTI_UNKNOWN;
			cp = ci->ci_magic;
		}

		free_ctinfo(ct);
		snprintf(buffer, sizeof(buffer), "multipart/%s", cp);
		if (get_ctinfo(buffer, ct, 0) == NOTOK)
			exit(EX_DATAERR);
		ct->c_type = CT_MULTIPART;
		ct->c_subtype = vrsn;

		m = mh_xcalloc(1, sizeof(*m));
		ct->c_ctparams = (void *) m;

		pp = &m->mp_parts;
		while (fgetstr(buffer, sizeof(buffer) - 1, in)) {
			struct part *part;
			CT p;

			if (user_content(in, file, buffer, &p) == DONE) {
				if (!m->mp_parts)
					adios(EX_DATAERR, NULL, "empty \"#begin ... #end\" sequence");
				return OK;
			}
			if (!p)
				continue;

			part = mh_xcalloc(1, sizeof(*part));
			*pp = part;
			pp = &part->mp_next;
			part->mp_part = p;
		}
		admonish(NULL, "premature end-of-file, missing #end");
		return OK;
	}

	/*
	** Unknown directive
	*/
	adios(EX_DATAERR, NULL, "unknown directive \"#%s\"", ci->ci_type);
	return NOTOK;  /* NOT REACHED */
}


static void
set_id(CT ct, int top)
{
	char msgid[BUFSIZ];
	static int partno;
	static time_t clock = 0;
	static char *msgfmt;

	if (clock == 0) {
		time(&clock);
		snprintf(msgid, sizeof(msgid), "<%d.%ld.%%d@%s>\n",
				(int) getpid(), (long) clock, LocalName());
		partno = 0;
		msgfmt = mh_xstrdup(msgid);
	}
	snprintf(msgid, sizeof(msgid), msgfmt, top ? 0 : ++partno);
	ct->c_id = mh_xstrdup(msgid);
}


/*
** Fill out, or expand the various contents in the composition
** draft.  Read-in any necessary files.  Parse and execute any
** commands specified by profile composition strings.
*/

static int
compose_content(CT ct)
{
	CE ce = ct->c_cefile;

	switch (ct->c_type) {
	case CT_MULTIPART:
	{
		int partnum;
		char *pp;
		char partnam[BUFSIZ];
		struct multipart *m = (struct multipart *) ct->c_ctparams;
		struct part *part;

		if (ct->c_partno) {
			snprintf(partnam, sizeof(partnam), "%s.",
					ct->c_partno);
			pp = partnam + strlen(partnam);
		} else {
			pp = partnam;
		}

		/* first, we call compose_content on all the subparts */
		for (part = m->mp_parts, partnum = 1; part;
				part = part->mp_next, partnum++) {
			CT p = part->mp_part;

			sprintf(pp, "%d", partnum);
			p->c_partno = mh_xstrdup(partnam);
			if (compose_content(p) == NOTOK)
				return NOTOK;
		}
	}
	break;

	case CT_MESSAGE:
		/* Nothing to do for type message */
		break;

	/*
	** Discrete types (text/application/audio/image/video)
	*/
	default:
		if (!ce->ce_file) {
			pid_t child_id;
			int xstdout, len, buflen;
			char *bp, **ap, *cp;
			char *vec[4], buffer[BUFSIZ];
			FILE *out;
			CI ci = &ct->c_ctinfo;
			char *tfile = NULL;

			if (!(cp = ci->ci_magic))
				adios(EX_SOFTWARE, NULL, "internal error(5)");

			tfile = m_mktemp2(NULL, invo_name, NULL, NULL);
			if (tfile == NULL) {
				adios(EX_CANTCREAT, "mhbuild", "unable to create temporary file");
			}
			ce->ce_file = mh_xstrdup(tfile);
			ce->ce_unlink = 1;

			xstdout = 0;

			/* Get buffer ready to go */
			bp = buffer;
			bp[0] = '\0';
			buflen = sizeof(buffer);

			/*
			** Parse composition string into buffer
			*/
			for ( ; *cp; cp++) {
				if (*cp == '%') {
					switch (*++cp) {
					case 'a':
					{
						/*
						** insert parameters from
						** directive
						*/
						char **ep;
						char *s = "";

						for (ap = ci->ci_attrs, ep = ci->ci_values; *ap; ap++, ep++) {
							snprintf(bp, buflen, "%s%s=\"%s\"", s, *ap, *ep);
							len = strlen(bp);
							bp += len;
							buflen -= len;
							s = " ";
						}
					}
					break;

					case 'F':
						/* %f, and stdout is not-redirected */
						xstdout = 1;
						/* and fall... */

					case 'f':
						/*
						** insert temporary filename
						** where content should be
						** written
						*/
						snprintf(bp, buflen, "%s", ce->ce_file);
						break;

					case 's':
						/* insert content subtype */
						strncpy(bp, ci->ci_subtype, buflen);
						break;

					case '%':
						/* insert character % */
						goto raw;

					default:
						*bp++ = *--cp;
						*bp = '\0';
						buflen--;
						continue;
					}
					len = strlen(bp);
					bp += len;
					buflen -= len;
				} else {
raw:
					*bp++ = *cp;
					*bp = '\0';
					buflen--;
				}
			}

			if (verbosw)
				printf("composing content %s/%s from command\n\t%s\n", ci->ci_type, ci->ci_subtype, buffer);

			fflush(stdout);  /* not sure if need for -noverbose */

			vec[0] = "/bin/sh";
			vec[1] = "-c";
			vec[2] = buffer;
			vec[3] = NULL;

			if ((out = fopen(ce->ce_file, "w")) == NULL)
				adios(EX_IOERR, ce->ce_file, "unable to open for writing");

			switch (child_id = fork()) {
			case NOTOK:
				adios(EX_OSERR, "fork", "unable to fork");
				/* NOTREACHED */

			case OK:
				if (!xstdout)
					dup2(fileno(out), 1);
				close(fileno(out));
				execvp("/bin/sh", vec);
				fprintf(stderr, "unable to exec ");
				perror("/bin/sh");
				_exit(EX_OSERR);
				/* NOTREACHED */

			default:
				fclose(out);
				if (pidXwait(child_id, NULL))
					exit(EX_SOFTWARE);
				break;
			}
		}
		break;
	}

	return OK;
}


/*
** Scan the content.
**
**    1) choose a transfer encoding.
**    2) check for clashes with multipart boundary string.
**    3) for text content, figure out which character set is being used.
**
** If there is a clash with one of the contents and the multipart boundary,
** this function will exit with NOTOK.  This will cause the scanning process
** to be repeated with a different multipart boundary.  It is possible
** (although highly unlikely) that this scan will be repeated multiple times.
*/

static int
scan_content(CT ct)
{
	int len;
	int check8bit = 0, contains8bit = 0;
	int checklinelen = 0, linelen = 0;
	int checkboundary = 0, boundaryclash = 0;
	int checklinespace = 0, linespace = 0;  /* trailing whitespace */
	unsigned char *cp = NULL, buffer[BUFSIZ];
	struct text *t = NULL;
	FILE *in = NULL;
	CE ce = ct->c_cefile;

	/*
	** handle multipart by scanning all subparts
	** and then checking their encoding.
	*/
	if (ct->c_type == CT_MULTIPART) {
		struct multipart *m = (struct multipart *) ct->c_ctparams;
		struct part *part;

		/* initially mark the domain of enclosing multipart as 7bit */
		ct->c_encoding = CE_7BIT;

		for (part = m->mp_parts; part; part = part->mp_next) {
			CT p = part->mp_part;

			if (scan_content(p) == NOTOK) {
				/* choose encoding for subpart */
				return NOTOK;
			}

			/*
			** if necessary, enlarge encoding for enclosing
			** multipart
			*/
			if (p->c_encoding == CE_BINARY)
				ct->c_encoding = CE_BINARY;
			if (p->c_encoding == CE_8BIT &&
					ct->c_encoding != CE_BINARY)
				ct->c_encoding = CE_8BIT;
		}

		return OK;
	}

	/*
	** Decide what to check while scanning this content.
	*/
	switch (ct->c_type) {
	case CT_TEXT:
		check8bit = 1;
		checkboundary = 1;
		checklinelen = 1;
		checklinespace = 1;
		break;

	case CT_MESSAGE:
		check8bit = 0;
		checklinelen = 0;
		checklinespace = 0;
		checkboundary = 1;
		break;

	case CT_APPLICATION:
	case CT_AUDIO:
	case CT_IMAGE:
	case CT_VIDEO:
		check8bit = 0;
		checklinelen = 0;
		checklinespace = 0;
		checkboundary = 0;
		break;
	}

	/*
	** Scan the unencoded content
	*/
	if (check8bit || checklinelen || checklinespace || checkboundary) {
		if ((in = fopen(ce->ce_file, "r")) == NULL)
			adios(EX_IOERR, ce->ce_file, "unable to open for reading");
		len = strlen(prefix);

		while (fgets(buffer, sizeof(buffer) - 1, in)) {
			if (check8bit) {
				for (cp = buffer; *cp; cp++) {
					if (!isascii(*cp)) {
						contains8bit = 1;
						/* no need to keep checking */
						check8bit = 0;
					}
				}
			}

			if (checklinelen && (strlen(buffer) > CPERLIN + 1)) {
				linelen = 1;
				checklinelen = 0;  /* no need to keep checking */
			}

			if (checklinespace &&
					(cp = buffer + strlen(buffer) - 2) >
					buffer && isspace(*cp)) {
				linespace = 1;
				/* no need to keep checking */
				checklinespace = 0;
			}

			/*
			** Check if content contains a line that clashes
			** with our standard boundary for multipart messages.
			*/
			if (checkboundary && buffer[0] == '-' &&
					buffer[1] == '-') {
				for (cp = buffer + strlen(buffer) - 1;
						cp >= buffer; cp--)
					if (!isspace(*cp))
						break;
				*++cp = '\0';
				if (strncmp(buffer + 2, prefix, len)==0 &&
						isdigit(buffer[2 + len])) {
					boundaryclash = 1;
					/* no need to keep checking */
					checkboundary = 0;
				}
			}
		}
		fclose(in);
	}

	/*
	** Decide which transfer encoding to use.
	*/
	switch (ct->c_type) {
	case CT_TEXT:
		/*
		** If the text content didn't specify a character
		** set, we need to figure out which one was used.
		*/
		t = (struct text *) ct->c_ctparams;
		if (t->tx_charset == CHARSET_UNSPECIFIED) {
			CI ci = &ct->c_ctinfo;
			char **ap, **ep;

			for (ap = ci->ci_attrs, ep = ci->ci_values; *ap; ap++, ep++)
				continue;

			if (contains8bit) {
				t->tx_charset = CHARSET_UNKNOWN;
				*ap = concat("charset=", write_charset_8bit(),
						NULL);
			} else {
				t->tx_charset = CHARSET_USASCII;
				*ap = mh_xstrdup("charset=us-ascii");
			}

			cp = strchr(*ap++, '=');
			*ap = NULL;
			*cp++ = '\0';
			*ep = cp;
		}

		if (contains8bit || linelen || linespace)
			ct->c_encoding = CE_QUOTED;
		else
			ct->c_encoding = CE_7BIT;
		break;

	case CT_MESSAGE:
		ct->c_encoding = CE_7BIT;
		break;

	case CT_APPLICATION:
	case CT_AUDIO:
	case CT_IMAGE:
	case CT_VIDEO:
		/*
		** Forcing use of base64, because these types likely
		** contain binary data and NUL bytes. Don't care about
		** files that would be clean.
		*/
		ct->c_encoding = CE_BASE64;
		break;
	}

	return (boundaryclash ? NOTOK : OK);
}


/*
** Scan the content structures, and build header
** fields that will need to be output into the
** message.
*/

static int
build_headers(CT ct)
{
	int cc, len;
	char **ap, **ep;
	char *np, *vp, buffer[BUFSIZ];
	CI ci = &ct->c_ctinfo;

	/*
	** If message is type multipart, then add the multipart
	** boundary to the list of attribute/value pairs.
	*/
	if (ct->c_type == CT_MULTIPART) {
		char *cp;
		static int level = 0;  /* store nesting level */

		ap = ci->ci_attrs;
		ep = ci->ci_values;
		snprintf(buffer, sizeof(buffer), "boundary=%s%d",
				prefix, level++);
		cp = strchr(*ap++ = mh_xstrdup(buffer), '=');
		*ap = NULL;
		*cp++ = '\0';
		*ep = cp;
	}

	/*
	** output the content type and subtype
	*/
	np = mh_xstrdup(TYPE_FIELD);
	vp = concat(" ", ci->ci_type, "/", ci->ci_subtype, NULL);

	/* keep track of length of line */
	len = strlen(TYPE_FIELD) + strlen(ci->ci_type) +
			strlen(ci->ci_subtype) + 3;

	/*
	** Append the attribute/value pairs to
	** the end of the Content-Type line.
	*/
	for (ap = ci->ci_attrs, ep = ci->ci_values; *ap; ap++, ep++) {
		vp = add(";", vp);
		len++;

		snprintf(buffer, sizeof(buffer), "%s=\"%s\"", *ap, *ep);
		if (len + 1 + (cc = strlen(buffer)) >= CPERLIN) {
			vp = add("\n\t", vp);
			len = 8;
		} else {
			vp = add(" ", vp);
			len++;
		}
		vp = add(buffer, vp);
		len += cc;
	}

	/*
	** Append any RFC-822 comment to the end of
	** the Content-Type line.
	*/
	if (ci->ci_comment) {
		snprintf(buffer, sizeof(buffer), "(%s)", ci->ci_comment);
		if (len + 1 + (cc = 2 + strlen(ci->ci_comment)) >= CPERLIN) {
			vp = add("\n\t", vp);
			len = 8;
		} else {
			vp = add(" ", vp);
			len++;
		}
		vp = add(buffer, vp);
		len += cc;
	}
	vp = add("\n", vp);
	add_header(ct, np, vp);

	/*
	** output the Content-ID
	*/
	if (ct->c_id) {
		np = mh_xstrdup(ID_FIELD);
		vp = concat(" ", ct->c_id, NULL);
		add_header(ct, np, vp);
	}

	/*
	** output the Content-Description
	*/
	if (ct->c_descr) {
		np = mh_xstrdup(DESCR_FIELD);
		vp = concat(" ", ct->c_descr, NULL);
		if (encode_rfc2047(DESCR_FIELD, &vp, NULL)) {
			adios(EX_DATAERR, NULL, "Unable to encode %s header", DESCR_FIELD);
		}
		add_header(ct, np, vp);
	}

	/*
	** output the Content-Disposition
	*/
	if (ct->c_dispo) {
		np = mh_xstrdup(DISPO_FIELD);
		vp = concat(" ", ct->c_dispo, NULL);
		add_header(ct, np, vp);
	}

	/*
	** output the Content-Transfer-Encoding
	*/
	switch (ct->c_encoding) {
	case CE_7BIT:
		/* Nothing to output */
		break;

	case CE_8BIT:
		if (ct->c_type == CT_MESSAGE)
			adios(EX_DATAERR, NULL, "internal error, invalid encoding");

		np = mh_xstrdup(ENCODING_FIELD);
		vp = concat(" ", "8bit", "\n", NULL);
		add_header(ct, np, vp);
		break;

	case CE_QUOTED:
		if (ct->c_type == CT_MESSAGE || ct->c_type == CT_MULTIPART)
			adios(EX_DATAERR, NULL, "internal error, invalid encoding");

		np = mh_xstrdup(ENCODING_FIELD);
		vp = concat(" ", "quoted-printable", "\n", NULL);
		add_header(ct, np, vp);
		break;

	case CE_BASE64:
		if (ct->c_type == CT_MESSAGE || ct->c_type == CT_MULTIPART)
			adios(EX_DATAERR, NULL, "internal error, invalid encoding");

		np = mh_xstrdup(ENCODING_FIELD);
		vp = concat(" ", "base64", "\n", NULL);
		add_header(ct, np, vp);
		break;

	case CE_BINARY:
		if (ct->c_type == CT_MESSAGE)
			adios(EX_DATAERR, NULL, "internal error, invalid encoding");

		np = mh_xstrdup(ENCODING_FIELD);
		vp = concat(" ", "binary", "\n", NULL);
		add_header(ct, np, vp);
		break;

	default:
		adios(EX_DATAERR, NULL, "unknown transfer encoding in content");
		break;
	}

	/*
	** Additional content specific header processing
	*/
	switch (ct->c_type) {
	case CT_MULTIPART:
	{
		struct multipart *m;
		struct part *part;

		m = (struct multipart *) ct->c_ctparams;
		for (part = m->mp_parts; part; part = part->mp_next) {
			CT p;

			p = part->mp_part;
			build_headers(p);
		}
	}
		break;

	default:
		/* Nothing to do */
		break;
	}

	return OK;
}
