#include <stdio.h>
#include <ctype.h>
#include <signal.h>
#include <sysexits.h>
#include "string.h"
#include "smtp.h"
#include "mail.h"
#include <sys/stat.h>

/*
 *  names of the limit files
 */
#define CON ".smtpscheds"
#define NCON ".nsmtpscheds"

#define	OLD	1*3600L		/* old C file -> try less often */
#define	VOLD	6*3600L		/* very old C file -> try much less often */
#define	OLDW	1*3600L		/* wait time between tries at old files */
#define	VOLDW	4*3600L		/* wait time between tries at very old files */

extern char *UPASROOT;
int warn = -1;
int removedays = -1;
int cleanup;
int verbose;
int testmode;
int Xonly;
int Conly;
string *replyaddr;
string *dest;
int mypid;
char **getcmd();

#define	MAXDEST 50		/* # destinations remembered */
#define	MAXTPERD 5*60		/* total time allowed before skipping dests */
struct {
	string	*dest;
	time_t	time;
} destlist[MAXDEST];		/* time consumed by unsuccessful tries at dest */
int	ndest = 0;

int	debug;


/*
 *  actions to take when locking
 */
#define BLOCK 0		/* wait for 5 minutes if the directory is locked */
#define SKIP 1		/* skip the directory if it is locked */
#define IGNORE 2	/* don't lock the directory or care if it is */

/*
 *  If called with arguments, those arguments are spool directories.  Descend
 *  each one processing the control files in it (C.* and X.*).
 *
 *  If called without arguments, descend all spool directories.
 *
 *  -s #scheds  specifies a maximum number of concurrent smtpscheds.
 *  -w #days	causes users to be warned if their mail files are older
 *		than #days days
 *  -r #days	causes mail older than #days days to be returned to sender
 *  -c		cleanup empty directories
 *  -D		debug
 *  -L level	logging level
 */
usage()
{
	fprintf(stderr, "smtpsched [-cvtDL] [-w #days] [-r #days] [-s #scheds] [dir]\n");
	exit(1);
}

main(ac, av)
	int ac;
	char *av[];
{
	DIR *dirp;
	Direct *dp;
	int max=0;
	int c;
	extern int optind;
	extern char *optarg;

	umask(2);

	mypid = getpid();

	/*
	 *  avoid annoying distractions
	 */
	signal(SIGPIPE, SIG_IGN);
	signal(SIGHUP, SIG_IGN);
	Openlog("smtpsched", LOG_PID, LOG_SMTPSCHED);
	setlogmask(LOG_UPTO(LOG_INFO));

	while ((c = getopt(ac, av, "XCDcvtr:w:s:L:")) != EOF)
		switch (c) {
		case 'X':	Xonly++;		break;
		case 'C':	Conly++;		break;
		case 't':	testmode++;		break;
		case 'v':	verbose++;		break;
		case 'c':	cleanup++;		break;
		case 'r':	removedays = atoi(optarg);	break;
		case 's':	max = atoi(optarg);	break;
		case 'w':	warn = atoi(optarg);	break;
		case 'L':	setloglevel(optarg);	break;
		case 'D':	debug++;		break;
		default:	usage();
		}

	/*
	 *  go to top spool directory
	 */
	if(chdir(SMTPQROOT)<0){
		Syslog(LOG_ALERT, "can't chdir to %s\n", SMTPQROOT);
		exit(1);
	}

	/*
	 *  if there are too many running exit
	 */
	if(max && toomany(max)<0)
		exit(0);

	/*
	 *  If specific directories, do just them.  Keep running the directory till there
	 *  is no change.  
	 */
	if(optind!=ac){
		for(; optind<ac; optind++)
			while(dodir(av[optind], SKIP) && !warn && !removedays)
				;
		return 0;
	}

	/*
	 *  walk through all directories in top directory.  the lock is
	 *  non-blocking (if neither r nor w options specified) to let
	 *  different instances of smtpsched skip over each other.
	 */
	dirp = opendir(".");
	if(dirp==NULL){
		Syslog(LOG_ALERT, "couldn't read %s\n", SMTPQROOT);
		exit(1);
	}
	while(dp = readdir(dirp)){
		if(strcmp(dp->d_name, ".")!=0 && strcmp(dp->d_name, "..")!=0)
			dodir(dp->d_name, (warn>=0 || removedays>=0) ? IGNORE : SKIP);
	}
	closedir(dirp);
	return 0;
}

/*
 *  do both directions in a directory
 */
dodir(dname, action)
	char *dname;
{
	int i, err;
	static string *ds;
	struct stat buf;

	if ((err=stat(dname, &buf)) < 0)
		return 0;

	if (!(buf.st_mode & S_IFDIR))
		return 0;

	ds = s_reset(ds);
	s_append(ds, dname);

	if (debug)
		fprintf(stderr, "Checking %s\n", dname);
	i = dodirdir(s_to_c(ds), action, "X.");
	i += dodirdir(s_to_c(ds), action, "C.");
	return i;
}

/*
 *  walk through all entries in this directory.  process any
 *  not starting with '.'.  lock the directory before proceeding.
 */
dodirdir(dname, action, direction)
	char *dname;
	char *direction;
{
	DIR *dirp;
	Direct *dp;
	int i;
	int changed=0;
	int ents=0, files=0;
	static string *ls;
	extern int errno;

	ls = s_reset(ls);
	s_append(ls, direction);
	s_append(ls, dname);

	/*
	 *  lock the directory.  the lock is in the parent directory.
	 */
	switch(action){
	case BLOCK:
		for(i=0; i<3; i++){
			if(lock(s_to_c(ls))==0)
				break;
			if (debug)
				Syslog(LOG_DEBUG, "pausing for lock");
			sleep(5);
		}
		if(i==3)
			return changed;
		break;
	case SKIP:
		if(lock(s_to_c(ls))<0){
			Syslog(LOG_DEBUG, "couldn't lock %s\n", dname);
			return changed;
		}
		break;
	case IGNORE:
		break;
	}

	/*
	 *  descend into the directory.  if it isn't a directory,
	 *  this will fail.
	 */
	if(chdir(dname)<0){
		if(action != IGNORE)
			unlock(s_to_c(ls));
		return changed;
	}

	/*
	 *  walk through the entries
	 */
	dirp = opendir(".");
	if(dirp==NULL){
		Syslog(LOG_INFO, "couldn't read directory %s\n", dname);
		if(chdir(SMTPQROOT)<0){
			Syslog(LOG_ALERT, "can't chdir back to SMTPQROOT\n");
			exit(1);
		}
		if(action != IGNORE)
			unlock(s_to_c(ls));
		return changed;
	}
	while(dp = readdir(dirp)){
		if(strcmp(dp->d_name, ".")==0 || strcmp(dp->d_name, "..")==0)
			continue;
		files++;
		if (cleanup)
			continue;
		if (dp->d_name[0] == *direction) {
			switch(dofile(dname, dp->d_name)){
			case 0:
				/* file removed */
				changed = 1;
				break;
			case 1:
				/* file left alone */
				ents += 1;
				break;
			}
		}
	}
	closedir(dirp);

	/*
	 *  go back up.  symbolic links could be painful!!!!
	 */
	if(chdir(SMTPQROOT)<0){
		Syslog(LOG_ALERT, "Can't chdir back to SMTPQROOT\n");
		exit(1);
	}

	/*
	 *  cleanup empty directories
	 */
	if(cleanup && files==0){
		Syslog(LOG_DEBUG, "%s empty\n", dname);
		if(rmdir(dname)<0)
			Syslog(LOG_ALERT, "can't unlink: %d\n", errno);
	}

	if(action != IGNORE)
		unlock(s_to_c(ls));
	return changed;
}

/*
 *  process a spool control file.  control file names start with C. or
 *  X.  all error goes into an error file.
 *
 *  return 0 if file removed, 1 otherwise.
 */
dofile(dname, name)
	char *dname;
	char *name;
{
	int rv;
	int fd, ofd;
	char *ef;
	extern char *fileoftype();
	struct stat sb;
	time_t now, Edate, Cdate;

	rv = -1;

	/*
	 *  if the file is inconsistent, remove it
	 */
	if(cleanup && inconsistent(name)){
		Syslog(LOG_NOTICE, "%s/%s inconsistent\n", dname, name);
		unlink(name);
		return 0;
	}

	/*
	 *  if this is not a control file, ignore it
	 */
	if(name[1]!='.' || (name[0]!='C' && name[0]!='X'))
		return 1;

	/*
	 *  if file is too old, warn user and remove it.  if checking age,
	 *  don't run the control file.
	 */
	if(warn>=0 || removedays>=0) {
		if(checkage(name)==0) {
			Syslog(LOG_NOTICE, "%s/%s too old\n", dname, name);
			doremove(name);
			return 0;
		}
		return 1;
	}

	/*
	 *  don't run control file when cleaning up
	 */
	if(cleanup)
		return 1;

	/*
	 * Backoff scheme: don't try old C files very often
	 */
	ef = fileoftype('E', name);
	if (name[0]=='C') {
		now = time((time_t *)0);
		Cdate = now;
		Edate = now-VOLDW-1;
		if (stat(name, &sb)==0)
			Cdate = sb.st_ctime;
		if (stat(ef, &sb)==0)
			Edate = sb.st_mtime;
		if (now-Cdate>VOLD && now-Edate<VOLDW
		 || now-Cdate>OLD  && now-Edate<OLDW) {
			if (verbose)
				Syslog(LOG_DEBUG, "ignore %s/%s: not time yet\n", dname, name);
			if (debug==0)
				return 1;
		}
	}

	/*
	 * in test mode, just return
	 */
	if (testmode) {
		Syslog(LOG_DEBUG, "would process %s/%s\n", dname, name);
		return 1;
	}
	/*
	 *  redirect output to an error file
	 */
	ofd = dup(2);
	close(2);
	fd = open(ef, 1);
	if(fd<0)
		fd = creat(ef, 0666);
	if(fd>=0){
		lseek(fd, 0l, 2);
	
		/*
		 *  process the file
		 */
		if(name[0]=='C') {
			rv = dosmtp(dname, name);
		} else if(name[0]=='X') {
			rv = dormail(dname, name);
		}

		/*
		 *  get old error file back
		 */
		close(2);
		(void) dup(ofd);
		close(ofd);
	}

	/*
	 *  if processing was successful, remove the spool files
	 */
	if(rv==0) {
		doremove(name);
		return 0;
	}
	return 1;
}

/*
 *  remove the control file, data file, and error file
 */
doremove(ctl)
	char *ctl;
{
	fflush(stdout);
	unlink(fileoftype('E', ctl));
	unlink(ctl);
	unlink(fileoftype('D', ctl));
}

/*
 *  run rmail.  rmail takes care of its own errors, so if rmail fails,
 *  just don't remove the files.
 */
dormail(dname, ctl)
	char *dname;
	char *ctl;
{
	char **av;
	int rc;

	/*
	 *  fork off the command
	 */
	if ((av = getcmd(ctl, "/bin/rmail")) == NULL) {
		Syslog(LOG_WARNING, "Could not get rmail params for %s", ctl);
		return -1;
	}

	if ((rc = docmd(ctl, av)) == 0){
		Syslog(LOG_DEBUG, "success");
		return 0;
	} else {
		Syslog(LOG_DEBUG, "failed, rc = %d", rc);
		return -1;
	}
}

/*
 *  run smtp.  if an error occurs, determine its importance and send
 *  a error mail message if it is fatal.
 */
dosmtp(dname, ctl)
	char *dname;
	char *ctl;
{
	static string *cmd;
	int status, i;
	char **av;
	time_t t0, t1;

	/*
	 *  fork off the command
	 */
	cmd = s_reset(cmd);
	s_append(cmd, UPASROOT);
	s_append(cmd, "smtp");
	av = getcmd(ctl, s_to_c(cmd));
	if (av==NULL) {
		Syslog(LOG_WARNING, "Could not get smtp params for %s", ctl);
		return -1;
	}
	/*
	 * Check whether unsuccessful attempts at this destination
	 * have taken too much time.  If so, pass over the file.
	 */
	for (i=0; i<ndest; i++) {
		if (strcmp(s_to_c(dest), s_to_c(destlist[i].dest))==0) {
			if (destlist[i].time > MAXTPERD) {
				Syslog(LOG_DEBUG, "passed %s (%d sec)\n", s_to_c(dest), destlist[i].time);
				fprintf(stderr, "can't contact destination\n");
				return -1;
			}
			break;
		}
	}
	if (i==ndest) {
		if (ndest<MAXDEST)
			ndest++;
		else
			i = 0;		/* loses storage */
/*
 * the following s_copy died on a malformed `C' file.  The
 * contents of these files should be checked more carefully. 
 */
		destlist[i].dest = s_copy(s_to_c(dest));
		destlist[i].time = 0;
	}
	time(&t0);
	switch(status=docmd(ctl, av)){
	case 0:		/* it worked */
		Syslog(LOG_DEBUG, "success\n");
		destlist[i].time = 0;
		return 0;

	case EX_UNAVAILABLE:	/* service unavailable */
	case EX_NOPERM:		/* permission denied */
	case EX_NOUSER:		/* rejected by the other side */
	case EX_NOHOST:		/* host name unknown */
	case EX_DATAERR:	/* data format error */
	case EX_USAGE:		/* command line usage error */
		Syslog(LOG_INFO, "failed with status %d\n", status);
		returnmail(ctl, 1);		/*permanant failure*/
		destlist[i].time = 0;
		return 0;

	case EX_CANTCREAT:	/* can't create (user) output file */
	case EX_IOERR:		/* input/output error */
	case EX_OSERR:		/* system error (e.g., can't fork) */
	case EX_OSFILE:		/* critical OS file missing */
	case EX_SOFTWARE:	/* internal software error */
	case EX_NOINPUT:	/* cannot open input */
	case EX_PROTOCOL:	/* remote error in protocol */
		/* gauss is having flakey datakit errors that confuse the
		 * SMTP protocol.  EX_PROTOCOL is a temporary error for gauss-ches*/
	case EX_TEMPFAIL:	/* temp failure; user is invited to retry */
		Syslog(LOG_INFO, "temp fail with status %d\n", status);
		time(&t1);			/*temporary failure*/
		destlist[i].time += t1-t0;
		return -1;

	default:	/* possibly a temporary problem */
		Syslog(LOG_WARNING, "unknown fail with status %d\n", status);
		time(&t1);
		destlist[i].time += t1-t0;
		return -1;
	}
}

/*
 *  open a control file and parse the first line.  It contains
 *  the reply address and the destination (for returning the mail).
 *
 *  It leaves the control file open and returns the fp.
 */
FILE *
parseline1(ctl)
	char *ctl;
{
	FILE *fp;
	static string *line;

	fp = fopen(ctl, "r");
	if(fp==NULL)
		return NULL;

	/*
	 *  get reply address and destination
	 */
	line = s_reset(line);
	if(s_read_line(fp, line)==NULL){
		fprintf(stderr, "smtpsched: error reading ctl file %s: %s\n", ctl,
			s_to_c(line));
		fclose(fp);
		return NULL;
	}
	replyaddr = s_parse(s_restart(line), s_reset(replyaddr));
	if(replyaddr==NULL){
		fprintf(stderr, "smtpsched: error reading ctl file replyaddr %s\n",
			ctl);
		fclose(fp);
		return NULL;
	}
	dest = s_parse(line, s_reset(dest));
	if(dest==NULL){
		fprintf(stderr, "smtpsched: error reading ctl file dest %s\n",
			ctl);
		fclose(fp);
		return NULL;
	}
	return fp;
}

/*
 * Read control file to get arguments for command.  Leave dest and replyaddr
 * available.  The control file has two lines.  The first is reply address
 * and recipients.  the second is the arguments for the command.
 */
char **
getcmd(ctl, cmd)
	char *ctl;
	char *cmd;
{
	static string *args;
	FILE *fp;
	static char *av[1024];
	int ac=0;
	char *cp;

	fp = parseline1(ctl);
	if (fp==NULL)
		return NULL;

	/*
	 *  make command line
	 */
	av[ac++] = cmd;
	args = s_reset(args);
	if(s_read_line(fp, args)==NULL){
		fprintf(stderr, "smtpsched: error reading ctl file %s\n", ctl);
		fclose(fp);
		return NULL;
	}
	fclose(fp);
	cp = s_to_c(args);
	cp[strlen(cp) - 1] = '\0';	/*zap the newline*/
	Syslog(LOG_INFO, "%s <%s", cmd, ctl);
	for(cp = s_to_c(args); *cp && ac<1023;){
		av[ac++] = cp++;
		while(*cp && !isspace(*cp))
			cp++;
		while(isspace(*cp))
			*cp++ = 0;
	}
	av[ac] = 0;
	return av;
}

/*
 *  execute a command, put standard error in the error file.
 */
docmd(ctl, av)
	char *ctl;
	char **av;
{
	int fd;
	int pid, status;
	int n;

	/*
	 *  fork off the command
	 */
	switch(pid = fork()){
	case -1:
		return -1;
	case 0:
		/*
		 *  make data file standard input
		 */
		close(0);
		fd = open(fileoftype('D', ctl), 0);
		if(fd<0){
			perror("smtpsched: error reading data file:\n");
			exit(1);
		}
	
		/*
		 *  make error file standard output
		 */
		close(1);
		fd = dup(2);
	
		/*
		 *  start the command
		 */
		execvp(av[0], av);
		exit(-2);
	default:
		/*
		 *  wait for the command to terminate
		 */
		while((n = wait(&status))>=0)
			if(n == pid)
				break;
		if(status&0xff)
			return -2;
		else
			return (status>>8)&0xff;
	}

}

/*
 *  see if the number of consumers has been exceeded.  if not, add this process
 *  to the list.
 *
 *  returns 0 if there were the number was not exceeded, -1 otherwise
 */
toomany(max)
	int max;
{
	FILE *ifp=NULL;
	FILE *ofp=NULL;
	int cur=0;
	int pid;

	/*
	 *  lock consumers file
	 */
	if(lock(CON)<0)
		return -1;

	/*
	 *  open old and new consumer files
	 */
	ofp = fopen(NCON, "w");
	if(ofp==NULL){
		fprintf(stderr, "can't open %s\n", NCON);
		goto error;
	}
	ifp = fopen(CON, "r");
	if(ifp!=NULL){
		/*
		 *  see how many consumers are still around
		 */
		while(fscanf(ifp, "%d", &pid)==1){
			if(kill(pid, 0) == 0){
				cur++;
				if(fprintf(ofp, "%d\n", pid)<0){
					fprintf(stderr, "error writing %s\n", NCON);
					goto error;
				}
			}
		}
		if(cur >= max)
			goto error;
	}

	/*
	 *  add us to the group of consumers
	 */
	if(fprintf(ofp, "%d\n", getpid())<0){
		fprintf(stderr, "error writing %s\n", NCON);
		goto error;
	}
	if(ifp!=NULL)
		fclose(ifp);
	if(fclose(ofp)==EOF)
		goto error;
	unlink(CON);
	if(link(NCON, CON)<0)
		fprintf(stderr, "can't link %s to %s file\n", CON, NCON);
	unlink(NCON);
	unlock(CON);
	return 0;
error:
	/*
	 *  too many consumers or we can't make a new consumer file
	 */
	if(ifp!=NULL)
		fclose(ifp);
	if(ofp!=NULL)
		fclose(ofp);
	unlink(NCON);
	unlock(CON);
	return -1;
}

/*
 *  return true if the file is inconsistent.  The following are inconsistent:
 *	- a control file without a datafile
 *	- an error file without a datafile
 *	- a day old data file without a control file
 *	- a limit file of any kind
 */
inconsistent(file)
	char *file;
{
	struct stat s;
	int days;

	/*
	 *  switch on file type
	 */
	switch(file[0]){
	case 'C':
	case 'X':
		/*
		 *  if no data file, control file is inconsistent
		 */
		if(stat(fileoftype('D', file), &s)<0)
			return 1;
		break;
	case 'E':
		/*
		 *  if no control file, error file is inconsistent
		 */
		if(stat(fileoftype('X', file), &s)<0
		&& stat(fileoftype('C', file), &s)<0)
			return 1;

		/*
		 *  if no data file, error file is inconsistent
		 */
		if(stat(fileoftype('D', file), &s)<0)
			return 1;
		break;
	case 'D':
		/*
		 *  look for a control file
		 */
		if(stat(fileoftype('X', file), &s)==0
		|| stat(fileoftype('C', file), &s)==0)
			break;

		/*
		 *  no control file, data file inconsistent if >=1 day old
		 */
		if(stat(file, &s)<0)
			return 0;
		days = (time((long *)0) - s.st_ctime)/(24*60*60);
		if(days>0)
			return 1;
		break;
	default:
		break;
	}
	return 0;
}

/*
 *  check the age of a file.  if it is greater than warn or remove, tell the
 *  sender.  return 0 if the file is to be removed, -1 otherwise.
 */
checkage(ctl)
	char *ctl;
{
	struct stat s;
	int days;
	char buf[256];
	FILE *fp;

	/*
	 *  get the file's age
	 */
	if(stat(ctl, &s)<0)
		return -1;
	days = (time((long *)0) - s.st_ctime)/(24*60*60);

	/*
	 *  check for removal
	 */
	if(removedays>=0 && days>=removedays){
		fp = parseline1(ctl);
		if(fp==NULL)
			return -1;
		fclose(fp);

		Syslog(LOG_INFO,"returning mail to %s orig to %s after %d days",
			s_to_c(replyaddr), s_to_c(dest), days);
		return returnmail(ctl, 1);
	}

	/*
	 *  check for warning
	 */
	if(warn>=0 && days>=warn){
		fp = parseline1(ctl);
		if(fp==NULL)
			return -1;
		fclose(fp);

		Syslog(LOG_INFO, "warning %s about %s after %d days",
			s_to_c(replyaddr), s_to_c(dest), days);
		returnmail(ctl, 0);
	}

	return -1;
}

/*
 *  return a piece of mail with a reason for the return
 */
returnmail(ctl, warn)
	char *ctl;
{
	int pid, status;
	string *cmd;
	int pfd[2];
	char buf[132];
	int fd, n;
	int reads;
	FILE *fp;
	FILE *ifp;
	long now;
	char asct[27];

	if(pipe(pfd)<0)
		return -1;

	switch(pid=fork()){
	case -1:
		close(pfd[0]);
		close(pfd[1]);
		return -1;
	case 0:
		/*
		 *  start up the mailer to take the refusal message
		 */
		close(0);
		dup(pfd[0]);
		close(pfd[1]);
		execl("/bin/rmail", "/bin/rmail",  s_to_c(replyaddr), 0);
		exit(1);
	default:
		/*
		 *  pipe the refusal message to the mailer
		 */
		close(pfd[0]);
		fp = fdopen(pfd[1], "w");
		if(fp==NULL) {
			close(pfd[1]);
			break;
		}

		/*
		 *  the From line
		 */
		now = time((long *)0);
		strcpy(asct, ctime(&now));
		asct[24] = 0;
		fprintf(fp, "From postmaster %s remote from \n", asct);

		/*
		 *  the refusal message
		 */
		if(warn) {
			fprintf(fp, "Subject: smtp mail failed\n\n");
			fprintf(fp, "Your mail to %s is undeliverable.\n",
				s_to_c(dest));
		} else {
			fprintf(fp, "Subject: smtp mail warning\n\n");
			fprintf(fp, "Your mail to %s is not yet delivered.\n",
				s_to_c(dest));
			fprintf(fp, "Delivery attempts continue.\n");
		}

		/*
		 *  then diagnosis of error
		 */
		fprintf(fp, "---------- diagnosis ----------\n");
		ifp = fopen(fileoftype('E', ctl), "r");
		if(ifp!=NULL){
			for(reads=0; reads<20; reads++) {
				if(fgets(buf, sizeof(buf), ifp)==NULL)
					break;
				fputs(buf, fp);
			}
			fclose(ifp);
		}

		/*
		 *  finally the message itself
		 */
		fprintf(fp, "---------- unsent mail ----------\n");
		ifp = fopen(fileoftype('D', ctl), "r");
		if(ifp!=NULL){
			for(reads=0; reads<50; reads++) {
				if(fgets(buf, sizeof(buf), ifp)==NULL)
					break;
				fputs(buf, fp);
			}
			fclose(ifp);
		}
		fclose(fp);

		/*
		 *  wait for the warning to finish
		 */
		while((n = wait(&status))>=0)
			if(n == pid)
				break;
		return status ? -1 : 0;
	}
	close(pfd[1]);
	return -1;
}
