/*
** Copyright 2000 Double Precision, Inc.  See COPYING for
** distribution information.
*/

#if	HAVE_CONFIG_H
#include	"config.h"
#endif
#include	<sys/types.h>
#include	<sys/stat.h>
#include	<time.h>
#include	<stdio.h>
#include	<errno.h>
#include	<string.h>
#include	<signal.h>
#if	HAVE_STRINGS_H
#include	<strings.h>
#endif
#if	HAVE_UNISTD_H
#include	<unistd.h>
#endif
#include	<stdlib.h>
#include	<ctype.h>
#include	<pwd.h>
#include	<fcntl.h>
#include	<signal.h>
#include	"rfc2045.h"
#include	"rfc2045charset.h"
#if HAVE_UNISTD_H
#include	<unistd.h>
#endif
#if HAVE_SYS_WAIT_H
#include	<sys/wait.h>
#endif
#include	"numlib/numlib.h"

#if     HAS_GETHOSTNAME
#else
int gethostname(const char *, size_t);
#endif

static const char rcsid[]="$Id$";

struct arg_list {
	struct arg_list *next;
	char *arg;
	} ;

/******************************************************************************

Open some file or a pipe for reading and writing.

******************************************************************************/

static FILE *openfile_or_pipe(const char *filename, const char *mode)
{
int	fd;
FILE	*fp;

	if (strcmp(filename, "-") == 0)	/* stdin or stdout */
		fd=dup( strcmp(mode, "r") ? 1:0);
	else if (*filename == '&')
		fd=dup( atoi(filename+1));	/* file descriptor */
	else fd=open(filename, (strcmp(mode, "r") ? O_WRONLY|O_CREAT|O_TRUNC:
			O_RDONLY), 0666);	/* or a file */
	if (fd < 0)
	{
		perror(filename);
		exit(1);
	}
	fp=fdopen(fd, mode);
	if (!fp)
	{
		perror("fdopen");
		exit(1);
	}
	return (fp);
}

/******************************************************************************

Open some file.  If we get a pipe, open a temporary file, and drain pipe's
contents into it.

******************************************************************************/

static FILE *openfile(const char *filename)
{
FILE	*fp=openfile_or_pipe(filename, "r");
int	fd=fileno(fp);
off_t	orig_pos;

	if ((orig_pos=lseek(fd, 0L, SEEK_CUR)) == -1 ||
		lseek(fd, 0L, SEEK_END) == -1 ||
		lseek(fd, 0L, SEEK_CUR) == -1 ||
		lseek(fd, orig_pos, SEEK_SET) == -1)	/* Must be a pipe */
	{
	FILE *t=tmpfile();
	int	c;

		if (!t)
		{
			perror("tmpfile");
			exit(1);
		}

		while ((c=getc(fp)) != EOF)
			putc(c, t);
		if (ferror(fp) || fflush(t)
			|| ferror(t) || fseek(t, 0L, SEEK_SET) == -1)
		{
			perror("write");
			exit(1);
		}
		fclose(fp);
		fp=t;
	}
	return (fp);
}

/******************************************************************************

Build argv/argc from a file.

******************************************************************************/

static void read_args(int *argcp, char ***argvp, const char *file)
{
FILE	*fp=openfile_or_pipe(file, "r");
struct arg_list *argfirst=0, *arglast=0, *argp;
char	buffer[BUFSIZ];
char	*p;
int	c;

	*argcp=0;
	while (fgets(buffer, sizeof(buffer), fp) != 0)
	{
	const	char *q;

		if ((p=strchr(buffer, '\n')) != 0)
			*p=0;
		else while ((c=getc(fp)) != '\n' && c != EOF)
			;	/* Just dump the excess */

		/* Skip the filler. */

		q=buffer;
		while (*q && isspace((int)(unsigned char)*q))
			++q;
		if (!*q)	continue;
		if (*q == '#')	continue;
		if (strcmp(buffer, "-") == 0)	break;

		argp=(struct arg_list *)malloc(sizeof(struct arg_list)+1+
			strlen(q));
		if (!argp)
		{
			perror("malloc");
			exit(1);
		}
		if (arglast)
			arglast->next=argp;
		else
			argfirst=argp;
		arglast=argp;
		++*argcp;
		argp->next=0;
		argp->arg=strcpy((char *)(argp+1), q);
	}

	if ((*argvp=malloc(sizeof (char *) * (*argcp+1))) == 0)
	{
		perror("malloc");
		exit(1);
	}
	c=0;
	for (argp=argfirst; argp; argp=argp->next)
	{
		(*argvp)[c]= argp->arg;
		++c;
	}
	(*argvp)[c]=0;
}

static void usage()
{
	fprintf(stderr,
"Usage:\n"
"  makemime -c type [-o file] [-e encoding] [-a \"Header: Contents\"] file\n"
"           -m [ type ] [-o file] [-e encoding] [-a \"Header: Contents\"] file\n"
"           -j [-o file] file1 file2\n"
"           @file\n"
"\n"
"   file:  filename    - read or write from filename\n"
"          -           - read or write from stdin or stdout\n"
"          &n          - read or write from file descriptor n\n"
"          \\( opts \\)  - read from child process, that generates [ opts ]\n"
"\n"
"Options:\n"
"\n"
"  -c type         - create a new mime section from \"file\" with this\n"
"                    Content-Type: (default is application/octet-stream).\n"
"  -m [ type ]     - create a multipart mime section from \"file\" of this\n"
"                    Content-Type: (default is multipart/mixed).\n"
"  -e encoding     - use the given encoding (7bit, 8bit, quoted-printable,\n"
"                    or base64), instead of guessing.\n"
"  -j file1 file2  - join mime section file2 to multipart section file1.\n"
"  -o file         - write ther result to file, instead of stdout (not\n"
"                    allowed in child processes).\n"
"  -a header       - prepend an additional header to the output.\n"
"\n"
"  @file - read all of the above options from file, one option or\n"
"          value on each line.\n"
	);
	exit (0);
}

/******************************************************************************

The arguments are parsed into the following structure, as a tree.

******************************************************************************/
struct mimestruct {

	/*
	** One or two input files.  We initialize either file or child,
	** depending on the source being a file, or a child process.
	** Later, we open a file pointer in either case.
	*/

	const char *inputfile1, *inputfile2;
	struct mimestruct *inputchild1, *inputchild2;
	FILE *inputfp1, *inputfp2;
	pid_t	child1, child2;

	/* Output file.  Defaults to "-", stdout */

	const char *outputfile;
	FILE	*outputfp;

		/* The handler and open functions */

	void (*handler_func)(struct mimestruct *);
	void (*open_func)(struct mimestruct *);

		/* The new mime type, and encoding (-e) */
	const char *mimetype;
	const char *mimeencoding;

		/* A list of -a headers */
	struct arg_list *a_first, *a_last;
	} ;

static void createsimplemime(struct mimestruct *);
static void createmultipartmime(struct mimestruct *);
static void joinmultipart(struct mimestruct *);

static void opencreatesimplemime(struct mimestruct *);
static void opencreatemultipartmime(struct mimestruct *);
static void openjoinmultipart(struct mimestruct *);

/******************************************************************************

Recursively build the mimestruct tree.

******************************************************************************/

struct mimestruct *parseargs(int *argcp, char ***argvp)
{
struct mimestruct *m=malloc(sizeof(struct mimestruct));
int argc= *argcp;
char **argv= *argvp;

	if (!m)
	{
		perror("malloc");
		exit(1);
	}
	memset(m, 0, sizeof(*m));

	if (argc == 0 || argv[0][0] != '-')	usage();

	if (strncmp(argv[0], "-c", 2) == 0)
	{
		m->handler_func= &createsimplemime;
		m->open_func= &opencreatesimplemime;
		if (argv[0][2])
		{
			m->mimetype=argv[0]+2;
			--argc;
			++argv;
		}
		else
		{
			--argc;
			++argv;
			if (argc && argv[0][0] != '-' && argv[0][0] != ')')
			{
				m->mimetype=argv[0];
				--argc;
				++argv;
			}
			else
				m->mimetype="application/octet-stream";
		}

		while (isspace((int)(unsigned char)*m->mimetype))
			++m->mimetype;
	}
	else if (strncmp(argv[0], "-m", 2) == 0)
	{
		m->handler_func= &createmultipartmime;
		m->open_func= &opencreatemultipartmime;
		if (argv[0][2])
		{
			m->mimetype=argv[0]+2;
			--argc;
			++argv;
		}
		else
		{
			--argc;
			++argv;
			if (argc && argv[0][0] != '-' && argv[0][0] != ')')
			{
				m->mimetype=argv[0];
				--argc;
				++argv;
			}
			else
				m->mimetype="multipart/mixed";
		}
		while (isspace((int)(unsigned char)*m->mimetype))
			++m->mimetype;
	}
	else if (strncmp(argv[0], "-j", 2) == 0)
	{
	const char *filename;

		m->handler_func= &joinmultipart;
		m->open_func= &openjoinmultipart;
		if (argv[0][2])
		{
			filename=argv[0]+2;
			--argc;
			++argv;
		}
		else
		{
			--argc;
			++argv;
			if (argc == 0)	usage();
			filename=argv[0];
			--argc;
			++argv;
		}

		while (isspace((int)(unsigned char)*filename))
			++filename;

		if (strcmp(filename, "(") == 0)
		{
			m->inputchild2=parseargs(&argc, &argv);
			if (argc == 0 || strcmp(argv[0], ")"))
				usage();
			--argc;
			++argv;
		}
		else
			m->inputfile2=filename;
	}

	/* Handle common options */

	while (argc)
	{
		if (strncmp(argv[0], "-o", 2) == 0)
		{
		const char *f=argv[0]+2;

			++argv;
			--argc;
			if (*f == 0)
			{
				if (!argc)	usage();
				f=argv[0];
				++argv;
				--argc;
			}
			while (isspace((int)(unsigned char)*f))
				++f;
			m->outputfile=f;
			continue;
		}

		if (strncmp(argv[0], "-e", 2) == 0)
		{
		char *f=argv[0]+2, *q;

			++argv;
			--argc;

			if (*f == 0)
			{
				if (!argc)	usage();
				f=argv[0];
				++argv;
				--argc;
			}

			for (q=f; *q; q++)
				*q=tolower((int)(unsigned char)*q);

			while (isspace((int)(unsigned char)*f))
				++f;

			if (strcmp(f, "7bit") && strcmp(f, "8bit") &&
				strcmp(f, "quoted-printable") &&
				strcmp(f, "base64"))
				usage();

			m->mimeencoding=f;
			continue;
		}

		if (strncmp(argv[0], "-a", 2) == 0)
		{
		char *f=argv[0]+2;
		struct arg_list *a;

			++argv;
			--argc;

			if (*f == 0)
			{
				if (!argc)	usage();
				f=argv[0];
				++argv;
				--argc;
			}

			while (isspace((int)(unsigned char)*f))
				++f;

			a=malloc(sizeof(struct arg_list));
			if (!a)
			{
				perror("malloc");
				exit(1);
			}
			if (m->a_last)
				m->a_last->next=a;
			else	m->a_first=a;
			m->a_last=a;
			a->arg=f;
			a->next=0;
			continue;
		}
		break;
	}

	/* We must now have the input file argument */

	if (!argc)	usage();

	if (strcmp(argv[0], "(") == 0)
	{
		--argc;
		++argv;
		m->inputchild1=parseargs(&argc, &argv);
		if (argc == 0 || strcmp(argv[0], ")"))
			usage();
		--argc;
		++argv;
	}
	else
	{
		m->inputfile1=argv[0];
		--argc;
		++argv;
	}

	*argcp=argc;
	*argvp=argv;
	return (m);
}

/******************************************************************************

After we're done, terminate with a zero exit code if all child processes also
terminated with a zero exit code.  Otherwise, terminate with a non-zero exit
code thus propagating any child's non-zero exit code to parent.

******************************************************************************/

static void goodexit(struct mimestruct *m, int exitcode)
{
	if (m->outputfp && (fflush(m->outputfp) || ferror(m->outputfp)))
	{
		perror("makemime");
		exit(1);
	}

	/*
	** Drain any leftover input, so that the child doesn't get
	** a SIGPIPE.
	*/

	while (m->inputfp1 && !feof(m->inputfp1) && !ferror(m->inputfp1))
		getc(m->inputfp1);

	while (m->inputfp2 && !feof(m->inputfp2) && !ferror(m->inputfp2))
		getc(m->inputfp2);

	if (m->inputfp1)
	{
		if (ferror(m->inputfp1))
		{
			perror("makemime");
			exitcode=1;
		}

		fclose(m->inputfp1);
	}
	if (m->inputfp2)
	{
		if (ferror(m->inputfp2))
		{
			perror("makemime");
			exitcode=1;
		}

		fclose(m->inputfp2);
	}

	while (m->child1 > 0 && m->child2 > 0)
	{
	int	waitstat;
	pid_t	p=wait(&waitstat);

		if (p <= 0 && errno == ECHILD)	break;

		if (p == m->child1)
			m->child1=0;
		else if (p == m->child2)
			m->child2=0;
		else	continue;
		if (waitstat)	exitcode=1;
	}
	exit(exitcode);
}

int main(int argc, char **argv)
{
struct	mimestruct *m;

	signal(SIGCHLD, SIG_DFL);
	if (argc > 1 && argv[1][0] == '@')
		read_args(&argc, &argv, argv[1]+1);
	else if (argc > 1)
	{
		--argc;
		++argv;
	}

	m=parseargs(&argc, &argv);
	if (argc)	usage();	/* Some arguments left */

	(*m->open_func)(m);
	(*m->handler_func)(m);
	goodexit(m, 0);
	return (0);
}

static struct mimestruct *base64m;

static void putbase64(const char *p, size_t l)
{
	fwrite(p, 1, l, base64m->outputfp);
}

static void createsimplemime(struct mimestruct *m)
{
int	c;
struct	arg_list *a;

	/* Determine encoding by reading the file, as follows:
	**
	** Default to 7bit.  Use 8bit if high-ascii bytes found.  Use
	** quoted printable if lines more than 200 characters found.
	** Use base64 if a null byte is found.
	*/

	if (m->mimeencoding == 0)
	{
	int	l=0;
	int	longline=0;
	long	orig_pos=ftell(m->inputfp1);

		if (orig_pos == -1)
		{
			perror("ftell");
			goodexit(m, 1);
		}

		m->mimeencoding="7bit";

		while ((c=getc(m->inputfp1)) != EOF)
		{
		unsigned char ch= (unsigned char)c;

			if (ch >= 0x80)
				m->mimeencoding="8bit";
			if (ch == 0)
			{
				m->mimeencoding="base64";
				longline=0;
				break;
			}
			if (ch == '\n')	l=0;
			else if (++l > 200)
				longline=1;

		}
		if (ferror(m->inputfp1)
			|| fseek(m->inputfp1, orig_pos, SEEK_SET)<0)
		{
			perror("fseek");
			goodexit(m, 1);
		}
		if (longline)
			m->mimeencoding="quoted-printable";
	}


	for (a=m->a_first; a; a=a->next)
		fprintf(m->outputfp, "%s\n", a->arg);

	fprintf(m->outputfp, "Content-Type: %s\n"
			"Content-Transfer-Encoding: %s\n\n",
			m->mimetype, m->mimeencoding);

	if (strcmp(m->mimeencoding, "quoted-printable") == 0)
	{
	int	l=0;
	int	c;

		while ((c=getc(m->inputfp1)) != EOF)
		{
			if (l > 72)
			{
				fprintf(m->outputfp, "=\n");
				l=0;
			}
			if (c == '\n')
				l=0;
			else if (c < ' ' || c == '=' || c > 0x7F)
			{
				fprintf(m->outputfp, "=%02X",
					(int)(unsigned char)c);
				l += 3;
				continue;
			}
			else ++l;
			putc(c, m->outputfp);
		}
		return;
	}
	if (strcmp(m->mimeencoding, "base64") == 0)
	{
	char	buf[BUFSIZ];
	int	l;

		base64m=m;
		rfc2045_base64encode_start( &putbase64 );
		while ((l=fread(buf, 1, sizeof(buf), m->inputfp1)) > 0)
			rfc2045_base64encode(buf, l);
		rfc2045_base64encode_end();
		return;
	}

	/* 7bit or 8bit */

	while ((c=getc(m->inputfp1)) != EOF)
		putc(c, m->outputfp);
}

/******************************************************************************

Satisfy paranoia by making sure that the MIME boundary we picked does not
appear in the contents of the bounded section.

******************************************************************************/

static int tryboundary(struct mimestruct *m, FILE *f, const char *bbuf)
{
char	buf[BUFSIZ];
char	*p;
int	l=strlen(bbuf);
int	c;
long	orig_pos=ftell(f);

	if (orig_pos == -1)
	{
		perror("ftell");
		goodexit(m, 1);
	}

	while ((p=fgets(buf, sizeof(buf), f)) != 0)
	{
		if (p[0] == '-' && p[1] == '-' &&
			strncmp(p+2, bbuf, l) == 0)
			break;

		if ((p=strchr(buf, '\n')) != 0)
			*p=0;
		else while ((c=getc(f)) != EOF && c != '\n')
			;
	}

	if (ferror(f) || fseek(f, orig_pos, SEEK_SET)<0)
	{
		perror("fseek");
		goodexit(m, 1);
	}

	return (p ? 1:0);
}

/******************************************************************************

Create a MIME boundary for some content.

******************************************************************************/

static const char *mkboundary(struct mimestruct *m, FILE *f)
{
pid_t	pid=getpid();
time_t	t;
static unsigned n=0;
static char bbuf[NUMBUFSIZE*4];
char	buf[NUMBUFSIZE];

	time(&t);

	do
	{
		strcpy(bbuf, "=_");
		strcat(bbuf, str_size_t(++n, buf));
		strcat(bbuf, "_");
		strcat(bbuf, str_time_t(t, buf));
		strcat(bbuf, "_");
		strcat(bbuf, str_pid_t(pid, buf));
	} while (tryboundary(m, f, bbuf));
	return (bbuf);
}

static void createmultipartmime(struct mimestruct *m)
{
const char *b=mkboundary(m, m->inputfp1);
struct arg_list *a;
int	c;

	if (m->mimeencoding == 0)
		m->mimeencoding="8bit";

	for (a=m->a_first; a; a=a->next)
		fprintf(m->outputfp, "%s\n", a->arg);
	fprintf(m->outputfp, "Content-Type: %s; boundary=\"%s\"\n"
			"Content-Transfer-Encoding: %s\n\n"
			RFC2045MIMEMSG
			"\n--%s\n",
		m->mimetype, b,
		m->mimeencoding,
		b);
	while ((c=getc(m->inputfp1)) != EOF)
		putc(c, m->outputfp);
	fprintf(m->outputfp, "\n--%s--\n", b);
}

static void joinmultipart(struct mimestruct *m)
{
const char *new_boundary;
char	*old_boundary=0;
int	old_boundary_len=0;
char	buffer[BUFSIZ];
char	*p;
int	c;

	do
	{
		new_boundary=mkboundary(m, m->inputfp1);
	} while (tryboundary(m, m->inputfp2, new_boundary));

	/* Copy the header */

	for (;;)
	{
		if (fgets(buffer, sizeof(buffer), m->inputfp2) == 0)
		{
			buffer[0]=0;
			break;
		}

		if (buffer[0] == '\n' || strncmp(buffer, "--", 2) == 0)
			break;

		if (strncasecmp(buffer, "content-type:", 13))
		{
			fprintf(m->outputfp, "%s", buffer);
			if ((p=strchr(buffer, '\n')) != 0)	continue;
			while ((c=getc(m->inputfp2)) != EOF && c != '\n')
				putc(c, m->outputfp);
			continue;
		}

		if ((p=strchr(buffer, '\n')) == 0)
			while ((c=getc(m->inputfp2)) != EOF && c != '\n')
				;

		p=strchr(buffer+13, ';');
		if (p)	*p=0;
		fprintf(m->outputfp, "Content-Type:%s; boundary=\"%s\"\n",
			buffer+13, new_boundary);

		for (;;)
		{
			c=getc(m->inputfp2);
			if (c != EOF)	ungetc(c, m->inputfp2);
			if (c == '\n' || !isspace((int)(unsigned char)c))
				break;
			while ((c=getc(m->inputfp2)) != EOF && c != '\n')
				;
		}
	}

	do
	{
		if (strncmp(buffer, "--", 2) == 0)
		{
			if (old_boundary == 0)
			{
				old_boundary=malloc(strlen(buffer)+1);
				if (!old_boundary)
				{
					perror("malloc");
					exit(1);
				}
				strcpy(old_boundary, buffer);
				if ((p=strchr(old_boundary, '\n')) != 0)
					*p=0;
				p=old_boundary+strlen(old_boundary);
				if (p >= old_boundary+4 &&
					strcmp(p-2, "--") == 0)
				p[-2]=0;
				old_boundary_len=strlen(old_boundary);
			}


			if (strncasecmp(buffer, old_boundary,
				old_boundary_len) == 0)
			{
				if ((p=strchr(buffer, '\n')) != 0)
					*p=0;
				else while ((c=getc(m->inputfp2)) != '\n'
					&& c != EOF)
					;

				c=strlen(buffer);
				if (c >= 4 && strcmp(buffer+(c-2), "--") == 0)
					break;
				fprintf(m->outputfp, "--%s\n",
					new_boundary);
				continue;
			}
		}
		fprintf(m->outputfp, "%s", buffer);
		if ((p=strchr(buffer, '\n')) == 0)
			while ((c=getc(m->inputfp2)) != '\n' && c != EOF)
				;
	} while (fgets(buffer, sizeof(buffer), m->inputfp2) != 0);

	fprintf(m->outputfp, "--%s\n", new_boundary);

	while ((c=getc(m->inputfp1)) != EOF)
		putc(c, m->outputfp);

	fprintf(m->outputfp, "\n--%s--\n", new_boundary);
	goodexit(m, 0);
}

/******************************************************************************

Open input from a child process

******************************************************************************/

static FILE *openchild(struct mimestruct *parent, struct mimestruct *child,
	pid_t	*pidptr,
	int usescratch)
{
int	pipefd[2];
char	buf[NUMBUFSIZE];
char	buf2[NUMBUFSIZE+1];
FILE	*fp;

	if (pipe(pipefd) < 0)
	{
		perror("pipe");
		exit(1);
	}

	*pidptr=fork();

	if (*pidptr < 0)
	{
		perror("fork");
		exit(1);
	}

	if (*pidptr == 0)
	{
		/* Duplicate pipe on stdout */

		close(pipefd[0]);
		close(1);
		dup(pipefd[1]);
		close(pipefd[1]);

		/* Close any input files opened by parent */

		if (parent->inputfp1)	fclose(parent->inputfp1);
		if (parent->inputfp2)	fclose(parent->inputfp2);

		/* Open, then execute the child process */

		(*child->open_func)(child);
		(*child->handler_func)(child);
		goodexit(child, 0);
	}
	close(pipefd[1]);

	/*
	** Open the pipe by calling openfile(), automatically creating
	** the scratch file, if necessary.
	*/

	buf[0]='&';
	strcpy(buf+1, str_size_t(pipefd[0], buf2));

	fp= usescratch ? openfile(buf):openfile_or_pipe(buf, "r");
	close(pipefd[0]);	/* fd was duped by openfile */
	return (fp);
}

static void openoutput(struct mimestruct *m)
{
	if (!m->outputfile)
		m->outputfile="-";

	m->outputfp= openfile_or_pipe(m->outputfile, "w");
}

static void openjoinmultipart(struct mimestruct *m)
{
	/* number two is the multipart section */
	if (m->inputchild2)
		m->inputfp2=openchild(m, m->inputchild2, &m->child2, 1);
	else
		m->inputfp2=openfile(m->inputfile2);


	if (m->inputchild1)
		m->inputfp1=openchild(m, m->inputchild1, &m->child1, 1);
	else
		m->inputfp1=openfile(m->inputfile1);
	openoutput(m);
}

static void opencreatesimplemime(struct mimestruct *m)
{
	if (m->inputchild1)
		m->inputfp1=openchild(m, m->inputchild1, &m->child1,
			m->mimeencoding ? 0:1);
	else
		m->inputfp1= m->mimeencoding
			? openfile_or_pipe(m->inputfile1, "r")
			: openfile(m->inputfile1);
	openoutput(m);
}

static void opencreatemultipartmime(struct mimestruct *m)
{
	if (m->inputchild1)
		m->inputfp1=openchild(m, m->inputchild1, &m->child1, 1);
	else
		m->inputfp1=openfile_or_pipe(m->inputfile1, "r");
	openoutput(m);
}