/* $Header: main.c,v 2.12 90/03/07 11:00:58 chip Exp $
 *
 * A program to deliver local mail with some flexibility.
 *
 * $Log:	main.c,v $
 * Revision 2.12  90/03/07  11:00:58  chip
 * Debugging messages only when verbose.  (Sigh.)
 * 
 * Revision 2.11  90/03/06  13:11:53  chip
 * Run error delivery file after normal delivery;
 * then call delivery routines again afterward.
 * 
 * Revision 2.10  90/03/06  12:21:13  chip
 * Move logging into log.c and address parsing into addr.c.
 * New: error delivery file for messages that fail.
 * Major rearrangement of delivery file code.
 * 
 * Revision 2.9  90/02/23  15:05:15  chip
 * Clean up the setuid/setgid renunciation logic.
 * 
 * Revision 2.8  90/02/23  14:16:47  chip
 * Support "#!" in delivery files.
 * Support "user|program" and "user?error" from delivery files.
 * Improve debugging and error message formatting.
 * Rearrange code for clarity.
 * 
 * Revision 2.7  90/02/06  11:56:40  chip
 * Enforce MBX_MODE regardless of UMASK.
 * Enforce ordered logging with a log lockfile.
 * Revise log format.
 * 
 * Revision 2.6  89/12/19  16:45:26  network
 * Include <time.h> for the functions.
 * 
 * Revision 2.5  89/12/19  16:29:32  network
 * Make timezone handling portable to System V.
 * 
 * Revision 2.4  89/11/10  12:23:55  network
 * Handle recursion.  Enhance logging.
 * 
 * Revision 2.3  89/11/01  12:19:02  network
 * Delintify.
 * 
 * Revision 2.2  89/11/01  11:51:47  network
 * Add logging.
 * 
 * Revision 2.1  89/06/09  12:25:32  network
 * Update RCS revisions.
 * 
 * Revision 1.13  89/06/09  12:23:53  network
 * Baseline for 2.0 release.
 * 
 */

#include "deliver.h"
#include "patchlevel.h"
#include <signal.h>

/*
 * External data.
 */

/* Variables set by getopt() [blech] */

extern  int     optind, opterr;
extern  char    *optarg;

/*
 * Local data
 */

static  char    dfl_sys[] = SYS_DELIVER;
static  char    dfl_post[] = POST_DELIVER;
static  char    dfl_err[] = ERR_DELIVER;
static  char    dfl_user[] = USER_DELIVER;

/*
 * Global data
 */

int     verbose         = FALSE;
int     dryrun          = FALSE;
int     rundfiles       = TRUE;
int     printaddrs      = FALSE;
int     leavetemps      = FALSE;
int     boxdelivery     = FALSE;

char    *progname       = "deliver";
char    version[32]     = "2.0";
char    *shell          = SHELL;

int     rec_level       = 0;
int     rec_parent      = -1;
char    *sys_deliver    = dfl_sys;
char    *post_deliver   = dfl_post;
char    *err_deliver    = dfl_err;
char    *user_deliver   = dfl_user;
char    *sender         = NULL;
char    *hostname       = NULL;

int     eff_uid         = -1;
int     eff_gid         = -1;
int     real_uid        = -1;
int     real_gid        = -1;

CONTEXT *eff_ct         = NULL;
CONTEXT *real_ct        = NULL;

int     trust_user      = FALSE;
int     trust_delfiles  = FALSE;
FILE    *log            = NULL;
char    *logfile        = NULL;
FILE    *errlog         = NULL;
char    *errlogfile     = NULL;

int     tty_input       = FALSE;
SIGFLAG got_sig         = FALSE;

char    *ttype[T_MAX]   = { "header", "body", "header copy", "body copy" };
char    *tfile[T_MAX]   = { NULL, NULL, NULL, NULL };
char    *tenv[T_MAX]    = { NULL, NULL, ENV_HEADER, ENV_BODY };
int     tfd[T_MAX]      = { -1, -1, -1, -1 };

/*
 * Local functions.
 */

static  SIGTYPE sighup(), sigint(), sigquit();

/*----------------------------------------------------------------------
 * The Program.
 */

main(argc, argv)
int     argc;
char    **argv;
{
	char    *p;
	int     i, c, errcount, copy;

	/* Make sure that stdout and stderr are interleaved correctly */

	Linebuf(stdout);
	Linebuf(stderr);

	/* Figure out the name used to invoke this program. */

	progname = basename(argv[0]);

	/* Special hack -- handle the recursion level and parent first. */

	if ((p = getenv(ENV_DLEVEL)) != NULL && (i = atoi(p)) > 0)
		rec_level = i;

	if ((p = getenv(ENV_DPID)) != NULL && (i = atoi(p)) > 0)
		rec_parent = i;

	/* If recursion level is non-zero, append it to the program name. */

	if (rec_level > 0)
	{
		char    *np;

		np = zalloc((unsigned) strlen(progname) + 16);
		(void) sprintf(np, "%s[%d]", progname, rec_level);
		progname = np;
	}

	/* What version of the program is this? */

	(void) sprintf(version + strlen(version), ".%02d", PATCHLEVEL);

	/* Let's be sane about the file creation mask. */

	(void) umask(UMASK);

	/* Find effective and real uids and gids. */

	eff_uid = geteuid();
	eff_gid = getegid();
	real_uid = getuid();
	real_gid = getgid();

	/* Make sure that setuidness is reasonable. */

	if (eff_uid != real_uid && eff_uid != 0)
	{
		error("if setuid, must be setuid root\n");
		leave(1);
	}

	/* Process environment: handle recursive invocation. */

	if ((p = getenv(ENV_DFLAGS)) != NULL)
	{
		while (*p)
		{
			switch (*p++)
			{
			case 'v':
				verbose = TRUE;
				break;
			case 'd':
				verbose = TRUE;
				dryrun = TRUE;
				break;
			case 'A':
				printaddrs = TRUE;
				dryrun = TRUE;
				break;
			case 'n':
				rundfiles = FALSE;
				break;
			case 't':
				leavetemps = TRUE;
				break;
			}
		}
	}

	if ((p = getenv(ENV_SYSDEL)) != NULL && *p)
		sys_deliver = p;
	if ((p = getenv(ENV_POSTDEL)) != NULL && *p)
		post_deliver = p;
	if ((p = getenv(ENV_ERRDEL)) != NULL && *p)
		err_deliver = p;
	if ((p = getenv(ENV_USERDEL)) != NULL && *p)
		user_deliver = p;

	if ((p = getenv(ENV_SENDER)) != NULL && *p)
		sender = p;
	if ((p = getenv(ENV_HOSTNAME)) != NULL && *p)
		hostname = p;

	/* Parse command line arguments */

	while ((c = getopt(argc, argv, "vdAntbs:p:e:u:r:h:")) != EOF)
	{
		switch (c)
		{
		case 'v':
			verbose = TRUE;
			break;
		case 'd':
			verbose = TRUE;
			dryrun = TRUE;
			break;
		case 'A':
			printaddrs = TRUE;
			dryrun = TRUE;
			break;
		case 'n':
			rundfiles = FALSE;
			break;
		case 't':
			leavetemps = TRUE;
			break;
		case 'b':
			boxdelivery = TRUE;
			break;
		case 's':
			if (*optarg)
				sys_deliver = optarg;
			break;
		case 'p':
			if (*optarg)
				post_deliver = optarg;
			break;
		case 'e':
			if (*optarg)
				err_deliver = optarg;
			break;
		case 'u':
			if (*optarg)
				user_deliver = optarg;
			break;
		case 'r':
			if (*optarg)
				sender = optarg;
			break;
		case 'h':
			if (*optarg)
				hostname = optarg;
			break;
		case '?':
			usage();
		}
	}

	/* Open temporary log files. */

	openlogs();

	/* Figure out the name of this host, unless we've been told. */

	if (!hostname && (hostname = gethost()) == NULL)
	{
		hostname = "unknown";
		error("unable to determine host name; using \"%s\"\n",
		      hostname);
	}

	/* If no destinations were given, forget it. */

	if (optind >= argc)
	{
		error("no recipients specified\n");
		usage();
	}

	/* If debugging, print a nice banner. */

	if (verbose)
	{
		message("%s %s running on host %s\n",
			progname, version, hostname);
	}

	/*
	 * Renounce setuid privileges if:
	 *   1.  We don't trust the real user id, OR
	 *   2.  The caller is using non-default delivery file names.
	 */

	if (trusted_uid(real_uid))
		trust_user = TRUE;

	if (strcmp(dfl_sys, sys_deliver) == 0
	 && strcmp(dfl_post, post_deliver) == 0
	 && strcmp(dfl_err, err_deliver) == 0
	 && strcmp(dfl_user, user_deliver) == 0)
		trust_delfiles = TRUE;

	if ((!trust_user && !trust_delfiles)
	 && (eff_uid != real_uid || eff_gid != real_gid))
	{
		if (setuid(real_uid) == -1)
		{
			syserr("can't renounce setuid privileges");
			leave(1);
		}
		if (setgid(real_gid) == -1)
		{
			syserr("can't renounce setgid privileges");
			leave(1);
		}

		if ((eff_uid = geteuid()) != real_uid
		 || (eff_gid = getegid()) != real_gid)
		{
			error("kernel bug: can't renounce setuid/setgid");
			leave(1);
		}

		if (verbose)
			message("renounced setuid privileges\n");
	}

	/* Get the contexts of our effective and real uids. */

	if ((eff_ct = uid_context(eff_uid)) == NULL)
		error("invalid effective uid %d!?\n", eff_uid);

	if ((real_ct = uid_context(real_uid)) == NULL)
		error("invalid real uid %d!?\n", real_uid);

	if (!eff_ct || !real_ct)
		leave(1);

	if (verbose)
	{
		message("effective uid = %s (%d/%d); real uid = %s (%d/%d)\n",
			eff_ct->ct_name, eff_ct->ct_uid, eff_ct->ct_gid,
			real_ct->ct_name, real_ct->ct_uid, real_ct->ct_gid);
	}

	/*
	 * Where is the message coming from?
	 */

	if (isatty(0))
		tty_input = TRUE;

	/*
	 * If we are not going to deliver, or if we are receiving the
	 * message from a tty, catch signals so we can remove temp files.
	 * Otherwise, ignore signals.
	 */

	if (dryrun || tty_input)
		catch_sigs();
	else
		ignore_sigs();

	/*
	 * Create the temporary files and write the message to them.
	 */

	copy = copy_message();

	/*
	 * No more signals...
	 */

	ignore_sigs();

	/*
	 * ... but if we had already caught a signal,
	 *     or if copy_msg() had a problem, leave.
	 */

	if ((copy < 0) || got_sig)
	{
		if (got_sig)
			error("caught signal - exiting\n");
		leave(1);
	}

	/*
	 * Set up useful environment variables.
	 * Note that this must be done _after_ copy_message(),
	 * since that's where the temp files are created.
	 */

	setup_environ();

	/*
	 * If recursion is too deep, consider mail undeliverable.
	 */

	if (rec_level > REC_LIMIT)
		main_toodeep();

	/*
	 * Else if the "-b" flag was specified, arguments are mailboxes.
	 */

	else if (boxdelivery)
		main_boxes(argc - optind, argv + optind);

	/*
	 * Otherwise, arguments are addresses.
	 */

	else
		main_addrs(argc - optind, argv + optind);

	/*
	 * Do all delivery.
	 * If anything happens, print a debugging message.
	 */

	i = 0;
	i += (mbox_deliver() > 0);
	i += (prog_deliver() > 0);
	i += (uucp_deliver() > 0);
	if (i > 0 && verbose)
		dumpdests("after delivery");

	/*
	 * If we're not in mailbox mode, and if the error delivery file
	 * executes, deliver to any destination(s) that it generated.
	 */

	if (!boxdelivery
	 && (i = err_dfile()) > 0)
	{
		if (verbose)
			dumpdests("after running error delivery file");

		i = 0;
		i += (mbox_deliver() > 0);
		i += (prog_deliver() > 0);
		i += (uucp_deliver() > 0);
		if (i > 0 && verbose)
			dumpdests("after delivery to error destinations");
	}

	/*
	 * Report all results in log file.
	 */

	logreport(argc - optind, argv + optind);

	/*
	 * Report any errors.
	 */

	errcount = report_errors(argc - optind, argv + optind);

	/*
	 * All done.
	 */

	leave(errcount ? 1 : 0);
	/* NOTREACHED */
}

/*----------------------------------------------------------------------
 * Normal address handling.
 * Call system delivery file, user delivery files,
 * post-user delivery file, error delivery file.
 */

main_addrs(ac, av)
int	ac;
char	**av;
{
	int	n;

	/* Run all destinations though the system delivery file. */

	if (sys_dfile(ac, av) >= 0)
	{
		if (verbose)
			dumpdests("after running system delivery file");
	}
	else
	{
		int     a;

		/*
		 * System delivery file is missing or ignored.
		 * Use the argument list verbatim.
		 */

		for (a = 0; a < ac; ++a)
			(void) addr_dest(av[a], (CONTEXT *)NULL);

		if (verbose)
			dumpdests("as taken from argument list");
	}

	/*
	 * Run each user destination through his delivery file.
	 */

	n = user_dfiles();
	if (n > 0 && verbose)
		dumpdests("after running user delivery files");

	/*
	 * Run each remaining destination though the post-user
	 * delivery file.
	 */

	n = post_dfile();
	if (n > 0 && verbose)
		dumpdests("after running post-user delivery file");
}

/*----------------------------------------------------------------------
 * Consider all arguments as mailbox filenames.
 */

main_boxes(ac, av)
int	ac;
char	**av;
{
	int     a;

	if (verbose)
		message("mailbox delivery as %s\n", real_ct->ct_name);

	for (a = 0; a < ac; ++a)
		(void) dest(real_ct->ct_name, CL_MBOX, av[a]);

	if (verbose)
		dumpdests("(should all be mailboxes)");
}

/*----------------------------------------------------------------------
 * Recursion too deep.  Bail out.
 */

main_toodeep()
{
	error("recursion limit (%d) exceeded; writing to %s:%s",
	      REC_LIMIT, eff_ct->ct_name, MBX_UNDEL);

	dest_undel(eff_ct->ct_name);
}

/*----------------------------------------------------------------------
 * Print a usage message and exit.
 */

usage()
{
	message("Usage: %s [-b][-A][-d][-v][-n][-t][-r from][-h host] args\n", progname);
	message("-b       All arguments are mailbox filenames.\n");
	message("         (Default: arguments are user names.)\n");
	message("-A       Resolve addresses but do not deliver.\n");
	message("-d       Be verbose but do not deliver.\n");
	message("-v       Be verbose and deliver.\n");
	message("-n       Do not run any delivery files.\n");
	message("-t       Do not remote temp files before exiting.\n");
	message("-s file  Specify the system delivery filename.\n");
	message("-p file  Specify the post-user delivery filename.\n");
	message("-e file  Specify the error delivery filename.\n");
	message("-u file  Specify the user delivery filename.\n");
	message("-r from  Specify the address to appear in the \"From \" line.\n");
	message("-h host  Specify the host name.\n");
	message("args     Either user addresses or mailboxes (-b).\n");
	leave(1);
}

/*----------------------------------------------------------------------
 * Clean up and exit.
 */

leave(code)
int     code;
{
	/* Report vital statistics if something went wrong. */

	errinfo();

	/* Discard temporary files. */

	if (! leavetemps)
	{
		int     t;

		for (t = 0; t < T_MAX; ++t)
		{
			if (tfd[t] != -1)
				(void) close(tfd[t]);
			if (tfile[t] && unlink(tfile[t]) == -1)
				syserr("can't unlink %s", tfile[t]);
		}
	}

	/* Save temporary logs by appending to real logfiles. */

	savelogs();

	/* Discard temporary logs unless user requested otherwise. */

	if (!leavetemps)
		tosslogs();

	/* "I am outa here." */

	exit(code);
}

/*----------------------------------------------------------------------
 * Catch signals.
 */

catch_sigs()
{
	if (signal(SIGHUP, SIG_IGN) != SIG_IGN)
		(void) signal(SIGHUP, sighup);
	if (signal(SIGINT, SIG_IGN) != SIG_IGN)
		(void) signal(SIGINT, sigint);
	if (signal(SIGQUIT, SIG_IGN) != SIG_IGN)
		(void) signal(SIGQUIT, sigquit);
}

/*----------------------------------------------------------------------
 * Ignore signals.
 */

ignore_sigs()
{
	(void) signal(SIGHUP, SIG_IGN);
	(void) signal(SIGINT, SIG_IGN);
	(void) signal(SIGQUIT, SIG_IGN);
}

static SIGTYPE
sighup()
{
	(void) signal(SIGHUP, sighup);
	got_sig = TRUE;
}

static SIGTYPE
sigint()
{
	(void) signal(SIGINT, sigint);
	got_sig = TRUE;
}

static SIGTYPE
sigquit()
{
	(void) signal(SIGQUIT, sigquit);
	got_sig = TRUE;
}

/*----------------------------------------------------------------------
 * Report any errors to stderr.
 * Return an error count.
 */

int
report_errors(ac, av)
int	ac;
char	**av;
{
	DEST	**dv;
	int	i, count, errcount;

	if ((dv = dest_array(&count)) == NULL)
		return 0;

	errcount = 0;
	for (i = 0; i < count; ++i)
	{
		DEST    *d;
		char	*e;
		int	len;

		d = dv[i];

		if (d->d_state != ST_ERROR)
			continue;

		if (++errcount == 1)
		{
			int	a;

			error("delivery error on host %s.\n", hostname);
			message("Delivery to %s address%s failed:\n",
				ac ? "these" : "this",
				ac ? "es" : "");
			for (a = 0; a < ac; ++a)
				message("\t%s\n", av[a]);
			message("Reason(s) for failure:\n");
		}

		message("\t\"%s\"", d->d_name);
		if (d->d_class == CL_MBOX)
			message(", mailbox \"%s\"", d->d_param);
		else if (d->d_class == CL_PROG)
			message(", program \"%s\"", d->d_param);

		e = derrmsg(d);
		len = strlen(d->d_name) + strlen(e);
		if (d->d_param)
			len += strlen(d->d_param);
		message(":%s%s\n", (len > 60) ? "\n\t\t" : " ", derrmsg(d));
	}

	free((char *) dv);

	return errcount;
}

/*----------------------------------------------------------------------
 * Is the given uid trusted?
 */

int
trusted_uid(uid)
int     uid;
{
	CONTEXT *ct;
	char    **n;
	static char *t[] = { TRUSTED_USERS, 0 };

	for (n = t; *n; ++n)
	{
		if ((ct = name_context(*n)) != NULL && uid == ct->ct_uid)
			return TRUE;
	}

	return FALSE;
}

/*----------------------------------------------------------------------
 * Set up useful environment variables.
 */

setup_environ()
{
	char    s[8];
	int     f = 0;

	(void) sprintf(s, "%d", getpid());
	alloc_env(ENV_DPID, s);

	(void) sprintf(s, "%d", rec_level + 1);
	alloc_env(ENV_DLEVEL, s);

	s[f++] = '-';
	if (verbose)
		s[f++] = (dryrun ? 'd' : 'v');
	if (printaddrs)
		s[f++] = 'A';
	if (leavetemps)
		s[f++] = 't';
	s[f] = 0;
	alloc_env(ENV_DFLAGS, (f > 1) ? s : "");

	if (sys_deliver && *sys_deliver)
		alloc_env(ENV_SYSDEL, sys_deliver);
	if (user_deliver && *user_deliver)
		alloc_env(ENV_USERDEL, user_deliver);
	if (hostname && *hostname)
		alloc_env(ENV_HOSTNAME, hostname);
	if (sender && *sender)
		alloc_env(ENV_SENDER, sender);

	alloc_env("IFS", " \t\n");
	del_env("ENV");         /* in case SHELL is ksh */
}
