/*
 *	Copyright 1988 by Rayan S. Zachariassen, all rights reserved.
 *	This will be free software, but only when it is finished.
 */

/* History:
 *
 * Based on code by Geoff Collyer.
 * Rewritten for Sun environment by Dennis Ferguson and Rayan Zachariassen.
 * RBIFF code originally added by Jean-Francois Lamy.
 * Heavily modified for ZMailer by Rayan Zachariassen.
 *
 */

#include <stdio.h>
#include "hostenv.h"
#include <ctype.h>
#include <errno.h>
#include <pwd.h>
#include <signal.h>
#include <sysexits.h>
#include <varargs.h>
#include <sys/param.h>
#include <fcntl.h>
#include <sys/file.h>
#include <sys/stat.h>
#include NDIR_H
#include <mail.h>
#include "ta.h"
#include "splay.h"

#ifdef	USE_LOCKF
#ifdef	F_OK
#undef	F_OK
#endif	/* F_OK */
#ifdef	X_OK
#undef	X_OK
#endif	/* X_OK */
#ifdef	W_OK
#undef	W_OK
#endif	/* W_OK */
#ifdef	R_OK
#undef	R_OK
#endif	/* R_OK */
#include <unistd.h>
#endif	/* USE_LOCKF */

#ifdef	USE_UNIONWAIT
#include <sys/wait.h>
#endif	/* USE_UNIONWAIT */

#ifdef	USE_MAILLOCK
#ifdef	MAILLOCK_H
#include MAILLOCK_H
#else
#include <maillock.h>
#endif
#endif	/* USE_MAILLOCK */

#ifdef USE_DOTLOCK
#include "dotlock.c"
#endif

#ifndef	USE_LSTAT
#define	lstat stat
#endif	/* !USE_LSTAT */

#ifndef	USE_SETREUID
#ifdef	USE_SETEUID
#define	setreuid(x,y)	seteuid(y)
#else
#define	setreuid(x,y)	setuid(y)
#endif
#endif	/* !USE_SETREUID */

#ifdef  USE_TIMEVAL
#include <sys/time.h>
#else   /* !USE_TIMEVAL */
/*#include <unistd.h> */
/* some systems have utimbuf defined in unistd.h, some don't... */
struct utimbuf {
	time_t  actime;
	time_t  modtime;
};
#endif  /* USE_TIMEVAL */


#ifndef	L_SET
#define	L_SET	0
#endif	/* !L_SET */
#ifndef	L_XTND
#define	L_XTND	2
#endif	/* !L_XTND */
#ifndef	F_OK
#define	F_OK	0
#endif	/* !F_OK */

/* Use fcntl() if we don't have flock() */
#if defined(F_SETLK) && !defined(LOCK_EX)
#include "flock.c"
#endif

#ifdef	BIFF
#include <netdb.h>
#include <sys/socket.h>
#ifdef	SYS_STREAM_H
#include SYS_STREAM_H
#endif	/* SYS_STREAM_H */
#include <netinet/in.h>
#include <net/if.h>
#endif	/* BIFF */

#ifdef RBIFF
#include <sys/dir.h>
#include <utmp.h>
#include <protocols/rwhod.h>
#define RWHODIR		"/var/spool/rwho"
#define	WHDRSIZE	(sizeof (wd) - sizeof (wd.wd_we))
#define RBIFFRC		".rbiff"
#endif	/* RBIFF */

#define	PROGNAME	"mailbox"	/* for logging */
#define	CHANNEL		"local"	/* the default channel name we deliver for */
#define TO_FILE		'/'	/* first character of file username */
#define TO_PIPE		'|'	/* first character of program username */
#define TO_USER		'\\'	/* first character of escaped username */
#define	QUOTE		'"'	/* some usernames have these around them */
#define	FROM_		"From "	/* mailbox separator string (q.v. writebuf) */

#define MAILMODE        0600    /* prevent snoopers from looking at mail */
#define	NONUIDSTR	"6963"	/* X:magic nonsense uid if nobody uid is bad */

/*
 * The following is stuck in for reference only.  You could add
 * alternate spool directories to the list (they are checked in
 * order, first to last) but if the MAILBOX zenvariable exists it will
 * override the entire list.
 */
char *maildirs[] = {
	"/var/mail",
	0 
};

/*
 * Macro to determine from the given error number whether this
 * should be considered a temporary failure or not.
 */
#ifdef	USE_NFSMBOX
#define	TEMPFAIL(err)	(err == EIO || err == ENETRESET || \
			 err == ECONNRESET || err == ENOBUFS || \
			 err == ETIMEDOUT || err == ECONNREFUSED || \
			 err == EHOSTDOWN || err == ENOTCONN)
#else	/* !USE_NFSMBOX */
#define TEMPFAIL(err)	(err == EIO)
#endif	/* USE_NFSMBOX */


#define	DIAGNOSTIC(R,E,A1,A2)	diagnostic((R), \
					(*((R)->addr->user) == TO_PIPE \
					 && (R)->status != EX_OK \
					 && (R)->status != EX_TEMPFAIL) ? \
					      EX_UNAVAILABLE : (E), (A1), (A2))

#define	XDIAGNOSTIC(R,E,A1,A2,A3)   diagnostic((R), \
					(*((R)->addr->user) == TO_PIPE \
					 && (R)->status != EX_OK \
					 && (R)->status != EX_TEMPFAIL) ? \
					      EX_UNAVAILABLE : (E), \
						(A1), (A2), (A3))

#if	defined(BIFF) || defined(RBIFF)
/*
 * Biff strategy:
 *
 * If any biff is desired, add user to list with empty structure.
 * For all users in list, if remote biff is desired, read rwho files
 * to determine the hosts user is on.  Add user to list for each host.
 * The "user" is really a struct containing username and offset into
 * their mailbox.
 */

struct biffer {
	struct biffer	*next;
	char		*user;
	long		offset;
	int		wantrbiff;
};

struct biffer *biffs = NULL;
struct biffer *eobiffs = NULL;

struct userhost {
	struct userhost *next;
	char		*hostname;
};

struct sptree *spt_users = NULL;
#endif	/* BIFF || RBIFF */

char *progname;
char *channel;
char *logfile;
FILE *logfp;
int readalready = 0;		/* does buffer contain valid message data? */
int currenteuid;		/* the current euid */
extern int nobody;		/* safe uid for file/program delivery */
int dobiff = 1;			/* enable biff notification */
int dorbiff = 0;		/*    "   rbiff   " */
int keepatime = 1;		/* preserve mbox st_atime, for login etc. */
int creatflag = 1;		/* attempt to create files that don't exist */

extern int errno;
extern void warning();
extern char *emalloc();

#ifndef	MAXPATHLEN
#define	MAXPATHLEN 1024
#endif

int
main(argc, argv)
	int argc;
	char *argv[];
{
	char file[MAXPATHLEN+1];
	char *s;
	int c, errflg, fd;
	struct ctldesc *dp;
	struct biffer *nbp;
	extern SIGNAL_TYPE wantout();
	extern time_t time();
	extern int optind;
	extern char *optarg;
	extern int emptyline();
	extern void prversion(), getnobody(), process(), biff(), rbiff();
	extern char *strerror();

	if (signal(SIGINT, SIG_IGN) != SIG_IGN)
		(void) signal(SIGINT, wantout);
	if (signal(SIGHUP, SIG_IGN) != SIG_IGN)
		(void) signal(SIGHUP, wantout);
	if (signal(SIGTERM, SIG_IGN) != SIG_IGN)
		(void) signal(SIGTERM, wantout);
	(void) signal(SIGPIPE, SIG_IGN);

	if ((progname = strrchr(argv[0], '/')) == NULL)
		progname = argv[0];
	else
		++progname;

	if (geteuid() != 0 || getuid() != 0) {
		(void) fprintf(stderr, "%s: not running as root!\n", progname);
		exit(EX_NOPERM);
	}

	errflg = 0;
	logfile = NULL;
	channel = CHANNEL;
	while ((c = getopt(argc, argv, "abc:gl:rV")) != EOF) {
		switch (c) {
		case 'c':	/* specify channel scanned for */
			channel = optarg;
			break;
		case 'V':
			prversion(PROGNAME);
			exit(EX_OK);
			break;
		case 'b':	/* toggle biffing */
			dobiff = !dobiff;
			break;
		case 'r':	/* toggle rbiffing */
			dorbiff = !dorbiff;
			break;
		case 'a':	/* toggle mbox st_atime preservation */
			keepatime = !keepatime;
			break;
		case 'g':	/* toggle file creation */
			creatflag = !creatflag;
			break;
		case 'l':	/* log file */
			logfile = emalloc(strlen(optarg)+1);
			(void) strcpy(logfile, optarg);
			break;
		default:
			++errflg;
			break;
		}
	}
	if (errflg || optind != argc) {
		(void) fprintf(stderr, "Usage: %s [-Vabr] [-c channel]\n",
				argv[0]);
		exit(EX_USAGE);
	}
	(void) setreuid(0, 0);		/* make us root all over */
	currenteuid = 0;

	if ((s = getzenv("MAILBOX")) != NULL) {
		maildirs[0] = s;
		maildirs[1] = NULL;
	}

	if (logfile != NULL) {
		if ((fd = open(logfile, O_CREAT|O_APPEND|O_WRONLY, 0644)) < 0)
			(void) fprintf(stderr,
				       "%s: open(\"%s\") failed: %s\n",
				       progname, logfile, strerror(errno));
		else
			logfp = fdopen(fd, "a");
	} else
		logfp = NULL;

	getnobody();

	while (!getout && fgets(file, sizeof file, stdin)) {
		if (emptyline(file, sizeof file))
			break;
		dp = ctlopen(file, channel, (char *)NULL, &getout, (int(*)())0);
		if (dp == NULL)
			continue;
		process(dp);
		ctlclose(dp);
	}

#if	defined(BIFF) || defined(RBIFF)
	/* now that the delivery phase is over, hoot'n wave */
	eobiffs = NULL;
	for (nbp = biffs ; nbp != NULL; nbp = nbp->next) {
		if (dobiff && nbp->offset >= 0)
			biff("localhost", nbp->user, nbp->offset);
#ifdef	RBIFF
		if (nbp->wantrbiff != 0) {
			if (spt_users == NULL)
				spt_users = sp_init();
			(void) sp_install(symbol(nbp->user),
						(char *)NULL, 0, spt_users);
		}
#endif	/* RBIFF */
	}
#ifdef	RBIFF
	if (spt_users != NULL) {
		readrwho();
		for (nbp = biffs ; nbp != NULL; nbp = nbp->next) {
			if (nbp->offset >= 0 && nbp->wantrbiff)
				rbiff(nbp);
		}
	}
#endif	/* RBIFF */
#endif	/* BIFF || RBIFF */
	exit(EX_OK);
	/* NOTREACHED */
	return 0;
}

void
process(dp)
	struct ctldesc *dp;
{
	char *cp, *ts, *at;
	struct rcpt *rp;
	time_t curtime;
	extern time_t time();
	extern void deliver();

	/*
	 * Strip backslashes prefixed to the user name,
	 * and strip the quotes from around a name.
	 */
	for (rp = dp->recipients; rp != NULL; rp = rp->next) {
		while (*(rp->addr->user) == TO_USER)
			rp->addr->user++;
		if ((at = strrchr(rp->addr->user,'@')) != NULL) *at = 0;
		if (*(rp->addr->user) == QUOTE) {
			cp = rp->addr->user + strlen(rp->addr->user) - 1;
			if (*cp == QUOTE) {
				*cp = '\0';
				rp->addr->user++;
			}
		}
		if (at) *at = '@'; /* If it followed quotes, a null prefixes it. */
#ifdef	RBIFF
		/*
		 * "contents" will be freed before rbiff done:
		 * must copy username somewhere.
		 */
		cp = emalloc((u_int)strlen(rp->addr->user)+1);
		(void) strcpy(cp, rp->addr->user);
		rp->addr->user = cp;
#endif	/* RBIFF */
	}

	readalready = 0; /* ignore any previous message data cache */
	(void) time(&curtime);
	ts = ctime(&curtime);

	for (rp = dp->recipients; rp != NULL; rp = rp->next) {
		/* seek to message body start on each iteration */
		if (lseek(dp->msgfd, dp->msgbodyoffset, L_SET) < 0L) {
			warning("Cannot seek to message body in %s! (%m)",
					dp->msgfile);
			DIAGNOSTIC(rp, EX_TEMPFAIL,
				       "cannot seek to message body!", 0);
		} else {
			if (dp->msgbodyoffset == 0)
				warning("Null message offset in \"%s\"!",
					      dp->msgfile);
			deliver(rp, dp, ts);
		}
	}
}

/*
 * deliver - deliver the letter in to user's mail box.  Return
 *	     errors and requests for further processing in the structure
 */

void
deliver(rp, dp, timestring)
	struct rcpt *rp;
	struct ctldesc *dp;
	char *timestring;
{
	register char **maild;
	int fdmail, uid, messagefd;
	FILE *fp;
	struct stat st, s2;
	char *file, *cp, *at;
	int havedotlock = 0, havemaillock = 0, haveflock = 0;
	int ismbox = 0;
#if	defined(BIFF) || defined(RBIFF)
	struct biffer *nbp;
#ifdef	RBIFF
	char *path;
#endif	/* RBIFF */
#endif	/* BIFF || RBIFF */
	struct passwd *pw;
	extern struct passwd *getpwnam();
	extern int setupuidgid(), createfile();
	extern int exstat(), creatembox();
	extern char *exists(), *strerror();
	extern FILE *putmail();
	extern void program(), setrootuid();

	messagefd = dp->msgfd;
	file = NULL;
	nbp = NULL;
	at = strchr(rp->addr->user,'@');
	uid = atoi(rp->addr->misc);
	switch (*(rp->addr->user)) {
	case TO_PIPE:	/* pipe to program */
		/* one should disallow this if uid == nobody? */
		if (uid == nobody) {
			DIAGNOSTIC(rp, EX_UNAVAILABLE,
				       "mail to program disallowed", 0);
			return;
		}
		program(rp, messagefd, timestring, dp);
		/* DIAGNOSTIC(rp, EX_UNAVAILABLE,
				  "mailing to programs (%s) is not supported",
				  rp->addr->user+1); */
		return;
	case TO_FILE:	/* append to file */
		if (uid == nobody) {
			DIAGNOSTIC(rp, EX_UNAVAILABLE,
				       "mail to file disallowed", 0);
			return;
		}
		if (!setupuidgid(rp, uid, 1))
			return;
		file = emalloc(strlen(rp->addr->user)+1);
		(void) strcpy(file, rp->addr->user);
		if (access(file, F_OK) < 0) {
			fdmail = createfile(rp, file, uid);
			if (fdmail < 0) {
				free(file);
				return;
			}
			(void) close(fdmail);
		}
		setrootuid(rp);
		break;
	default:	/* local user */
		ismbox = 1;
		/* Zap the possible '@' for a moment -- and restore later
		   [mea@utu.fi] */
		if (at) *at = 0;
		if ((pw = getpwnam(rp->addr->user)) == NULL) {
			if (isascii(*(rp->addr->user))
				   && isupper(*(rp->addr->user))) {
				for (cp = rp->addr->user ; *cp != '\0'; ++cp)
					if (isascii(*cp) && isupper(*cp))
						*cp = tolower(*cp);
			}
			if ((pw = getpwnam(rp->addr->user)) == NULL) {
				DIAGNOSTIC(rp, EX_NOUSER,
				   "user \"%s\" doesn't exist", rp->addr->user);
				return;
			}
		}
		for (maild = &maildirs[0]; *maild != 0; maild++) {
			if ((file = exists(*maild, rp)) != NULL)
				break;		/* found it */
			if (rp->status != EX_OK)
				return;
		}
		if (*maild == 0)		/* didn't find it? */
			if (!creatembox(rp, &file, &st.st_uid, &st.st_gid, pw))
				return;		/* creatembox sets status */
#if	defined(BIFF) || defined(RBIFF)
		if (!dobiff && !dorbiff)
			break;
		nbp = (struct biffer *)emalloc(sizeof (struct biffer));
		nbp->next = NULL;
		nbp->user = emalloc(strlen(rp->addr->user)+1);
		strcpy(nbp->user, rp->addr->user);
		nbp->offset = -1;
		nbp->wantrbiff = 0;
#ifdef	RBIFF
		if (dorbiff) {
#ifdef	RBIFF_ALWAYS
			nbp->wantrbiff = 1;
#else
			path = emalloc(2+strlen(pw->pw_dir)+strlen(RBIFFRC));
			sprintf(path, "%s/%s", pw->pw_dir, RBIFFRC);
			nbp->wantrbiff = (access(path, F_OK) == 0);
#endif	/* RBIFF_ALWAYS */
		}
#endif	/* RBIFF */
		if (biffs == NULL)
			biffs = eobiffs = nbp;
		else {
			eobiffs->next = nbp;
			eobiffs = nbp;
		}
#endif	/* BIFF || RBIFF */
		if (at) *at = '@';
		break;
	}
	if (exstat(rp, file, &st, lstat) < 0) {
		DIAGNOSTIC(rp, EX_TEMPFAIL,
			       "mailbox file \"%s\" disappeared", file);
		return;
	}
	/* we only deliver to unlinked, regular file */
	if ((st.st_mode & S_IFMT) != S_IFREG && strcmp(file,"/dev/null") != 0) {
		/* except for /dev/null, growl */
		/* XX: may want to deliver to named pipes */
		DIAGNOSTIC(rp, EX_UNAVAILABLE,
			       "attempted delivery to special file \"%s\"",
			       file);
		return;
	}
	if (st.st_nlink > 1) {
		DIAGNOSTIC(rp, EX_UNAVAILABLE,
			       "too many links to file \"%s\"", file);
		return;
	}
	/*
	 * If we're delivering to a mailbox, and the mail spool directory
	 * is group writable, set our gid to that of the directory.
	 * This allows us to create lock files if need be, without
	 * making the spool dir world-writable.
	 */
	if (ismbox && (cp = strrchr(file, '/')) != NULL) {
		*cp = '\0';
		if (stat(file, &s2) == 0 && (s2.st_mode&020))
			st.st_gid = s2.st_gid;
		*cp = '/';
	}
	if (!setupuidgid(rp, st.st_uid, st.st_gid))
		return;			/* setupuidgid sets status */

	if ((fdmail = open(file, O_RDWR|O_APPEND)) < 0) {
		char fmtbuf[512];

		(void) sprintf(fmtbuf, "open(\"%%s\") failed: %s",
				       strerror(errno));
		if (TEMPFAIL(errno))
			rp->status = EX_TEMPFAIL;
		else if (errno == EACCES)
			rp->status = EX_NOPERM;
		else
			rp->status = EX_UNAVAILABLE;
		DIAGNOSTIC(rp, rp->status, fmtbuf, file);
		return;
	}
        /*
         * The mbox may have been removed and symlinked
         * after the exstat() above, but before the open(),
         * so verify that we have the same inode.
         */
        if (fstat(fdmail, &s2) < 0) {
                DIAGNOSTIC(rp, EX_TEMPFAIL,
                               "can't fstat mailbox \"%s\"", file);
                close(fdmail);
                return;
        }
	/*
	 * If we're delivering to /dev/null under Solaris 2.x,
	 * this test will always succeed.  /dev/null is a symlink,
	 * so "st" will contain the symlink's inode, and "s2" the target's.
	 * So skip this check if we're dealing with /dev/null.
	 */ 
	if (strcmp(file, "/dev/null") != 0 &&
	    (st.st_ino != s2.st_ino || st.st_dev != s2.st_dev || s2.st_nlink != 1)) {
		DIAGNOSTIC(rp, EX_TEMPFAIL,
			"lost race for mailbox \"%s\"", file);
                close(fdmail);
                return;
        }
	/* don't lock non-files */;
	if ((st.st_mode & S_IFMT) == S_IFREG) {
#ifdef	USE_MAILLOCK
		if (ismbox) {
			int i;
			char buf[BUFSIZ];

			switch (i = maillock(pw->pw_name, 2)) {
			case L_SUCCESS: cp = NULL; break;
			case L_MAXTRYS:
				DIAGNOSTIC(rp, EX_TEMPFAIL, "\"%s\" is locked", file);
				return;
			case L_NAMELEN: cp = "recipient name > 13 chars"; break;
			case L_TMPLOCK: cp = "problem creating temp lockfile"; break;
			case L_TMPWRITE: cp = "problem writing pid into temp lockfile"; break;
			case L_ERROR:	cp = "Something other than EEXIST happened"; break;
			case L_MANLOCK:cp = "cannot set mandatory lock on temp lockfile"; break;
			default:
				sprintf(buf, "maillock() error %d", i);
				cp = buf;
				break;
			}
			if (cp != NULL) {
				XDIAGNOSTIC(rp, EX_UNAVAILABLE,
					"Error locking \"%s\": %s", file, cp);
				return;
			}
			havemaillock = 1;
		}
#endif	/* USE_MAILLOCK */
#ifdef  USE_DOTLOCK
		if (ismbox && !(havedotlock = dotlock(file) == 0)) {
                        char mbuf[256];
                        sprintf(mbuf, "\"%s\", errno %d", file, errno);
                        DIAGNOSTIC(rp, errno == EBUSY ? EX_TEMPFAIL :
                                (errno == EACCES ? EX_NOPERM : EX_UNAVAILABLE),
                                "can't lock %s", mbuf);
                        return;
                }
#endif  /* USE_DOTLOCK */
#ifdef  USE_NFSMBOX
                if (nfslock(file, LOCK_EX) != 0)
#else   /* !USE_NFSMBOX */
#ifdef  USE_LOCKF
                /* Seek to beginning for locking! [mea@utu.fi] */
                if (lseek(fdmail, 0L, L_SET) < 0 || lockf(fdmail, F_LOCK, 0) < 0
)
#else   /* !USE_LOCKF */
#ifdef	LOCK_EX
		if (!(haveflock = flock(fdmail, LOCK_EX) == 0))
#else	/* !LOCK_EX */
		if (0)
#endif	/* LOCK_EX */
#endif  /* USE_LOCKF */
#endif  /* USE_NFSMBOX */
                {
                        DIAGNOSTIC(rp, EX_TEMPFAIL, "can't lock \"%s\"", file);
#ifdef	USE_MAILLOCK
			if (havemaillock)
				mailunlock();
#endif
#ifdef  USE_DOTLOCK
                        if (havedotlock)
                                dotunlock(file);
#endif  /* USE_DOTLOCK */
                        return;
                }
	}
#if	defined(BIFF) || defined(RBIFF)
	if (nbp != NULL)
		nbp->offset = lseek(fdmail, 0L, L_XTND);
#endif	/* BIFF || RBIFF */
	fp = putmail(rp, fdmail, messagefd, timestring, file, dp);
	if ((st.st_mode & S_IFMT) == S_IFREG) {
#ifdef	USE_NFSMBOX
		(void) unlock(file);
#endif	/* USE_NFSMBOX */
#ifdef	USE_LOCKF
		(void) lockf(fdmail, F_ULOCK, 0);/* XX: does this really work?*/
#endif	/* USE_LOCKF */
#ifdef	USE_MAILLOCK
		if (havemaillock)
			(void) mailunlock();
#endif	/* USE_MAILLOCK */
#ifdef	USE_DOTLOCK
		if (havedotlock)
			dotunlock(file);
#endif	/*USE_DOTLOCK*/
#ifdef	LOCK_UN
		if (haveflock)
			(void) flock(fdmail, LOCK_UN);
#endif	/*LOCK_UN*/
	}
	setrootuid(rp);
	if (fp != NULL) {
		(void) fclose(fp);	/* this closes fdmail */
		DIAGNOSTIC(rp, EX_OK, (char *)NULL, 0);
	}
#if	defined(BIFF) || defined(RBIFF)
	else if (nbp != NULL) /* putmail() has produced a DIAGNOSTIC */
		nbp->offset = -1;
#endif	/* BIFF || RBIFF */
	return;
}

FILE *
putmail(rp, fdmail, messagefd, timestring, file, dp)
	struct rcpt *rp;
	int fdmail, messagefd;
	char *timestring, *file;
	struct ctldesc *dp;
{
	FILE *fp;
	int len;
	long eofindex;
	char buf[2];
	struct stat st;
	extern int appendlet();
	extern char *strerror();

	if ((fp = fdopen(fdmail, "a+")) == NULL) {
		DIAGNOSTIC(rp, EX_TEMPFAIL, "cannot fdopen(%d)", fdmail);
		return NULL;
	}
	/* append From_ line and print out the header */
	eofindex = ftell(fp);
	if (*(rp->addr->user) != TO_PIPE && eofindex > 0L
	    && fseek(fp, -2L, 2) != -1) {
		/*
		 * Determine how many newlines to append to previous text.
		 *
		 * If the last characters are:	\n\n, append 0 newlines,
		 *				[^\n]\n or \n append 1 newline,
		 *				else append 2 newlines.
		 *
		 * Optionally preserve the mbox's atime, so
		 * login etc. can distinguish new mail from old.
		 * The mtime will be set to now by the following write() calls.
		 */
		if (!keepatime || fstat(fileno(fp), &st) < 0)
			st.st_atime = 0;
		len = fread(buf, 1, 2, fp);
		if (st.st_atime != 0) {
#ifdef	USE_TIMEVAL
			struct timeval tv[2];
			tv[0].tv_sec = st.st_atime;
			tv[1].tv_sec = st.st_mtime;
			tv[0].tv_usec = tv[1].tv_usec = 0;
			(void) utimes(file, tv);
#else	/* !USE_TIMEVAL */
			struct utimbuf tv;
			tv.actime = st.st_atime;
			tv.modtime = st.st_mtime;
			(void) utime(file, &tv);
#endif	/* USE_TIMEVAL */
		}
		if (len == 1 || len == 2) {
			--len;
			len = (buf[len]!='\n') + (len == 1 ? buf[0]!='\n' : 1);
			(void) fseek(fp, 0, 2);	/* to end of file, again */
			if (len > 0 && (fwrite("\n\n", 1, len, fp) != len
					|| fflush(fp) == EOF)) {
				DIAGNOSTIC(rp, EX_IOERR,
					"cleansing of \"%s\" failed", file);
				/* XX: should I really do this? */
#ifdef	USE_FTRUNCATE
				(void) fflush(fp);
				(void) ftruncate(fileno(fp), (u_long)eofindex);
#endif	/* USE_FTRUNCATE */
				(void) fclose(fp);
				return NULL;
			}
		} else
			(void) fseek(fp, 0L, 2); /* to end of file, again */
	} else if (eofindex > 0L && eofindex < (sizeof "From x\n")) {
		/* A mail message *cannot* be this small.  It must be
		   a corrupted mailbox file.  Ignore them trash bytes. */
		(void) fseek(fp, 0L, 0);
		eofindex = 0;
	}
	len = strlen(rp->newmsgheader);
	if (fprintf(fp, "%s%s %s", FROM_,rp->addr->link->user,timestring) == EOF
	    || fwrite(rp->newmsgheader, 1, len, fp) < len
	    || fputc('\n', fp) == EOF) {
		DIAGNOSTIC(rp, EX_IOERR, "header write to \"%s\" failed", file);
		/* XX: should I really do this? */
		(void) fflush(fp);
		(void) ftruncate(fileno(fp), (u_long)eofindex);
		(void) fclose(fp);
		return NULL;
	}
	if (appendlet(rp, fp, file, messagefd) == EOF) {
		/* XX: should I really do this? */
		(void) fflush(fp);
		(void) ftruncate(fileno(fp), (u_long)eofindex);
		(void) fclose(fp);
		return NULL;
	}
	if (fputc('\n', fp) == EOF || fflush(fp) == EOF) {
		XDIAGNOSTIC(rp, EX_IOERR,
			       "message write to \"%s\" failed: %s",
			       file, strerror(errno));
		/* XX: should I really do this? */
		(void) fflush(fp);
		(void) ftruncate(fileno(fp), (u_long)eofindex);
		(void) fclose(fp);
		return NULL;
	}
	if (logfp != NULL) {
		(void) fprintf(logfp, "%s: %ld + %ld : \"%s\" user \"%s\" pid %d\n",
				      dp->logident, eofindex,
				      ftell(fp) - eofindex, file,
				      rp->addr->user, getpid());
		(void) fflush(logfp);
	}
	return fp;
}

void
program(rp, messagefd, timestring, dp)
	struct rcpt *rp;
	int messagefd;
	char *timestring;
	struct ctldesc *dp;
{
	int i, pid, uid, gid, in[2], out[2];
	char *env[20], buf[8192], *cp, *s;
#ifdef	USE_UNIONWAIT
	union wait status;
#else	/* !USE_UNIONWAIT */
	int status;
#endif	/* USE_UNIONWAIT */
	struct passwd *pw;
	FILE *errfp;
	FILE *fp;
	extern char **environ;
	extern struct passwd *getpwuid();

	i = 0;
	env[i++] = "SHELL=/bin/sh";
	env[i++] = "IFS= \t\n";
	cp = buf;
	if ((s = getzenv("PATH")) == NULL)
		env[i++] = "PATH=/usr/ucb:/usr/bin:/bin";
	else {
		(void) sprintf(cp, "PATH=%s", s);
		env[i++] = cp;
		cp += strlen(cp) + 1;
	}
	uid = atoi(rp->addr->misc);
	if ((pw = getpwuid(uid)) == NULL) {
		gid = 0;
		env[i++] = "HOME=/tmp";
		env[i++] = "USER=anonymous";
	} else {
		gid = pw->pw_gid;
		(void) sprintf(cp, "HOME=%s", pw->pw_dir);
		env[i++] = cp;
		cp += strlen(cp) + 1;
		(void) sprintf(cp, "USER=%s", pw->pw_name);
		env[i++] = cp;
		cp += strlen(cp) + 1;
	}
	(void) sprintf(cp, "SENDER=%s", rp->addr->link->user);
	env[i++] = cp;
	cp += strlen(cp) + 1;
	(void) sprintf(cp, "UID=%d", uid);
	env[i++] = cp;
	if ((s = getzenv("ZCONFIG")) == NULL)
		s = ZMAILER_ENV_FILE;
	cp += strlen(cp) + 1;
	(void) sprintf(cp, "ZCONFIG=%s", s);
	env[i++] = cp;
	if ((s = getzenv("MAILBIN")) == NULL)
		s = MAILBIN;
	cp += strlen(cp) + 1;
	(void) sprintf(cp, "MAILBIN=%s", s);
	env[i++] = cp;
	if ((s = getzenv("MAILSHARE")) == NULL)
		s = MAILSHARE;
	cp += strlen(cp) + 1;
	(void) sprintf(cp, "MAILSHARE=%s", s);
	env[i++] = cp;
	env[i] = NULL;

	/* now we can fork off and run the command... */
	if (pipe(in) < 0) {
		DIAGNOSTIC(rp, EX_OSERR, "cannot create pipe from \"%s\"",
			       rp->addr->user+1);
		return;
	}
	if (pipe(out) < 0) {
		DIAGNOSTIC(rp, EX_OSERR, "cannot create pipe to \"%s\"",
			       rp->addr->user+1);
		(void) close(in[0]);
		(void) close(in[1]);
		return;
	}
	if ((pid = fork()) == 0) {		/* child */
		environ = env;
		(void) setgid(gid);
		(void) setuid(uid);
		(void) close(in[0]);
		(void) close(out[1]);
		/* its stdout and stderr is the pipe, its stdin is our fdmail */
		(void) close(0);
		(void) dup(out[0]);		/* in fd 0 */
		(void) close(1);
		(void) dup(in[1]);		/* in fd 1 */
		(void) close(2);
		(void) dup(in[1]);		/* in fd 2 */
		(void) close(out[0]);
		(void) close(in[1]);
		(void) signal(SIGINT, SIG_IGN);
		(void) signal(SIGHUP, SIG_IGN);
		(void) signal(SIGTERM, SIG_DFL);
		/*
		 * Note that argv[0] is set to the command we are running.
		 * That way, we should get some better error messages, at
		 * least more understandable in rejection messages.
		 * Some bourne shells may go into restricted mode if the
		 * stuff to run contains an 'r'. XX: investigate.
		 */
		(void) execl("/bin/sh", rp->addr->user+1,
				 "-c", rp->addr->user+1, (char *)NULL);
		(void) execl("/sbin/sh", rp->addr->user+1,
				 "-c", rp->addr->user+1, (char *)NULL);
		(void) write(2, "Cannot exec /bin/sh\n", 20);
		_exit(128);
	} else if (pid < 0) {			/* fork failed */
		DIAGNOSTIC(rp, EX_OSERR, "cannot fork", 0);
		return;
	}					/* parent */
	(void) close(out[0]);
	(void) close(in[1]);
	errfp = fdopen(in[0], "r");
	/* write the message */
	fp = putmail(rp, out[1], messagefd, timestring, rp->addr->user+1, dp);
	if (fp == NULL) {
		pid = wait(&status);
		(void) close(out[1]);
		(void) fclose(errfp);
		(void) close(in[0]);
		return;
	}
	(void) fclose(fp);
	/* read any messages from its stdout/err on in[0] */
	/* ... having forked and set up the pipe, we quickly continue */
	if (fgets(buf, sizeof buf, errfp) == NULL)
		buf[0] = '\0';
	else if ((cp = strchr(buf, '\n')) != NULL)
		*cp = '\0';
	pid = wait(&status);
	(void) close(out[1]);
	(void) fclose(errfp);
	(void) close(in[0]);
	cp = buf + strlen(buf);
#ifdef	USE_UNIONWAIT
	if (status.w_termsig) {
		if (cp != buf)
			*cp++ = ' ';
		(void) sprintf(cp, "[signal %d", status.w_termsig);
		if (status.w_coredump)
			(void) strcat(cp, " (Core dumped)");
		(void) strcat(cp, "]");
		i = EX_TEMPFAIL;
	} else if (status.w_retcode) {
		if (cp != buf)
			*cp++ = ' ';
		(void) sprintf(cp, "[exit status %d]", status.w_retcode);
		i = EX_UNAVAILABLE;
	} else
		i = EX_OK;
#else	/* !USE_UNIONWAIT */
	if ((status&0177) > 0) {
		if (cp != buf)
			*cp++ = ' ';
		(void) sprintf(cp, "[signal %d", status&0177);
		if (status&0200)
			(void) strcat(cp, " (Core dumped)");
		(void) strcat(cp, "]");
		i = EX_TEMPFAIL;
	} else if (((status>>8)&0377) > 0) {
		if (cp != buf)
			*cp++ = ' ';
		(void) sprintf(cp, "[exit status %d]", (status>>8)&0377);
		i = EX_UNAVAILABLE;
	} else
		i = EX_OK;
#endif	/* USE_UNIONWAIT */
	DIAGNOSTIC(rp, i, "%s", buf);
	return;
}

/*
 * creatembox - see if we can create the mailbox
 */
int
creatembox(rp, filep, uid, gid, pw)
	struct rcpt *rp;
	char **filep;
	short *uid, *gid;
	struct passwd *pw;
{
	char **maild;
	int fd;
	extern int exstat(), setupuidgid(), createfile();
	extern void setrootuid();

	*uid = (short)pw->pw_uid;
	*gid = (short)pw->pw_gid;

	for (maild = &maildirs[0]; *maild != 0; maild++) {
		if (*filep != NULL)
			free(*filep);
		*filep = emalloc(2+strlen(*maild)+strlen(rp->addr->user));
		(void) sprintf(*filep, "%s/%s", *maild, rp->addr->user);
		if ((fd = createfile(rp, *filep, *uid)) >= 0) {
#ifdef	USE_FCHOWN
			(void) fchown(fd, *uid, *gid);
#else	/* !USE_FCHOWN */
			(void) chown(*filep, *uid, *gid);
#endif	/* USE_FCHOWN */
			(void) close(fd);
			return 1;
		}
	}

	/* assert *filep != NULL */
	if (fd == -1)
		DIAGNOSTIC(rp, EX_CANTCREAT, "can't create \"%s\"", *filep);
	/* otherwise the message was printed by createfile() */
	free(*filep);
	return 0;
}

int
createfile(rp, file, uid)
	struct rcpt *rp;
	char *file;
	short uid;
{
	int fd, i = 0, saverrno;
	struct stat st;
	char *cp, msg[BUFSIZ];
	extern char *strerror();
	extern int setupuidgid(), exstat();
	extern void setrootuid();

	if ((fd = open(file, O_RDWR|O_CREAT|O_EXCL, MAILMODE)) < 0) {
		saverrno = errno;
		if ((cp = strrchr(file, '/')) != NULL) {
			*cp = '\0';
			if (exstat(rp, file, &st, stat) < 0) {
				DIAGNOSTIC(rp, rp->status,
				"file information unavailable for \"%s\"", file);
				return -2;
			}
			if (st.st_mode & 020) {		/* group writable? */
				if (!setupuidgid(rp, uid, st.st_gid)) {
					DIAGNOSTIC(rp, rp->status,
		       "failed changing group id to create file in \"%s\"",
		      					file);
					return -2;
				}
			}
			*cp = '/';
		}
		if ((fd = open(file, O_RDWR|O_CREAT|O_EXCL, MAILMODE)) < 0) {
			setrootuid(rp);
			saverrno = errno;
		} else {
			setrootuid(rp);
			return fd;
		}
	} else
		return fd;

	/* No system calls in this spot -- must preserve errno */
	if (TEMPFAIL(saverrno))	/* temporary error? */
		i = EX_TEMPFAIL;
	else if (saverrno != EACCES && saverrno != ENOENT)
		i = EX_UNAVAILABLE;
	else if (saverrno == EACCES)
		i = EX_NOPERM;
	else /* if (saverrno == ENOENT) */
		i = EX_CANTCREAT;
	/* convoluted to maintain 4 arguments to DIAGNOSTIC */
	(void) sprintf(msg, "error (%s) creating \"%%s\"", strerror(saverrno));
	DIAGNOSTIC(rp, i, msg, file);
	return -2;
}

/*
 * setupuidgid - set the euid and gid of the process
 */
int
setupuidgid(rp, uid, gid)
	struct rcpt *rp;
	int uid;
	int gid;
{
	extern void setrootuid();
	
	setrootuid(rp);
	if (setgid(gid) < 0) {
		DIAGNOSTIC(rp, EX_OSERR, "can't setgid to %d", gid);
		return 0;
	}
	if (*(rp->addr->user) == TO_FILE || *(rp->addr->user) == TO_PIPE)
		uid = atoi(rp->addr->misc);
	if (setreuid(-1, uid) < 0) {
		if (uid < 0 && atoi(rp->addr->misc) < 0) {
			/* use magic non-sense +ve uid < MAXSHORT */
			rp->addr->misc = NONUIDSTR;
			return setupuidgid(rp, atoi(NONUIDSTR), gid);
		}
		DIAGNOSTIC(rp, EX_OSERR, "can't seteuid to %d", uid);
		return 0;
	}
	currenteuid = uid;
	return 1;
}


/*
 * exists - see if a mail box exists.  Looks at the exit status
 *	    to guess whether failure is due to nfs server being
 *	    down.
 */

char *
exists(maildir, rp)
	char *maildir;
	struct rcpt *rp;
{
	char *file;

	file = emalloc(2+strlen(maildir)+strlen(rp->addr->user));
	(void) sprintf(file, "%s/%s", maildir, rp->addr->user);
	if (access(file, F_OK) == 0) {	/* file exists */
		rp->status = EX_OK;
		return file;
	}
	free(file);

	if (TEMPFAIL(errno)) {			/* temporary error? */
		DIAGNOSTIC(rp, EX_TEMPFAIL, "error accessing \"%s\"", file);
	} else if (errno == ENOENT || errno == EACCES)	/* really not there? */
		rp->status = EX_OK;
	else {					/* who knows? */
		DIAGNOSTIC(rp, EX_SOFTWARE,
			       "unexpected error accessing \"%s\"", file);
	}
	return NULL;
}


/*
 * setrootuid - set us back to root uid
 */

void
setrootuid(rp)
	struct rcpt *rp;
{
	if (currenteuid != 0) {
		if (setreuid(-1, 0) < 0)
			DIAGNOSTIC(rp, EX_OSERR, "can't reset uid to root", 0);
	}
	currenteuid = 0;
}


/*
 * appendlet - append letter to file pointed at by fd
 */
int
appendlet(rp, fp, file, mfd)
	struct rcpt *rp;
	FILE *fp;
	int mfd;
	char *file;
{
	static char buffer[BUFSIZ];
	register int i;
	register int bufferfull;
	extern int writebuf();

	/* can we use cache of message body data */
	if (readalready != 0) {
		if (writebuf(fp, buffer, readalready) != readalready) {
			DIAGNOSTIC(rp, EX_IOERR,
				       "write to \"%s\" failed", file);
			return EOF;
		}
		rp->status = EX_OK;
		return 0;
	}

	/* we are assumed to be positioned properly at start of message body */
	bufferfull = 0;
	(void) writebuf((FILE *)NULL, (char *)NULL, 0);	/* initialize state */
	while ((i = read(mfd, buffer, sizeof buffer)) != 0) {
		if (i < 0) {
			DIAGNOSTIC(rp, EX_IOERR,
				       "read error from message file", 0);
			return EOF;
		}
		if (writebuf(fp, buffer, i) != i) {
			DIAGNOSTIC(rp, EX_IOERR,
				       "write to \"%s\" failed", file);
			return EOF;
		}
		readalready = i;
		bufferfull++;
	}

	if (bufferfull > 1)	/* not all in memory, need to reread */
		readalready = 0;
	return 0;
}


/*
 * estat - stat with error checking
 */
int
exstat(rp, file, stbufp, statfcn)
	struct rcpt *rp;
	char *file;
	struct stat *stbufp;
	int (*statfcn)();
{
	if (statfcn(file, stbufp) < 0) {
		if (TEMPFAIL(errno))
			rp->status = EX_TEMPFAIL;
		else
			rp->status = EX_SOFTWARE;
		DIAGNOSTIC(rp, rp->status, "can't stat \"%s\"", file);
		return -1;
	}
	return 0;
}

#if	defined(BIFF) || defined(RBIFF)
void
biff(hostname, username, offset)
	char *hostname, *username;
	long offset;
{
	int f, symid;
	struct hostent *hp;
	static struct servent *sp = NULL;
	struct sockaddr_in biffaddr, *bap;
	struct spblk *spl;
	static struct sptree *spt_hosts = NULL;
	char *buf;
	extern int symbol();
	extern void hp_init();
	extern char **hp_getaddr();

	if (sp == NULL && (sp = getservbyname("biff", "udp")) == NULL)
		return;
	symid = symbol(hostname);
	if (spt_hosts == NULL)
		spt_hosts = sp_init();
	spl = sp_lookup(symid, spt_hosts);
	if (spl == NULL) {
		if ((hp = gethostbyname(hostname)) == NULL)
			return;
		bzero((char *)&biffaddr, sizeof biffaddr);
		biffaddr.sin_family = hp->h_addrtype;
		hp_init(hp);
		bcopy(*hp_getaddr(), (char *)&biffaddr.sin_addr, hp->h_length);
		biffaddr.sin_port = sp->s_port;
		bap = (struct sockaddr_in *)emalloc(sizeof(struct sockaddr_in));
		*bap = biffaddr;
		(void) sp_install(symid, (char *)bap, 0, spt_hosts);
	} else
		biffaddr = *(struct sockaddr_in *)spl->data;
	buf = emalloc(3+strlen(username)+20);
	(void) sprintf(buf, "%s@%ld\n", username, offset);
	f = socket(AF_INET, SOCK_DGRAM, 0);
	(void) sendto(f, buf, strlen(buf)+1, 0,
		      (struct sockaddr *)&biffaddr, sizeof biffaddr);
	free(buf);
	(void) close(f);
}
#endif	/* BIFF || RBIFF */


#ifdef RBIFF
readrwho()
{
	struct whod wd;
	struct outmp outmp;
#define NMAX sizeof(outmp.out_name)
#define LMAX sizeof(outmp.out_line)

	int cc, width, f, n, i, symid;
	register struct whod *w = &wd;
	register struct whoent *we;
	register char *tp, *s;
	register FILE *fi;
	long now;
	char username[NMAX+1];
	DIR *dirp, *opendir();
	struct direct *dp, *readdir();
	struct userhost *uhp;
	struct spblk *spl;
	
	if (spt_users == NULL)
		return;
	now = time(0);
	if (chdir(RWHODIR) || (dirp = opendir(".")) == NULL ) {
		perror(RWHODIR);
		exit(EX_OSFILE);
	}
	while (dp = readdir(dirp)) {
		if (dp->d_ino == 0
		    || strncmp(dp->d_name, "whod.", 5)
		    || (f = open(dp->d_name, 0)) < 0 )
			continue;
		cc = read(f, (char *)&wd, sizeof (struct whod));
		if (cc < WHDRSIZE) {
			(void) close(f);
			continue;
		}
		if (now - w->wd_recvtime > 5 * 60) {
			(void) close(f);
			continue;
		}
		cc -= WHDRSIZE;
		we = w->wd_we;
		for (n = cc / sizeof (struct whoent); n > 0; n--,we++){
			/* make sure name null terminated */
			strncpy(username, we->we_utmp.out_name, NMAX);
			username[NMAX] = 0;
			/* add to data structure */

			symid = symbol(username);
			if ((spl = sp_lookup(symid, spt_users)) == NULL)
				continue;
			uhp = (struct userhost *)spl->data;
			if (uhp != NULL
			    && strcmp(uhp->hostname, w->wd_hostname) == 0)
				continue;

			uhp = (struct userhost *)emalloc(sizeof (struct userhost));
			uhp->next = (struct userhost *)spl->data;
			uhp->hostname = emalloc(strlen(w->wd_hostname)+1);
			strcpy(uhp->hostname, w->wd_hostname);
			spl->data = (unsigned char *)uhp;
		}
		close(f);
	}
	closedir(dirp);
}

void
rbiff(nbp)
	struct biffer *nbp;
{
	struct spblk *spl;
	struct userhost *uhp;
	int symid;

	symid = symbol(nbp->user);
	if ((spl = sp_lookup(symid, spt_users)) == NULL)
		return;
	for (uhp = (struct userhost *)spl->data; uhp != NULL; uhp = uhp->next)
		biff(uhp->hostname, nbp->user, nbp->offset);
}
#endif	/* RBIFF */

/*
 * Writebuf() is like write(), except any instances of "From " at the
 * beginning of a line cause insertion of '>' at that point.
 */

int
writebuf(fp, buf, len)
	FILE *fp;
	char *buf;
	int len;
{
	register char *cp, *s;
	register int n;
	int wlen, tlen, i = 0;
	register char expect;
	static char save = '\0';

	if (buf == NULL) {		/* magic initialization */
		save = 'F';
		return 0;
	}
	expect = save;
	for (cp = buf, n = len, wlen = tlen = 0, s = NULL; n > 0; --n, ++cp) {
		if (*cp == '\n')
			expect = 'F';
		else if (expect != '\0') {
			if (*cp == expect) {
				switch (expect) {
				case 'F':	expect = 'r'; s = cp; break;
				case 'r':	expect = 'o'; break;
				case 'o':	expect = 'm'; break;
				case 'm':	expect = ' '; break;
				case ' ':
					/* write from start of buffer to s */
					if (s > buf && tlen >= 0) {
						wlen = fwrite(buf,
							      1, s-buf, fp);
						if (wlen > 0)
							tlen += wlen;
						else
							tlen = -1;
					} else if (s == NULL) {
						i = sizeof FROM_ -(cp-buf)-2;
						fseek(fp, -i, 1);
					}
					/* write separator character */
					if (tlen >= 0) {
						save = '>';
						(void) fwrite(&save, 1, 1, fp);
					}
					expect = '\0';
					if (s == NULL) {
						fwrite(FROM_, 1, i, fp);
						s = buf;
					}
					/* anticipate future instances */
					buf = s;
					break;
				}
			} else
				expect = '\0';
		}
	}
	save = expect;
	if (cp > buf && tlen >= 0) {
		if ((wlen = fwrite(buf, 1, cp-buf, fp)) > 0)
			tlen += wlen;
		else
			tlen = -1;
	}
	return tlen;	/* errno is correct if tlen == -1 ! */
}
