/*

    #     ####            ####
    #    #    #          #    #
    #    #    #          #
    #    #    #   ###    #
    #    #    #   ###    #    #
    #     ####    ###     ####

	I/O routines.
*/

/*
 * $Id: io.c,v 3.0.1.17 2001/01/10 16:48:56 ram Exp $
 *
 *  Copyright (c) 1990-1993, Raphael Manfredi
 *  
 *  You may redistribute only under the terms of the Artistic License,
 *  as specified in the README file that comes with the distribution.
 *  You may reuse parts of this distribution only within the terms of
 *  that same Artistic License; a copy of which may be found at the root
 *  of the source tree for mailagent 3.0.
 *
 * $Log: io.c,v $
 * Revision 3.0.1.17  2001/01/10 16:48:56  ram
 * patch69: switched to dynamic init of standard file array for GNU libc
 *
 * Revision 3.0.1.16  1999/07/12  13:43:57  ram
 * patch66: renamed metaconfig obsolete symbol
 * patch66: now uses say() when mail is DUMPED
 *
 * Revision 3.0.1.15  1999/01/13  18:06:18  ram
 * patch64: fixed wrong localization of variables in unique_filename()
 * patch64: additions to agent.wait are more robust and use locking
 *
 * Revision 3.0.1.14  1998/07/28  17:32:33  ram
 * patch63: unique_filename() could loop forever, abort if no locking
 *
 * Revision 3.0.1.13  1998/07/28  16:57:41  ram
 * patch62: fixed race condition whilst electing queue filename
 *
 * Revision 3.0.1.12  1997/09/15  15:01:35  ram
 * patch57: factorized code to derive a unique filename
 *
 * Revision 3.0.1.11  1997/02/20  11:35:20  ram
 * patch55: new io_redirect() routine to handle the -o switch
 *
 * Revision 3.0.1.10  1996/12/26  10:46:11  ram
 * patch51: include <unistd.h> for R_OK and define fallbacks
 * patch51: declared strsave() in case it's not done in <string.h>
 * patch51: fixed an incredible typo while declaring progpath[]
 *
 * Revision 3.0.1.9  1996/12/24  13:57:08  ram
 * patch45: message is now read in a pool, instead of one big chunk
 * patch45: attempt to locate mailagent and perl
 * patch45: perform basic security checks on exec()ed programs
 *
 * Revision 3.0.1.8  1995/08/31  16:19:17  ram
 * patch42: new routine write_fd() to write mail onto an opened file
 * patch42: write_file() now relies on new write_fd() to do its main job
 * patch42: read_stdin() was made a once routine
 * patch42: emergency_save() now attempts to read mail if not done already
 * patch42: emergency_save() will dump message on stdout as a fall back
 *
 * Revision 3.0.1.7  1995/08/07  17:23:26  ram
 * patch41: forgot to return value in agent_lockfile()
 *
 * Revision 3.0.1.6  1995/08/07  16:09:03  ram
 * patch37: avoid forking of a new mailagent if one is sitting in background
 * patch37: added support for locking on filesystems with short filenames
 *
 * Revision 3.0.1.5  1995/03/21  12:54:19  ram
 * patch35: now relies on USE_WIFSTAT to use WIFEXITED() and friends
 *
 * Revision 3.0.1.4  1995/02/03  17:55:55  ram
 * patch30: avoid closing stdio files if not connected to a tty
 *
 * Revision 3.0.1.3  1994/10/04  17:25:54  ram
 * patch17: now detect and avoid possible queue filename conflicts
 *
 * Revision 3.0.1.2  1994/07/01  14:52:04  ram
 * patch8: now honours the queuewait config variable when present
 *
 * Revision 3.0.1.1  1994/01/26  09:27:13  ram
 * patch5: now only try to include <sys/fcntl.h> when hope is lost
 * patch5: filter will now put itself in daemon state while waiting
 *
 * Revision 3.0  1993/11/29  13:48:10  ram
 * Baseline for mailagent 3.0 netwide release.
 *
 */

#include "config.h"
#include "portable.h"
#include <sys/types.h>
#include <stdio.h>
#include <errno.h>
#include <sys/stat.h>

#ifdef I_UNISTD
#include <unistd.h>		/* R_OK and friends */
#endif

#ifdef I_SYS_WAIT
#include <sys/wait.h>
#endif

#ifdef I_FCNTL
#include <fcntl.h>
#endif
#ifdef I_SYS_FILE
#include <sys/file.h>
#endif

#ifndef I_FCNTL
#ifndef I_SYS_FILE
#include <sys/fcntl.h>	/* Try this one in last resort */
#endif
#endif

#ifdef I_STRING
#include <string.h>
#else
#include <strings.h>
#endif

#ifdef I_SYS_IOCTL
#include <sys/ioctl.h>
#endif

/*
 * The following should be defined in <sys/stat.h>.
 */
#ifndef S_IFMT
#define S_IFMT	0170000		/* Type of file */
#endif
#ifndef S_IFREG
#define S_IFREG	0100000		/* Regular file */
#endif
#ifndef S_IFCHR
#define S_IFCHR	0020000		/* Character special (ttys fall into that) */
#endif
#ifndef S_ISCHR
#define S_ISCHR(m)	(((m) & S_IFMT) == S_IFCHR)
#endif
#ifndef S_ISREG
#define S_ISREG(m)	(((m) & S_IFMT) == S_IFREG)
#endif

#ifndef X_OK
#define X_OK	1			/* Test for execute (search) permission */
#endif
#ifndef R_OK
#define R_OK	4			/* Test for read permission */
#endif

#include "hash.h"
#include "parser.h"
#include "lock.h"
#include "logfile.h"
#include "environ.h"
#include "sysexits.h"
#include "confmagic.h"

#define BUFSIZE		1024			/* Amount of bytes read in a single call */
#define CHUNK		(10 * BUFSIZE)	/* Granularity of pool */
#define CHUNKSIZE	(10 * CHUNK)	/* Maximum size of realloc()ed pool */
#define MAX_STRING	2048			/* Maximum string's length */
#define MAX_TRYS	1024			/* Maximum attempts for unique queue file */
#define QUEUE_WAIT	60				/* Default waiting time in queue */
#define AGENT_WAIT	"agent.wait"	/* File listing out-of-the-queue mails */
#define DEBUG_LOG	12				/* Lowest debugging log level */
#ifdef FLEXFILENAMES
#define AGENT_LOCK	"perl.lock"		/* Lock file used by mailagent */
#else
#define AGENT_LOCK	"perl!"			/* Same as above, if filename length < 14 */
#endif

/* The following array of stdio streams is used by close_tty() */
private FILE *stream_by_fd[] = {
	0,		/* file descriptor 0 */
	0,		/* ... 1 */
	0,		/* ... 2 */
};
#define STDIO_FDS	(sizeof(stream_by_fd) / sizeof(FILE *))

private char *agent_lockfile();	/* Name of the mailagent lock */
private int get_lock();			/* Attempt to get a lockfile */
private void release_agent();	/* Remove mailagent's lock if needed */
private int process_mail();		/* Process mail by feeding the mailagent */
private void queue_mail();		/* Queue mail for delayed processing */
private char *write_file();		/* Write mail on disk */
private int write_fd();			/* Write mail onto file descriptor */
private char *save_file();		/* Emergency saving into a file */
private void goto_daemon();		/* Disassociate process from tty */

/*
 * Mail is stored in a linked list of chunks, each chunk being able to
 * contain at most CHUNKSIZE bytes. This prevents exponential memory
 * consumption when facing messages two or three order of magnitude bigger
 * than the basic CHUNK granularity.
 */
struct mail {
	struct pool *first;			/* Data description in successive pools */
	struct pool *last;			/* Last created pool */
	int len;					/* Mail length in bytes */
};
struct pool {
	struct pool *next;			/* Pool data */
	char *arena;				/* Data for this pool chunk */
	int size;					/* Size of data that can be held in arena */
	int offset;					/* Filling offset */
};
private int queued = 0;			/* True when mail queued safely */
private struct mail mail;		/* Where mail is expected to lie */

extern int errno;				/* System call error status */
extern char *malloc();			/* Memory allocation */
extern char *realloc();			/* Re-allocation of memory pool */
extern char *logname();			/* User's login name */
extern char *strsave();			/* Save string somewhere in core */
extern int loglvl;				/* Logging level */

private struct pool *pool_alloc(size)
{
	/* Allocate a new pool of given size, or fail if not enough memory */

	struct pool *lp;

	lp = (struct pool *) malloc(sizeof(struct pool));
	if (!lp)
		goto failed;
	
	lp->arena = malloc(size);
	if (!lp->arena)
		goto failed;
	
	lp->size = size;
	lp->offset = 0;				/* Nothing read yet */
	lp->next = 0;				/* Assume we're at the end of chain */

	return lp;

failed:
	fatal("out of memory");
}

private struct pool *pool_init(size)
int size;
{
	/* Initialize mail by creating the first pool */

	mail.first = mail.last = pool_alloc(size);
	mail.len = 0;

	return mail.last;
}

private void pool_extend(pool, size)
struct pool **pool;
int size;
{
	/* Make more room in pool and update parameters accordingly. If we
	 * can extend the size of the pool because we're under the CHUNKSIZE
	 * limit, then do so. Otherwise, allocate a new pool and link it to
	 * the current one, then update our caller's view to point to the new
	 * pool so new allocation is quasi-transparent.
	 */

	struct pool *lp = *pool;	/* Current pool */
	struct pool *np;			/* New pool, if necessary */

	if (lp->size < CHUNKSIZE) {
		lp->size += size;
		lp->arena = realloc(lp->arena, lp->size);
		if (!lp->arena)
			fatal("out of memory");
		return;
	}

	/* Need to create a new pool */

	mail.last = np = pool_alloc(size);
	lp->next = np;
	*pool = np;					/* Update our caller's view */
}

private void pool_read(pool, buf, len)
struct pool **pool;
char *buf;				/* Where data lie */
int len;				/* Amount of data in buf to transfer */
{
	struct pool *lp = *pool;		/* Local pool pointer */
	int fit;						/* Amount of data that can fit */

	while (fit = len) {				/* Assume everything will fit */
		if ((lp->offset + len) > lp->size)
			fit = lp->size - lp->offset;

		bcopy(buf, lp->arena + lp->offset, fit);
		lp->offset += fit;
		mail.len += fit;

		len -= fit;
		if (len == 0)
			return;					/* Everything fitted in pool */

		pool_extend(&lp, CHUNK);	/* Resize it or fail */
		*pool = lp;					/* Update our caller's view */
	}
}

private int pool_write(fd, pool)
int fd;
struct pool *pool;
{
	/* Write the pool onto fd. We do not call a single write on the pool areana
	 * as in "write(fd, buf, len)" in case the pool length exceeds the maximum
	 * amount of bytes the system can atomically write.
	 *
	 * Returns the amount of bytes written, or -1 on error.
	 */

	register1 char *mailptr;		/* Pointer into arena buffer */
	register2 int length;			/* Number of bytes already written */
	register3 int amount;			/* Amount of bytes written by last call */
	register4 int n;				/* Result from the write system call */
	register5 int len;				/* Bytes remaining to be written */
	
	for (
		mailptr = pool->arena, length = 0, len = pool->offset;
		length < len;
		mailptr += amount, length += amount
	) {
		amount = len - length;
		if (amount > BUFSIZ)		/* Do not write more than BUFSIZ */
			amount = BUFSIZ;
		n = write(fd, mailptr, amount);
		if (n == -1 || n != amount) {
			if (n == -1)
				add_log(1, "SYSERR write: %m (%e)");
			return -1;
		}
	}

	return length;
}

private void read_stdin()
{
	/* Read the whole stdandard input into memory and return a pointer to its
	 * location in memory. Any I/O error is fatal. Set the length of the
	 * data read into 'len'.
	 *
	 * This routine may be called from two distinct places, but should only
	 * run once, for obvious reasons...
	 */

	int amount = 0;				/* Total amount of data read */
	int n;						/* Bytes read by last system call */
	struct pool *pool;			/* Current pool where input is stored */
	char buf[BUFSIZE];
	static int done = 0;		/* Ensure routine is run once only */

	if (done++) return;

	add_log(19, "reading mail");

	pool = pool_init(CHUNK);

	while (n = read(0, buf, BUFSIZE)) {
		if (n == -1) {
			add_log(1, "SYSERR read: %m (%e)");
			fatal("I/O error while reading mail");
		}
		pool_read(&pool, buf, n);		/* Copy read bytes */
		amount += n;					/* Update amount of bytes read */
	}

	if (amount != mail.len)
		fatal("corrupted mail: read %d bytes, now has %d", amount, mail.len);

	add_log(16, "got mail (%d bytes)", amount);
}

public void process()
{
	char *queue;						/* Location of mailagent's queue */

	(void) umask(077);					/* Files we create are private ones */

	queue = ht_value(&symtab, "queue");	/* Fetch queue location */
	if (queue == (char *) 0)
		fatal("queue directory not defined");

	read_stdin();						/* Read mail */
	(void) get_lock();					/* Get a lock file */
	queue_mail(queue);					/* Process also it locked */
	release_lock();						/* Release lock file if necessary */
}

public int was_queued()
{
	return queued;			/* Was mail queued? */
}

private int is_main()
{
	/* Test whether we are the main filter (i.e. whether or not we are
	 * entitled to launch a new mailagent process). This is the case when
	 * we were able to grab a filter lock, in which case is_locked() returns
	 * true.
	 *
	 * However, it is also possible a mailagent process put in the background
	 * be processing some mail, albeit the main filter that originally launched
	 * it disappeared. Therefore, we also check for an existing mailagent lock,
	 * when we are locked.
	 */

	char *agentlock;			/* Path of the mailagent lock file */
	struct stat buf;			/* Stat buffer */
	static int done = 0;
	static int result = 0;		/* Assume we're not the main filter */

	if (done)
		return result;
	done = 1;

	if (!is_locked())
		return 0;		/* We're not a main filter, one is already active */

	/*
	 * No filter lock held, we are a candidate for being a main filter!
	 */

	agentlock = agent_lockfile();
	if (-1 == stat(agentlock, &buf)) {
		if (errno != ENOENT) {
			add_log(1, "SYSERR stat: %m (%e)");
			add_log(2, "ERROR cannot stat %s", agentlock);
		}
		return result = 1;	/* No mailagent is currently active */
	}

	if (LOCK_OLD == check_lock(agentlock, "mailagent")) {
		release_agent();
		return result = 1;	/* Old lockfile removed, assume we're the main */
	}

	add_log(5, "NOTICE mailagent seems to be active in background");
	return 0;
}

private int get_lock()
{
	/* Try to get a filter lock in the spool directory. Propagate the return
	 * status of filter_lock(): 0 for ok, -1 for failure.
	 */

	char *spool;						/* Location of spool directory */

	spool = ht_value(&symtab, "spool");	/* Fetch spool location */
	if (spool == (char *) 0)
		fatal("spool directory not defined");

	return filter_lock(spool);			/* Get a lock in spool directory */
}

private char *agent_lockfile()
{
	/* Once function used to compute the path of maialagent's lock file */

	char *spool;					/* Location of spool directory */
	static int done = 0;
	static char agentlock[MAX_STRING];	/* Result */

	if (done)
		return agentlock;

	done = 1;

	spool = ht_value(&symtab, "spool");	/* Fetch spool location */
	if (spool == (char *) 0)			/* Should not happen */
		spool = "";

	sprintf(agentlock, "%s/%s", spool, AGENT_LOCK);
	add_log(12, "mailagent lock in %s", agentlock);

	return agentlock;
}

private void release_agent()
{
	/* In case of abnormal failure, the mailagent may leave its lock file
	 * in the spool directory. Remove it if necessary.
	 */

	struct stat buf;				/* Stat buffer */
	char *agentlock = agent_lockfile();

	if (-1 == stat(agentlock, &buf))
		return;						/* Assume no lock file left behind */

	if (-1 == unlink(agentlock)) {
		add_log(1, "SYSERR unlink: %m (%e)");
		add_log(2, "ERROR could not remove mailagent's lock");
	} else
		add_log(5, "NOTICE removed mailagent's lock");
}

private int unique_filename(buf, format, dir, base)
char buf[];
char *format;
char *dir;
char *base;
{
	/* Compute unique filename in directory dir and store it in buf. The
	 * generated file name will be something like dir/base<pid>, but it
	 * really depends on the value of the sprintf format, usually "%s%d".
	 * A trailing %c may be appended to the format to help getting a unique
	 * name.
	 * Returns opened file descriptor on the elected file ("locked"), or -1.
	 */

	char fmt[MAX_STRING];	/* Final sprintf() format string */
	int try = 0;			/* Count attempts to find a unique filename */
	int alternate = 0;		/* True when alternate naming was chosen */
	char trailer = '\0';	/* Trailer character after pid */

	sprintf(fmt, "%s%s%s", "%s/", format, "%c");

	for (;;) {
		int fd;					/* Opened file */

		sprintf(buf, fmt, dir, base, progpid + try, trailer);

		/*
		 * Must "lock" the file in the queue in case mailagent happens to be
		 * sleeping in the background and wakes up between the time we create
		 * the file and before we get a chance to rename() the temporary file.
		 * When that happens, an empty message would be processed and if the
		 * file system is NFS-mounted, it could be rename()-ed and then unlinked
		 * by mailagent after a so-called "successful" processing... thereby
		 * loosing the message completely.
		 */

		if (-1 == file_lock(buf, buf, 0)) {
			add_log(6, "NOTICE could not lock %s", buf);
			return -1;
		}

		fd = open(buf, O_WRONLY | O_CREAT | O_EXCL, 0600);
		if (fd != -1)
			return fd;			/* Returns "locked" pathname */

		if (errno != EEXIST) {
			add_log(1, "SYSERR open: %m (%e)");
			add_log(2, "ERROR can't create %s", buf);
			file_unlock(buf);
			return -1;
		}

		/*
		 * Must do that now to preserve the value of errno in the test above
		 * for the sake of %m and %e. Alternative would be to save/restore
		 * errno before calling add_log().
		 */

		file_unlock(buf);

		if (++try > MAX_TRYS) {
			if (alternate > ('z' - 'a'))
				fatal("unable to find unique queue filename");
			try = 0;
			trailer = 'a' + alternate++;	/* ASCII-dependant */
		}
	}

	return -1;				/* Unable to find unique filename in dir */
}

private void queue_mail(queue)
char *queue;				/* Location of the queue directory */
{
	char *where;			/* Where mail is stored */
	char real[MAX_STRING];	/* Real queue mail */
	char *base;				/* Pointer to base name */
	struct stat buf;		/* To make sure queued file remains */
	char *type;				/* "qm" or "fm" mails */
	int fd;					/* fd from unique_filename() */

	where = write_file(queue, "Tm");
	if (where == (char *) 0) {
		add_log(1, "ERROR unable to queue mail");
		fatal("try again later");
	}

	/* If we have a lock, create a qm* file suitable for mailagent processing.
	 * Otherwise, create a fm* file and the mailagent will process it
	 * immediately. Because of my paranoid nature, we loop at least MAX_TRYS
	 * to get a unique queue filename (duplicates may happen if mail is
	 * delivered on distinct machines simultaneously with an NFS-mounted queue).
	 * If that's not enough. we try again once for each letter in the alphabet,
	 * adding it as a trailer character for better uniqueness.
	 */

	type = is_main() ? "qm" : "fm";

	if (-1 == (fd = unique_filename(real, "%s%d", queue, type)))
		fatal("unable to find unique queue filename");
	(void) close(fd);

	if (-1 == rename(where, real)) {
		add_log(1, "SYSERR rename: %m (%e)");
		add_log(2, "ERROR could not rename %s into %s", where, real);
		file_unlock(real);		/* Locked by unique_filename() */
		fatal("try again later");
	}

	/* Compute base name of queued mail */
	base = rindex(real, '/');
	if (base++ == (char *) 0)
		base = real;

	add_log(4, "QUEUED [%s] %d bytes", base, mail.len);
	queued = 1;
	file_unlock(real);			/* Better have this after logging QUEUED */

	/* If we got a lock, then no mailagent is running and we may process the
	 * mail. Otherwise, do nothing. The mail will be processed by the currently
	 * active mailagent.
	 */

	if (!is_main())				/* Another mailagent is running */
		return;					/* Leave mail in queue */

	if (0 == process_mail(real)) {
		/* Mailagent may have simply queued the mail for itself by renaming
		 * it, so of course we would not be able to remove it. Hence the
		 * test for ENOENT to avoid error messages when the file does not
		 * exit any more.
		 */
		if (-1 == unlink(real) && errno != ENOENT) {
			add_log(1, "SYSERR unlink: %m (%e)");
			add_log(2, "ERROR could not remove queued mail");
		}
		return;
	}
	/* Paranoia: make sure the queued mail is still there */
	if (-1 == stat(real, &buf)) {
		queued = 0;			/* Or emergency_save() would not do anything */
		add_log(1, "SYSERR stat: %m (%e)");
		add_log(1, "ERROR queue file [%s] vanished", base);
		if (-1 == emergency_save())
			add_log(1, "ERROR mail probably lost");
	} else {
		add_log(4, "WARNING mailagent failed, [%s] left in queue", base);
		release_agent();	/* Remove mailagent's lock file if needed */
	}
}

private char *locate(prog, path)
char *prog;			/* Program to locate within path */
char *path;			/* The path under which we should look for program */
{
	/* Locate specified program within the `path' and return pointer to static
	 * data with the full path of the program when found, or a null pointer
	 * otherwise.
	 */
	
	static char progpath[MAX_STRING + 1];
	char *cp;					/* Current path pointer */
	char *ep;					/* End of current path component */
	int len;					/* Length of current path component */
	struct stat buf;			/* Stat buffer */
	int proglen = strlen(prog);

	/*
	 * Loop over the path and extract components between `:'. The `cp'
	 * variable points to the beginning of the place to look for the next
	 * component.
	 */

	for (ep = cp = path; ep && *cp; cp = ep ? (ep + 1) : ep) {
		ep = index(cp, ':');				/* Lookup next `:' separator */
		len = ep ? (ep - cp) : strlen(cp);	/* Slurp remaining if not found */
		if ((len + proglen + 1) > MAX_STRING) {
			add_log(4, "WARNING skipping directory while looking for %s", prog);
			continue;
		}
		if (len) {
			strncpy(progpath, cp, len);		/* Will not add trailing '\0' */
			progpath[len] = '\0';
		} else
			strcpy(progpath, ".");			/* Good old "null" path field */
		strcat(progpath, "/");
		strcat(progpath, prog);
		if (-1 == stat(progpath, &buf))		/* No entry in file system */
			continue;
		if (!S_ISREG(buf.st_mode))			/* Not a regular file */
			continue;
		if (-1 == access(progpath, R_OK|X_OK)) {
			add_log(4, "WARNING no read and/or execute rights on %s", progpath);
			continue;
		}
		return progpath;		/* Ok, we found it */
	}

	return (char *) 0;			/* Program not found */
}

private int process_mail(location)
char *location;
{
	/* Process mail held in 'location' by invoking the mailagent on it. If the
	 * command fails, return -1. Otherwise, return 0;
	 * Note that we will exit if the first fork is not possible, but that is
	 * harmless, because we know the mail was safely queued, otherwise we would
	 * not be here trying to make the mailagent process it.
	 */
	
	char **envp;			/* Environment pointer */
#ifdef UNION_WAIT
	union wait status;		/* Waiting status */
#else
	int status;				/* Status from command */
#endif
	int xstat;				/* The exit status value */
	int pid;				/* Pid of our children */
	int res;				/* Result from wait */
	int delay;				/* Delay in seconds before invoking mailagent */
	char *perl;				/* perl path */
	char *mailagent;		/* mailagent path */
	char *path = get_env("PATH");

	if (loglvl <= 20) {		/* Loggging level higher than 20 is for tests */
		pid = fork();
		if (pid == -1) {	/* Resources busy, most probably */
			release_lock();
			add_log(1, "SYSERR fork: %m (%e)");
			add_log(6, "NOTICE exiting to save resources");
			my_exit(EX_OK);	/* Exiting will also release sendmail process */
		} else if (pid != 0)
			my_exit(EX_OK);	/* Release waiting sendmail */
		else
			goto_daemon();	/* Remaining child is to disassociate from tty */
	}

	/*
	 * Compute waiting delay, defaults to QUEUE_WAIT seconds if not defined.
	 */

	delay = get_confval("queuewait", CF_DEFAULT, QUEUE_WAIT);

	/* Now hopefully we detached ourselves from sendmail, which thinks the mail
	 * has been delivered. Not yet, but close. Simply wait a little in case
	 * more mail is comming. This process is going to remain alive while the
	 * mailagent is running so as to trap any weird exit status. But the size
	 * of the perl process (with script compiled) is about 1650K on my MIPS,
	 * so the more we delay the invocation, the better.
	 */

	if (loglvl < DEBUG_LOG)	/* Higher logging level reserverd for debugging */
		sleep(delay);		/* Delay invocation of mailagent */
	progpid = getpid();		/* This may be the child (if fork succeded) */
	envp = make_env();		/* Build new environment */

	/*
	 * We locate both mailagent and perl in the specified path. Not finding
	 * mailagent is a fatal error, but not finding perl simply means we
	 * fall back to the hardwired perl path (determined by Configure).
	 *
	 * Given the sensitive nature of mailagent processing, it is vital to
	 * make sure both perl and mailagent cannot be tampered with by ordinary
	 * users, or that would defeat all sanity checks performed on the config
	 * and rule files.
	 */
	
	mailagent = locate("mailagent", path);
	if (!mailagent) {
		add_log(1, "ERROR cannot locate mailagent anywhere in PATH");
		if (path)
			add_log(6, "NOTICE looked for mailagent under %s", path);
		return -1;
	} else
		mailagent = strsave(mailagent);		/* Save static data for perusal */

	perl = locate("perl", path);			/* Override hardwired default */
	if (!perl) perl = PERLPATH;				/* ...if possible */

	add_log(12, "perl at %s", perl);
	add_log(12, "mailagent at %s", mailagent);

	if (!(exec_secure(perl) && exec_secure(mailagent))) {
		add_log(1, "ERROR running mailagent would be unsecure");
		return -1;
	}

	/*
	 * Issue a virtual fork and let the child execute mailagent...
	 */

	pid = vfork();			/* Virtual fork this time... */
	if (pid == -1) {
		add_log(1, "SYSERR vfork: %m (%e)");
		add_log(1, "ERROR cannot run mailagent");
		return -1;
	}

	if (pid == 0) {			/* This is the child */
		execle(perl, "perl", mailagent, location, (char *) 0, envp);
		add_log(1, "SYSERR execle: %m (%e)");
		add_log(1, "ERROR cannot run perl to start %s", mailagent);
		my_exit(EX_UNAVAILABLE);
	}
	
	/* Parent process */

	while (pid != (res = wait(&status)))
		if (res == -1) {
			add_log(1, "SYSERR wait: %m (%e)");
			return -1;
		}

#ifdef USE_WIFSTAT
	if (WIFEXITED(status)) {			/* Exited normally */
		xstat = WEXITSTATUS(status);
		if (xstat != 0) {
			add_log(3, "ERROR mailagent returned status %d", xstat);
			return -1;
		}
	} else if (WIFSIGNALED(status)) {	/* Signal received */
		xstat = WTERMSIG(status);
		add_log(3, "ERROR mailagent terminated by signal %d", xstat);
		return -1;
	} else if (WIFSTOPPED(status)) {	/* Process stopped */
		xstat = WSTOPSIG(status);
		add_log(3, "WARNING mailagent stopped by signal %d", xstat);
		add_log(6, "NOTICE terminating mailagent, pid %d", pid);
		if (-1 == kill(pid, 15))
			add_log(1, "SYSERR kill: %m (%e)");
		return -1;
	} else
		add_log(1, "BUG please report bug 'posix-wait' to author");
#else
#ifdef UNION_WAIT
	xstat = status.w_status;
#else
	xstat = status;
#endif
	if ((xstat & 0xff) == 0177) {		/* Process stopped */
		xstat >>= 8;
		add_log(3, "WARNING mailagent stopped by signal %d", xstat);
		add_log(6, "NOTICE terminating mailagent, pid %d", pid);
		if (-1 == kill(pid, 15))
			add_log(1, "SYSERR kill: %m (%e)");
		return -1;
	} else if ((xstat & 0xff) != 0) {	/* Signal received */
		xstat &= 0xff;
		if (xstat & 0200) {				/* Dumped a core ? */
			xstat &= 0177;
			add_log(3, "ERROR mailagent dumped core on signal %d", xstat);
		} else
			add_log(3, "ERROR mailagent terminated by signal %d", xstat);
		return -1;
	} else {
		xstat >>= 8;
		if (xstat != 0) {
			add_log(3, "ERROR mailagent returned status %d", xstat);
			return -1;
		}
	}
#endif
	
	add_log(19, "mailagent ok");

	return 0;
}

public int emergency_save()
{
	/* Save mail in emeregency files and add the path to the agent.wait file,
	 * so that the mailagent knows where to look when processing its queue.
	 * Return -1 if the mail was not sucessfully saved, 0 otherwise.
	 */

	char *where;			/* Where file was stored (static data) */
	char *home = homedir();	/* Location of the home directory */
	char path[MAX_STRING];	/* Location of the AGENT_WAIT file */
	char *spool;			/* Location of the spool directory */
	char *emergdir;			/* Emergency directory */
	int fd;					/* File descriptor to write in AGENT_WAIT */
	int size;				/* Length of 'where' string */
	int old_size;			/* Old size of AGENT_WAIT file */
	struct stat buf;		/* To stat AGENT_WAIT */
	int error = 0;			/* Assume no error during AGENT_WAIT writes */
	int locked = 0;			/* True when AGENT_WAIT was locked */

	/*
	 * It is possible that we come here due to a configuration error, for
	 * instance, and that we had not a chance to read our standard input
	 * yet. So do that now.
	 *
	 * Thanks to Rosina Bignall <bigna@leopard.cs.byu.edu> for finding
	 * this hole at her depend ;-)
	 */

	read_stdin();		/* Read mail if not already done yet */

	if (mail.len == 0) {
		say("mail not read, cannot dump");
		return -1;	/* Failed */
	}

	if (queued) {
		add_log(6, "NOTICE mail was safely queued");
		return 0;
	}

	emergdir = ht_value(&symtab, "emergdir");
	if ((emergdir != (char *) 0) && (char *) 0 != (where = save_file(emergdir)))
		goto ok;
	if ((home != (char *) 0) && (char *) 0 != (where = save_file(home)))
		goto ok;
	if (where = save_file("/usr/spool/uucppublic"))
		goto ok;
	if (where = save_file("/var/spool/uucppublic"))
		goto ok;
	if (where = save_file("/usr/tmp"))
		goto ok;
	if (where = save_file("/var/tmp"))
		goto ok;
	if (where = save_file("/tmp"))
		goto ok;

	/*
	 * Attempt dumping on stdout, as a fall back.
	 */

	say("dumping mail on stdout...");
	fflush(stderr);		/* In case they did >file 2>&1 */

	if (-1 != write_fd(1, "stdout")) {
		char *logmsg = "DUMPED to stdout";
		say(logmsg);
		add_log(6, logmsg);
		return 0;
	}

	say("unable to dump mail anywhere");
	return -1;	/* Failed */

ok:
	say("DUMPED in %s", where);

	/*
	 * Attempt to write path of saved mail in the AGENT_WAIT file
	 *
	 * The file is locked to prevent concurrent update by mailagent, which
	 * could be regenerating the file. Still, we don't want to wait too much
	 * so only loop for 10 seconds.
	 */

	spool = ht_value(&symtab, "spool");
	if (spool == (char *) 0)
		return 0;

	locked = 0 == file_lock(AGENT_WAIT, "agent.wait", 10);
	if (!locked)
		add_log(6, "WARNING updating %s without lock", AGENT_WAIT);

	sprintf(path, "%s/%s", spool, AGENT_WAIT);
	if (-1 == (fd = open(path, O_WRONLY | O_APPEND | O_CREAT, 0600))) {
		add_log(1, "SYSERR open: %m (%e)");
		add_log(6, "WARNING mailagent ignores mail was left in %s", where);
		if (locked)
			file_unlock(AGENT_WAIT);
		return 0;
	}

	/*
	 * Stat AGENT_WAIT to save the old size, and enable a comparison later
	 * on to really be sure that the path of the message was properly
	 * memorized in there. Yes, this is paranoid, but I was bitten once
	 * by the old code that did not check that.
	 */

	if (-1 == stat(path, &buf)) {
		add_log(1, "SYSERR stat: %m (%e)");
		add_log(6, "WARNING cannot stat %s", path);
		old_size = -1;				/* Mark: old size not available */
	} else
		old_size = buf.st_size;

	size = strlen(where);
	where[size + 1] = '\0';			/* Make room for trailing new-line */
	where[size] = '\n';
	if (-1 == write(fd, where, size + 1)) {
		add_log(1, "SYSERR write: %m (%e)");
		add_log(4, "ERROR could not append to %s", path);
		error++;
	}
	where[size] = '\0';

	if (-1 == close(fd)) {
		add_log(1, "SYSERR close: %m (%e)");
		add_log(4, "ERROR could not flush to %s", path);
		error++;
	}

	if (locked)
		file_unlock(AGENT_WAIT);

	if (-1 == stat(path, &buf)) {
		add_log(1, "SYSERR stat: %m (%e)");
		add_log(6, "WARNING cannot stat %s", path);
		old_size = -1;				/* Disable sanity check below */
	}

	/*
	 * If an error is already recorded, it is useless to do a size
	 * sanity check since the size will not be correct anyway.
	 */

	if (!error && old_size != -1) {
		int expected = old_size + size + 1;
		if (expected != buf.st_size) {
			add_log(2, "ERROR %s truncated to %d bytes (should have had %d)",
				buf.st_size, expected);
			error++;
		}
	} else if (!error)
		add_log(8, "cannot double-check %s was properly flushed", path);

	/*
	 * Setting `queued' to 1 means that, upon exit, filter will return a
	 * success status to the MTA.
	 */

	if (!error) {
		add_log(7, "NOTICE memorized %s", where);
		queued = 1;
	} else
		add_log(6, "WARNING mailagent ignores mail was left in %s", where);

	return 0;
}

private char *save_file(dir)
char *dir;				/* Where saving should be done (directory) */
{
	/* Attempt to write mail in directory 'dir' and return a pointer to static
	 * data holding the path name of the saved file if writing was ok.
	 * Otherwise, return a null pointer and unlink any already created file.
	 */

	struct stat buf;				/* Stat buffer */

	/* Make sure 'dir' entry exists, although we do not make sure it is really
	 * a directory. If 'dir' is in fact a file, then open() will loudly
	 * complain. We only want to avoid spurious log messages.
	 */

	if (-1 == stat(dir, &buf))		/* No entry in file system, probably */
		return (char *) 0;			/* Saving failed */

	return write_file(dir, logname());
}

private char *write_file(dir, template)
char *dir;				/* Where saving should be done (directory) */
char *template;			/* First part of the file name */
{
	/* Attempt to write mail in directory 'dir' and return a pointer to static
	 * data holding the path name of the saved file if writing was ok.
	 * Otherwise, return a null pointer and unlink any already created file.
	 * The file 'dir/template.$$' is created (where '$$' refers to the pid of
	 * the current process). As login name <= 8 and pid is <= 5, we are below
	 * the fatidic 14 chars limit for filenames.
	 */

	static char path[MAX_STRING];	/* Path name of created file */
	int fd;							/* File descriptor */
	int status;						/* Status from write_fd() */
	struct stat buf;				/* Stat buffer */

	if (-1 == (fd = unique_filename(path, "%s.%d", dir, template)))
		return (char *) 0;

	status = write_fd(fd, path);		/* Write mail to file descriptor fd */
	file_unlock(path);					/* Was locked by unique_filename */
	close(fd);
	if (status == -1)					/* Something wrong happened */
		goto error;

	add_log(19, "mail in %s", path);	/* We did not detect any error so far */

	/* I don't really trust writes through NFS soft-mounted partitions, and I
	 * am also suspicious about hard-mounted ones. I could have opened the file
	 * with the O_SYNC flag, but the effect on NFS is not well defined either.
	 * So, let's just make sure the mail has been correctly written on the disk
	 * by comparing the file size and the orginal message size. If they differ,
	 * complain and return an error.
	 */

	if (-1 == stat(path, &buf))		/* No entry in file system, probably */
		return (char *) 0;			/* Saving failed */

	if (buf.st_size != mail.len) {	/* Not written entirely */
		add_log(2, "ERROR mail truncated to %d bytes (had %d)",
			buf.st_size, mail.len);
		goto error;					/* Remove file and report error */
	}

	return path;			/* Where mail was writen (static data) */

error:		/* Come here when a write error has been detected */

	if (-1 == unlink(path)) {
		add_log(1, "SYSERR unlink: %m (%e)");
		add_log(4, "WARNING leaving %s around", path);
	}

	return (char *) 0;
}

private int write_fd(fd, path)
int fd;					/* On which file descriptor saving occurs */
char *path;				/* Path name associated with that fd (may be NULL) */
{
	/* Write mail to the specified fd and return 0 if OK, -1 on error.
	 * Since mail is scattered amongst various pools of at most CHUNKSIZE
	 * bytes, we loop against all pools and write them in turn.
	 */

	struct pool *lp;

	for (lp = mail.first; lp; lp = lp->next) {
		if (-1 == pool_write(fd, lp)) {
			if (path != (char *) 0)
				add_log(2, "ERROR cannot write to file %s", path);
			return -1;	/* Failed */
		}
	}

	return 0;			/* OK */
}

private void close_tty(fd)
int fd;
{
	/* Close file if attached to a tty, otherwise do nothing. This is used
	 * by goto_daemon() to close file descriptors related to a tty to try
	 * to void any tty associations if other modern methods have failed.
	 * Unfortunately, we cannot just blindly close those descriptors in
	 * case output was redirected to some file...
	 */

	struct stat buf;				/* Stat buffer */

	if (-1 == fstat(fd, &buf)) {
		add_log(1, "SYSERR fstat: %m (%e)");
		add_log(6, "WARNING could not stat file descriptor #%d", fd);
		return;		/* Don't close it then */
	}

	/*
	 * The GNU libc had this bight idea to make stdin, stdout and stderr
	 * unsuitable for static initialization.  Wonderful.  Do it an runtime
	 * then.  -- RAM, 05/01/2001
	 */

	if (!stream_by_fd[0]) {
		stream_by_fd[0] = stdin;
		stream_by_fd[1] = stdout;
		stream_by_fd[2] = stderr;
	}
		
	/* Close file descriptor if attached to a tty. Otherwise, flush it if
	 * it is of the standard I/O kind, in case we did some buffered fprintf()
	 * on those.
	 */

	if (S_ISCHR(buf.st_mode)) {		/* File is a character device (tty) */
		if (fd < STDIO_FDS)
			(void) fclose(stream_by_fd[fd]);
		else
			(void) close(fd);
	} else if (fd < STDIO_FDS)
		(void) fflush(stream_by_fd[fd]);
}

private void goto_daemon()
{
	/* Make sure filter process goes into daemon state by releasing its
	 * control terminal and becoming the leader of a new process group
	 * or session.
	 *
	 * Harald Koch <chk@enfm.utcc.utoronto.ca> reported that this was
	 * needed when filter is invoked by zmailer's transport process.
	 * Otherwise the father waiting for his children does not get to see
	 * the EOF on the pipe, hanging forever.
	 */

	int fd;

#ifdef USE_TIOCNOTTY
	/*
	 * Errors from this open are discarded, since it is quite possible
	 * filter be launched without a controling tty, for instance when
	 * called via a daemon process like sendmail... :-)
	 */
	if ((fd = open("/dev/tty", 2)) >= 0) {
		if (-1 == ioctl(fd, TIOCNOTTY, (char *) 0)) {
			add_log(1, "SYSERR ioctl: %m (%e)");
			add_log(6, "WARNING could not release tty control");
		}
		(void) close(fd);
	}
#endif

	(void) close_tty(0);
	(void) close_tty(1);
	(void) close_tty(2);

	if (-1 == setsid()) {
		add_log(1, "SYSERR setsid: %m (%e)");
		add_log(6, "WARNING did not become session leader");
	}
}

public int io_redirect(filename, is_setid, ruid)
char *filename;			/* Filename for redirection */
int is_setid;			/* True when set[ug]id */
int ruid;				/* Real uid */
{
	/* Redirect stdout and stderr to specified filename. When is_setid
	 * is true, we don't create the file but only append to it, and
	 * only if the file is owned by the real uid.
	 * Returns true if operation succeeded, false otherwise.
	 */

	int fd;					/* New fd created for new stdout/stderr */
	int stdfd;				/* Saved stderr file descriptor */
	struct stat buf;		/* Stat buffer */

	if (is_setid) {			/* Pre-check file when running set[ug]id */
		if (-1 == stat(filename, &buf)) {
			if (errno != ENOENT) {
				add_log(1, "SYSERR stat: %m (%e)");
				add_log(2, "ERROR cannot stat %s", filename);
				return 0;	/* Failed */
			}
			add_log(1, "ERROR cannot create %s when running set[ug]id",
				filename);
			return 0;
		}
		if (buf.st_uid != ruid) {
			add_log(1, "ERROR cannot append to %s (not owned by UID %d)",
				filename, ruid);
			return 0;
		}
	}

	/*
	 * Now open the specified file.
	 *
	 * The pre-checking above, meant to spot errors, also opens a
	 * race-condition wrt. the file ownership, when running set[ug]id.
	 * Closing it would mean using the real uid to perform the open, which
	 * is hard to do portably (need to switch to the real uid and back to
	 * the effective later on).
	 *
	 * Workaround: we open the file, and re-check the opened fd via a fstat().
	 * We close it without writing anything if the ownership is not right.
	 * Note that we forbid the creation explicitely when running set[ug]id.
	 */

	fd = open(filename, O_WRONLY | O_APPEND | (is_setid ? 0 : O_CREAT), 0600);
	if (fd == -1) {
		add_log(1, "SYSERR open: %m (%e)");
		return 0;
	}

	if (is_setid) {			/* Re-check file when running set[ug]id */
		if (-1 == fstat(fd, &buf)) {	/* Note we stat() the fd! */
			close(fd);
			add_log(1, "SYSERR stat: %m (%e)");
			add_log(2, "ERROR can't locate %s after opening", filename);
			return 0;
		}
		if (buf.st_uid != ruid) {
			close(fd);
			add_log(1, "ERROR cannot append to %s (not owned by UID %d)",
				filename, ruid);
			return 0;
		}
	}

	/*
	 * Time to redirect the new fd to 1 and 2.
	 * Keep the original stderr file descriptor around, in case there is
	 * a dup2() failure.
	 */

	stdfd = dup(2);
	fflush(stderr);		/* In case we have to force a write() on 2 below */

	if (-1 == dup2(fd, 1) || -1 == dup2(fd, 2)) {
		add_log(1, "SYSERR dup2: %m (%e)");
		if (-1 == dup2(stdfd, 2)) {		/* #2 may have been closed by dup2 */
			char *msg = "Emergency restore of stderr failed...";
			write(stdfd, msg, strlen(msg));		/* They might see this */
		}
		close(stdfd);
		close(fd);
		return 0;
	}

	close(stdfd);
	close(fd);
	return 1;		/* OK -- stderr and stdout redirected */
}

#ifndef HAS_RENAME
public int rename(from, to)
char *from;				/* Original name */
char *to;				/* Target name */
{
	(void) unlink(to);
	if (-1 == link(from, to))
		return -1;
	if (-1 == unlink(from))
		return -1;

	return 0;
}
#endif

#ifndef HAS_SETSID
public int setsid()
{
	/* Set the process group ID and create a new session for the process.
	 * This is a pale imitation of the setsid() system call. Actually, we
	 * go into a lot more trouble here than is really needed...
	 */

	int error = 0;

#ifdef HAS_SETPGID
	/*
	 * setpgid() supersedes setpgrp() in OSF/1.
	 */
	error = setpgid(0 ,getpid());
#else
#ifdef HAS_SETPGRP
	/*
	 * Good old way to get a process group leader.
	 */
#ifdef USE_BSD_SETPGRP
	error = setpgrp(0 ,getpid());	/* bsd way */
#else
	error = setpgrp();				/* usg way */
#endif
#endif
#endif

	/*
	 * When none of the above is defined, do nothing.
	 */

	return error;
}
#endif

