/* SmAccessD (c) 2000 Stefan Richter <richtest@bauwesen.tu-cottbus.de>
 *
 * S*ndmail Access Database Update Daemon v1.0
 *
 * This daemon updates the access database of sendmail[TM] version 8.9.x
 * or higher whenever a remote POP client logs in.  This allows for "SMTP
 * after POP" (or "POP before SMTP" or "POP authenticated relaying").
 * After a certain time, POP client addresses are removed from access_db.
 * You need a POP daemon that writes client addresses into a named pipe,
 * e.g. a patched qpopper of Qualcomm, Inc.  A patch is available at
 * http://www.bauwesen.tu-cottbus.de/~richtest/smaccessd/.
 *
 * Edit smaccesd.h to suit your site's configuration.
 *
 * DISCLAIMER: Redistribution and use of this software in source and
 * binary forms, with or without modification, are permitted.  The
 * program is provided "as is" without warranty of any kind, either
 * expressed or implied, including, but not limited to, the implied
 * warranties of merchantability and fitness for a particular purpose.
 * The entire risk as to the quality and performance of the program is
 * with you.
 */

#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <sys/stat.h>
#include <syslog.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <time.h>
#include <sys/time.h>
#include "smaccessd.h"

/* some other makemap arguments */
#define MAKEMAP_NAME       "makemap"
#define MAKEMAP_AUGMENT    "-o"
#define MAKEMAP_REPLACE    "-r"

/* read buffer for addresses from FIFO */
#define ADDRESS_READBUFFER     100

/* return code of InetAddr() on failure */
#define INVALID_ADDRESS       0xffffffff

/* permission flags of named pipe */
#ifdef  WORLDWRITEABLE_FIFO
#define FIFO_PERMISSION          0622
#else
#define FIFO_PERMISSION          0620
#endif



void ParentSignalHandler(int signum);
void ChildSignalHandler(int signum);
void Report(int pri, const char *format, const char *other, int err);
void DiscardText(void);
int  RemoveFiles(void);
int  CheckFiles(void);
int  ReadAccessText(void);
int  WriteAccessMap(int augment, int clear);
void EndlessLoop(void);
int  InetNtoA(char *str, unsigned long int a0);
unsigned long int InetAddr(const char *str);


/* buffer for ETC_MAIL_ACCESSTXT file */
char  *accesstext;
size_t accesstext_len;

/* file descriptor of named pipe, -1 if not open */
int fifo_des=-1;

/* list of addresses/masks to exclude */
const char *exclude_s[]={ EXCLUDE_ADDRESSES };

/* same in binary form */
unsigned long int exclude_a[sizeof(exclude_s)/sizeof(*exclude_s)];

/* recent POP clients */
typedef struct {
	/* client address */
	unsigned long int a;

	/* counter of cycles since client logged in the last time */
	unsigned char t;

	/* counter of cycles since address has been reported to syslog *
	 * (0xFF means "not yet written to database nor to syslog")    */
	unsigned char l;
} CACHE_TYPE;
CACHE_TYPE cache[CACHE_ENTRIES];

/* maximal value that counters in type CACHE_TYPE can hold */
#define MAX_COUNT  255

/* 0 means connected to parent's tty */
int daemon_detached;

/* effective user and group id */
gid_t daemon_euid, daemon_egid;

/* process ID of daemon */
pid_t daemon_pid;

/* process ID of a running makemap child process (most of the time 0) */
pid_t makemap_pid;

/* last modification of access text */
time_t access_mtime;


int main(int argc, char **argv)
{
	int  i, fail;
	FILE *handle;
	
	/* is another smaccessd running? */
	handle = fopen(VAR_RUN__PID, "r");
	if (handle) {
		if (fscanf(handle, "%d", &i)==1) {
			/* let him die,... */
			if (!kill(i, SIGTERM)) {
				sleep(3);
				/* ...die, die! */
				kill(i, SIGKILL);
			}
		}
		fclose(handle);
	}
	
	/* spawn background process */
	daemon_pid = fork();
	switch (daemon_pid) {

	case -1:  /*** Error ***/
		
		Report(1, "Cannot fork: %s", NULL, 1);
		break;

	case 0:   /*** Child ***/

		/* whoami */
		daemon_euid = geteuid();
		daemon_egid = getegid();
		daemon_pid  = getpid();

		/* don't exit if a pipe breaks */
		signal(SIGPIPE, SIG_IGN);

		/* SIGUSR2 is for testing */
		signal(SIGUSR2, SIG_IGN);

		/* default file creation permissions mask */
		umask(DEFAULT_UMASK);

		/* hello world, that's me */
		handle = fopen(VAR_RUN__PID, "w");
		fail = handle==NULL;
		if (handle) {
			fail  = fprintf(handle, "%d\n", (int)daemon_pid)<2;
			fail |= fclose(handle);
		}
		if (fail)
			Report(1, "Cannot write %s: %s", VAR_RUN__PID, 1);

		/* build binary representation of exclude list */
		for (i=0; i<sizeof(exclude_s)/sizeof(*exclude_s); ++i)
			exclude_a[i] = InetAddr(exclude_s[i]);
		if ((i&=1))
			Report(1, "Exclude list corrupt. Edit source and recompile", NULL, 0);
		fail |= i;

		/* don't trust an already existing FIFO (truncate() has no effect) */
		unlink(VAR_RUN__FIFO);

		/* create FIFO and read access_db clear text source file */
		fail |=      CheckFiles();
		fail |=  ReadAccessText();

		/* won't write access_db if already failed */
		if (fail || WriteAccessMap(0,1)) {
			RemoveFiles();
			break;
		}

		/* open a connection to syslog */
		{
			char *ident;
			for (ident=*argv+strlen(*argv); ident>*argv; --ident)
				if (*ident=='/') {++ident; break;}
			openlog(ident, 0, SYSLOG_FACILITY);
		}

		/* disconnect from standard i/o (detach from tty) */
		daemon_detached = 1;
		close(STDIN_FILENO);
		close(STDOUT_FILENO);
		close(STDERR_FILENO);

		signal(SIGTERM, ChildSignalHandler);  /* clean up and exit */
		signal(SIGHUP,  ChildSignalHandler);  /* reconfigure       */

		/* everything OK, terminate parent */
		if (kill(getppid(), SIGUSR1)) {
			Report(1, "Cannot kill -USR1 parent: %s", NULL, 1);
			RemoveFiles();
			break;
		}

		Report(0, "started with PID %d", (char *)daemon_pid, 0);
		EndlessLoop();
		
	default:  /*** Parent ***/
		
		/* signal SIGUSR1 == child succeeded */
		signal(SIGUSR1, ParentSignalHandler);

		/* wait for either the child terminating or a signal incoming */
		if (wait(NULL)==daemon_pid)
			Report(1, "Background process died", NULL, 0);
		else
			Report(1, "No receipt from background process", NULL, 0);
	}
		
	exit(EXIT_FAILURE);
}



void ParentSignalHandler(int signum)
{
	exit(EXIT_SUCCESS);
}



void ChildSignalHandler(int signum)
{
	/* block SIGHUP during signal processing */
	signal(SIGHUP, SIG_IGN);
	
	/* clean up end exit */
	if (signum==SIGTERM) {
		int fail;
		if (makemap_pid) kill(makemap_pid, SIGTERM);
		fail  = WriteAccessMap(0,1);
		fail |= RemoveFiles();
		Report(0, "Exit after SIGTERM", NULL, 0);
		exit(fail ? EXIT_FAILURE : EXIT_SUCCESS);
	}
		
	/* reconfigure */
	access_mtime = 0;
	CheckFiles();
	ReadAccessText();
	WriteAccessMap(0,0);
	
	signal(SIGHUP, ChildSignalHandler);
}



void Report(int pri, const char *format, const char *other, int err)
{
	const char *errst = err?strerror(errno):NULL;
	
	if (!other) other = errst;
	
	if (daemon_detached) {
		syslog(pri?SYSLOG_ERROR:SYSLOG_INFO, format, other, errst);
	} else {
		FILE *handle = pri?stderr:stdout;
		fprintf(handle, format, other, errst);
		fputs(".\n", handle);
	}
}



void DiscardText(void)
{
	free(accesstext);
	accesstext = NULL;
	accesstext_len = 0;
}



int RemoveFiles(void)
{
	/* binary OR because of lazy execution of logic OR */
	return unlink(VAR_RUN__FIFO) | unlink(VAR_RUN__PID);
}



int CheckFiles()
{
	struct stat statbuf;
	FILE        *handle;
	int            i, j;
	
	i = stat(VAR_RUN__FIFO, &statbuf);
	/* fifo cannot be stat'ed? */
	if (i) {
		if (errno!=ENOENT) {
			Report(1, "Cannot stat %s: %s", VAR_RUN__FIFO, 1);
			return 1;
		}
	/* is FIFO no FIFO or has it wrong permissions or owner? */ 
	} else {
		if ((statbuf.st_mode & 0777) != FIFO_PERMISSION ||
				!S_ISFIFO(statbuf.st_mode) ||
				statbuf.st_uid != daemon_euid ||
				statbuf.st_gid != daemon_egid ) {
			if (fifo_des!=-1) {close(fifo_des); fifo_des = -1;}
			if (unlink(VAR_RUN__FIFO)) {
				Report(1, "Cannot unlink %s: %s", VAR_RUN__FIFO, 1);
				return 1;
			}
			i = 1;
		}
	}
	/* create a new FIFO */
	if (i) {
		if (fifo_des!=-1) {close(fifo_des); fifo_des = -1;}
		umask(0);
		if (mknod(VAR_RUN__FIFO, S_IFIFO|FIFO_PERMISSION, 0)) {
			umask(DEFAULT_UMASK);
			Report(1, "Cannot create %s: %s", VAR_RUN__FIFO, 1);
			return 1;
		}
		umask(DEFAULT_UMASK);
	}
	/* open FIFO */
	if (fifo_des==-1) {
		fifo_des = open(VAR_RUN__FIFO, O_RDWR|O_NONBLOCK|O_TRUNC);
		if (fifo_des==-1) {
			Report(1, "Cannot open %s: %s", VAR_RUN__FIFO, 1);
			return 1;
		}
	}

	/* check PID file */
	j = 0;
	handle = fopen(VAR_RUN__PID, "r");
	if (handle) {
		i = fscanf(handle, "%d", &j);
		fclose(handle);
	}
	if (j==0 || i==0) {
		Report(1, "Error reading %s", VAR_RUN__PID, 0);
		return 1;
	}
	/* Is j not my PID? If so, is it a plausible PID?                      *
	 * Can this process be sent a SIGUSR2? If not, does this process exist? */
	if (j!=daemon_pid && j>1 && (!kill(j,SIGUSR2) || errno!=ESRCH)) {
		Report(1, "Is another daemon running (PID %d)? Exiting NOW."
			"Access database may be corrupted.", (char *)j, 0);
		exit(EXIT_FAILURE);
	}
	
	return 0;
}



int ReadAccessText()
{
	struct stat statbuf;
	FILE *handle;
	
	if (stat(ETC_MAIL_ACCESSTXT, &statbuf)) {
		Report(0, "Cannot stat %s: %s", ETC_MAIL_ACCESSTXT, 1);
		return 1;
	}
	
	/* return if file seems to be the same old */
	if (statbuf.st_mtime==access_mtime) return 0;

	access_mtime = statbuf.st_mtime;
	DiscardText();
	
	/* empty file */
	if (!statbuf.st_size) return 0;

	accesstext = malloc(statbuf.st_size+1);
	if (!accesstext) {
		Report(1, "Out of heap memory", NULL, 0);
		return 1;
	}

	handle = fopen(ETC_MAIL_ACCESSTXT, "r");
	if (!handle) {
		Report(0, "Cannot read %s: %s", ETC_MAIL_ACCESSTXT, 1);
		DiscardText();
		return 1;
	}

	accesstext_len = fread(accesstext, 1, statbuf.st_size, handle);
	if (ferror(handle)) {
		Report(0, "Error reading %s", ETC_MAIL_ACCESSTXT, 0);
		DiscardText();
		fclose(handle);
		return 1;
	}
		
	fclose(handle);
	return 0;
}



int WriteAccessMap(int augment, int clear)
{
	int pipedes[2], i, fail;
	
	/* set up an anonymous pipe to connect makemap with */
	if (pipe(pipedes)) {
		Report(1, "Cannot create pipe: %s", NULL, 1);
		return 1;
	}
	
	/* spawn a child process */
	makemap_pid = fork();
	switch (makemap_pid) {
		
	case -1:  /*** Error ***/
		
		Report(1, "Cannot fork: %s", NULL, 1);
		close(pipedes[0]);
		close(pipedes[1]);
		return 1;
		
	case 0:   /*** Child ***/
		
		/* write end of pipe not needed */
		close(pipedes[1]);
		
		/* hide FIFO from makemap */
		if (fifo_des!=-1) close(fifo_des);

		/* read end of pipe must become stdin */
		if (*pipedes!=STDIN_FILENO) {
			if (dup2(*pipedes, STDIN_FILENO)==-1) {
				Report(1, "Cannot set up I/O for %s: %s",
						MAKEMAP_NAME, 1);
				exit(EXIT_FAILURE);
			}
			close(*pipedes);
		}
		/* run makemap */
		{
			char *argv[] = {
				MAKEMAP_NAME,    NULL,
				MAKEMAP_REPLACE, MAKEMAP_MAPTYPE,
				ETC_MAIL_ACCESS, NULL};
			argv[1] = augment ? MAKEMAP_AUGMENT : MAKEMAP_NAME;
			execv(USR_SBIN_MAKEMAP, augment ? argv : argv+1);
		}
		Report(1, "Cannot execute %s: %s", USR_SBIN_MAKEMAP, 1);
		exit(EXIT_FAILURE);
	}

	/* release read end of pipe */
	close(*pipedes);
	
	fail = 0;
	
	/* write access_db clear text source */
	if (!augment && accesstext_len)
		fail = write(pipedes[1],accesstext,accesstext_len) < accesstext_len;

	/* write POP login entries */
	if (!clear) {
		char str[16];
		int sln;
	
		write(pipedes[1], "\n", 1);
		for (i=0; i<CACHE_ENTRIES; ++i) {
			/* too old */
			if (cache[i].t*(TIME_OF_CYCLE)>(TIME_IN_CACHE))
				continue;
			
			/* if in "augment" mode, care for new or very old entries only */
			if (augment &&
				cache[i].l*(TIME_OF_CYCLE)<(TIME_TO_REBUILD))
				continue;
			
			/* convert binary to string */
			sln = InetNtoA(str, cache[i].a);
			
			/* if in "augment" mode, write only new entries */
			/* write a line like "1.2.3.4<tab>RELAY<nl>"    */
			/* on success, sln will become zero            */
			if (!augment || cache[i].l==MAX_COUNT) {
				sln = write(pipedes[1],str,sln) - sln
			    	+ write(pipedes[1],"\tRELAY\n",7) - 7;
				fail |= sln;
			}
			/* mark successfully written new entry */
			if (cache[i].l==MAX_COUNT && sln==0) {
				cache[i].l = 1;
				continue;
			}
			/* report very old entry */
			if (cache[i].l*(TIME_OF_CYCLE)>=(TIME_TO_REBUILD)) {
				cache[i].l = 1;
				Report(0, "%s still active", str, 0);
			}
		}
	}
	
	fail |= close(pipedes[1]);
	if (fail) Report(1, "Error while writing to pipe", NULL, 0);
	
	/* wait for child to finish and check return code */
	waitpid(makemap_pid, &i, 0);
	makemap_pid = 0;
	if ((WIFEXITED(i) && WEXITSTATUS(i)) || WIFSIGNALED(i)) {
		Report(1, "%s exited irregularly", MAKEMAP_NAME, 0);
		fail |= 1;
	}
	
	return fail;
}

	

void EndlessLoop(void)
{
	fd_set des_set;
	unsigned long int a;
	int i, j, oldest_j, oldest_t;
	struct timeval pause;
#define MINUTES *60
	time_t currenttime = time(NULL);
	time_t nextcheck   = currenttime + (TIME_TO_CHECKFILES)MINUTES;
	time_t nextrebuild = currenttime + (TIME_TO_REBUILD)MINUTES;
	time_t nextwrite   = currenttime + (TIME_PAUSE);
	time_t nextcycle   = currenttime + (TIME_OF_CYCLE)MINUTES;
	int newaddress=0;
	ssize_t inp, off=0;
	char buffer[ADDRESS_READBUFFER+1];

	/* initialize SMTP after POP list */
	for (i=0; i<CACHE_ENTRIES; ++i) cache[i].t = MAX_COUNT;

	for (;;) {
		/* calculate next time to wake up */
		pause.tv_sec = nextcycle;
		if (newaddress && pause.tv_sec>nextwrite) pause.tv_sec = nextwrite;
		if (pause.tv_sec>nextcheck)               pause.tv_sec = nextcheck;
		if (pause.tv_sec>nextrebuild)             pause.tv_sec = nextrebuild;
		pause.tv_sec -= currenttime;
		
		/* sleep at least 1 second */
		if (pause.tv_sec<1) pause.tv_sec = 1;
		
		/* sleep a bit longer than needed */
		pause.tv_usec = 600000;
		
		/* listen to the FIFO */
		FD_ZERO(&des_set); if (fifo_des!=-1) FD_SET(fifo_des, &des_set);
		i = select(fifo_des+1, &des_set, NULL, NULL, &pause);
		
		currenttime = time(NULL);
		
		if (i==-1) {
			/* on error, take a break */
			if (errno!=EINTR) sleep(TIME_PAUSE);
			/* on error or after signal, begin next cycle */
			continue;
		}
		
		/* update counters */
		if (currenttime>=nextcycle) {
			for (j=0; j<(CACHE_ENTRIES); ++j) {
				/* erase expired address */
				if (cache[j].t*(TIME_OF_CYCLE)>TIME_IN_CACHE) {
					cache[j].a = 0;
					continue;
				}
				if (cache[j].t<MAX_COUNT) ++cache[j].t;
				if (cache[j].l<MAX_COUNT) ++cache[j].l;
			}
			nextcycle += (TIME_OF_CYCLE)MINUTES;
		}
		
		/*** BEGIN READ ***/
		if (i) {
			/* read everything we can get */
			inp = read(fifo_des, buffer+off, ADDRESS_READBUFFER-off) + off;
			
			/* set stop bytes */
			buffer[inp] = '\0';
			for (i=off; i<inp; ++i) if (buffer[i]<=' ') buffer[i] = '\0';
			
			/* point to begin of buffer */
			off = 0;
			
			/* invalidate a */
			a = INVALID_ADDRESS;
			
			/* fetch loginname/adress pairs */
			for (i=0; i<inp; i=off+strlen(buffer+off)) {
				
				/* search start position of current string */
				for (off=i; off<inp; ++off) if (buffer[off]) break;
				
				/* search @ mark (shortest user id is 2 bytes long) */
				for (j=off+2; j<inp && buffer[j]; )
					if (buffer[j++]=='@') break;
				
				/* test if a valid address has been found */
				a = InetAddr(buffer+j);
				
				/* if not, go to next string */
				if (a==INVALID_ADDRESS) continue;
				
				/* rewrite to InetAddr()'s interpretation */
				/* InetNtoA(buffer+j, a); */
					
				/* exclude this address? */
				for (j=0; j<sizeof(exclude_s)/sizeof(*exclude_s); j+=2) {
					if ((a & exclude_a[j+1])==exclude_a[j]) {
						j = -1;
						break;
					}
				}
				/* is address already in cache? */
				if (j>-1) for (j=0; j<(CACHE_ENTRIES); ++j) {
					if (cache[j].a==a) {
						cache[j].t = 0;
						j = -1;
						break;
					}
				}
				/* insert */
				if (j>-1) {
					/* we have a new address */
					newaddress = 1;
					/* find oldest entry... */
					oldest_j = oldest_t = 0;
					for (j=0; j<(CACHE_ENTRIES); ++j) {
						if (cache[j].t>oldest_t) {
							oldest_t = cache[j].t;
							oldest_j = j;
						}
					}
					/* ...overwrite it... */
					cache[oldest_j].a = a;
					cache[oldest_j].t = 0;
					cache[oldest_j].l = MAX_COUNT;

					/* ...report new entry... */
					Report(0, "%s", buffer+off, 0);
				}
				/* ...and read next string */
			}
			
			if (a==INVALID_ADDRESS) {
				/* shift uninterpretable string to begin of buffer,
				   maybe it will be completed by next read */
				for (i=off; i<inp; ++i) buffer[i-off] = buffer[i];
			
				/* calculate buffer offset for next read() */
				off = inp-off;
				
				/* too much junk? simply throw it away. */
				if (off>ADDRESS_READBUFFER*4/5) off = 0;
			} else
				off = 0;
			
		}
		/*** END READ ***/
		
		/* check configuration for modifications */
		if (currenttime>=nextcheck) {
			CheckFiles();
			ReadAccessText();
			nextcheck += (TIME_TO_CHECKFILES)MINUTES;
		}
		
		/* fall asleep if not allowed to write again */
		if (currenttime<nextwrite) continue;
		
		/* rebuild access_db from scratch */
		if (currenttime>=nextrebuild) {
			WriteAccessMap(0,0);
			nextrebuild += (TIME_TO_REBUILD)MINUTES;
			nextwrite = currenttime + (TIME_PAUSE);
			newaddress = 0;
		}
		
		/* add new addresses */
		if (newaddress) {
			WriteAccessMap(1,0);
			nextwrite = currenttime + (TIME_PAUSE);
			newaddress = 0;
		}
		
	} /*** for (;;) ***/
	
}


/* similar to inet_ntoa() */
int InetNtoA(char *str, unsigned long int a0)
{
	unsigned long int a1, a2, a3;
	
	a3 = a0 & 255;  a0 >>= 8;
	a2 = a0 & 255;  a0 >>= 8;
	a1 = a0 & 255;  a0 >>= 8;
	return sprintf(str, "%lu.%lu.%lu.%lu", a0, a1, a2, a3);
}


/* similar to inet_addr(), but insists on 4 decimals */
unsigned long int InetAddr(const char *str)
{
	int dots;
	unsigned long int dec, ret;
	
	for (dec=ret=dots=0; *str; ++str) {
		if (*str=='.' &&
				/* dots must be surrounded by decimals */
				*--str>='0' && *str++<='9' &&
				*++str>='0' && *str--<='9') {
			++dots;
			ret <<= 8;
			dec = 0;
			continue;
		}
		if (*str<='9' && *str>='0') {
			dec = dec*10 + *str-'0';
			if (dec<256) {
				ret = (ret & 0xffffff00) | dec;
				continue;
			}
		}
		/* an error occured */
		dots = 0;
		break;
	}
	
	return dots==3 ? ret : INVALID_ADDRESS;
}


/* end of smaccessd.c */

