/*
* rcptfilter.c - written by vesely in milan on 8 aug 2006
*
* Run rcptfilter.sh
* gcc -W -O -DNDEBUG -o /usr/local/sbin/rcptfilter rcptfilter.c
* gcc -W -Wall -Wno-parentheses -O0 -g -o rcptfilter rcptfilter.c
*/

#define SCRIPTFILE "rcptfilter.sh"

static char const usage[] =
"usage:\n"
"\trcptfilter -h\n"
"\trcptfilter -D uid/gid -M rcptfilter[-ext] ...\n"
"The first format is only useful to print this note.\n"
"The second format is used by Courier's local output module if the\n"
"maildropfilter configuration file contains the path of this executable.\n"
"In this case, rcptfilter changes directory to HOME and looks for file\n"
"\"" SCRIPTFILE "\" and runs it (with null input and output, logged error)\n"
"and then returns an exit code of 0 if the script exits rc < 50, 1 otherwise.\n"
"The script will have argument $1 set to \"RCPT\" and the other arguments\n"
"set to whatever was passed to rcptfilter as ellipsis (...). The variables\n"
"LOCAL and HOST will be set from ext. The rest of the environment remains.";

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <syslog.h>
#include <ctype.h>
#include <errno.h>
#include <assert.h>

static volatile int
	signal_child = 0,
	signal_timed_out = 0,
	signal_break = 0;

static void sig_catcher(int sig)
{
#if !defined(NDEBUG)
	char buf[80];
	unsigned s = snprintf(buf, sizeof buf,
		"rcptfilter[%d]: received signal %d\n",
		(int)getpid(), sig);
	if (s >= sizeof buf)
	{
		buf[sizeof buf - 1] = '\n';
		s = sizeof buf;
	}
	write(2, buf, s);
#endif
	switch(sig)
	{
		case SIGALRM:
			signal_timed_out = 1;
			break;

		case SIGHUP:
		case SIGPIPE:
		case SIGINT:
		case SIGQUIT:
		case SIGTERM:
			signal_break = 1;
			break;

		case SIGCHLD:
			signal_child = 1;
			break;

		default:
			break;
	}
}

#if 0
static void reset_signal(void)
{
	struct sigaction act;
	memset(&act, 0, sizeof act);
	sigemptyset(&act.sa_mask);
	act.sa_handler = SIG_DFL;

	sigaction(SIGALRM, &act, NULL);
	sigaction(SIGPIPE, &act, NULL);
	sigaction(SIGINT, &act, NULL);
	sigaction(SIGTERM, &act, NULL);
	sigaction(SIGHUP, &act, NULL);
	sigaction(SIGCHLD, &act, NULL);
}
#endif

static void set_signal(void)
{
	struct sigaction act;
	memset(&act, 0, sizeof act);
	sigemptyset(&act.sa_mask);
		
	act.sa_handler = sig_catcher;
	sigaction(SIGALRM, &act, NULL);
	sigaction(SIGPIPE, &act, NULL);
	sigaction(SIGINT, &act, NULL);
	sigaction(SIGTERM, &act, NULL);
	sigaction(SIGHUP, &act, NULL);
	sigaction(SIGCHLD, &act, NULL);
}

static void addtoenv(char const *name, char const *value)
{
	char *freeonexit = (char*)malloc(strlen(name) + strlen(value) + 1);
	if (freeonexit)
		putenv(strcat(strcpy(freeonexit, name), value));
}

static int run_script(char const *ext, char *argv[], char const *home)
{
	int rtc = 0;
	char *local = strdup(ext);
	if (local)
	{
		int err[2];
		char *host = strchr(local, '@');
		
		if (host)
			*host++ = 0;
		else
			host = "";
		addtoenv("HOST=", host);
		addtoenv("LOCAL=", local);
		free(local);
		
		if (pipe(err) < 0)
			syslog(LOG_CRIT, "Cannot open pipe: %s\n", strerror(errno));
		else
		{
			pid_t const pid = fork();
			if (pid < 0)
				syslog(LOG_CRIT, "Cannot fork: %s\n", strerror(errno));
			else if (pid)
			{
				char buf[2048], *next = &buf[0];
				char *const first = &buf[0], *const last = &buf[sizeof buf - 2];
				
				close(err[1]);
				alarm(30);
				last[1] = 0; // terminator on forced newline
				while (signal_timed_out == 0 &&
					signal_break == 0 &&
					signal_child == 0)
				{
					int rd = read(err[0], next, last - next);
#if !defined NDEBUG
	printf("rd=%2d, next=%2d\n", rd, next - first);
#endif
					if (rd > 0)
					{
						char *p = first, *br;
						next += rd;
					
						*next = next == last? '\n': 0; // force newline if full
					
						while ((br = strchr(p, '\n')) != NULL)
						{
							int level = LOG_CRIT;
							*br = 0;
					
							/*
							* e.g., "##This is a warning\n"
							*/
							while (*p == '#' && level < LOG_DEBUG)
								++p, ++level;
						
							if (*p) syslog(level, "script: %s\n", p);
							p = br + (br < last);     // +1 if not forced newline
							assert(first <= p && p <= next);
						}
					
						memmove(first, p, next - p);
						next -= p - first;
						assert(first <= next && next < last);
					}
					else if (rd == 0 || errno != EINTR && errno != EAGAIN)
					{
						if (rd)
							syslog(LOG_CRIT, "Pipe broken: %s\n", strerror(errno));
						break;
					}
				}
				alarm(0);
				if (signal_timed_out || signal_break)
				{
					kill(pid, SIGTERM);
				}
				close(err[0]);
				
				for (;;)
				{
					int status;
					pid_t wpid = wait(&status);
					if (wpid < 0 && errno != EAGAIN && errno != EINTR)
					{
						syslog(LOG_CRIT,
							"Cannot wait %s/" SCRIPTFILE "[%u]: %s\n",
							home, (unsigned)wpid, strerror(errno));
						break;
					}
					else if (wpid == pid)
					{
						if (WIFEXITED(status))
						{
							int level, s_rtc = WEXITSTATUS(status);
							switch (s_rtc)
							{
								case 0: case 64: case 99: level = LOG_DEBUG; break;
								default: level = LOG_CRIT; break;
							}
							rtc = s_rtc > 50;
							syslog(level,
								"%s/" SCRIPTFILE " with %s exited %d, rtc=%d\n",
								home, ext, s_rtc, rtc);
						}
						else if (WIFSIGNALED(status))
						{
							syslog(LOG_CRIT,
								"%s/" SCRIPTFILE " terminated with signal %d, rtc=%d\n",
								home, WTERMSIG(status), rtc);
						}
						else continue; // stopped?
						
						break;
					}
				}
			}
			else // child process
			{
				close(0);
				open("/dev/null", O_RDONLY);
				close(1);
				open("/dev/null", O_WRONLY);
				close(2);
				dup(err[1]);
				close(err[0]);
				close(err[1]);
				closelog();
				execv(SCRIPTFILE, argv);
				syslog(LOG_MAIL|LOG_CRIT, "rcptfilter: cannot execv: %s\n",
					strerror(errno));
				exit(0);
			}
		}
		
	}
	return rtc;
}

int main(int argc, char *argv[])
{
	int rtc = 0;
	int i, uid = 0, gid = 0, err = 0;
	char *ext = NULL, *home = getenv("HOME");
	static char const argerror[] = "invoked with wrong argument: ";

	char *xargv[32];
	size_t xargc = 0;
	
	openlog("rcptfilter", LOG_PID, LOG_MAIL);
	xargv[xargc++] = SCRIPTFILE;
	xargv[xargc++] = "RCPT";
	
	for (i = 1; i < argc; ++i)
	{
		char *a = argv[i];
		int pass_it = 1;
		
		if (*a == '-')
		{
			pass_it = 0;
			switch (*++a)
			{
				case 'D':
					if (i + 1 >= argc)
					{
						syslog(LOG_CRIT, "%s-D requires a value\n", argerror);
						++err;
					}
					else
					{
						char *t = NULL;
						a = argv[++i];
						uid = strtoul(a, &t, 10);
						if (t && *t == '/')
						{
							gid = strtoul(t + 1, &t, 10);
							if (t && *t) t = NULL;
						}
						else t = NULL;
						if (t == NULL)
						{
							syslog(LOG_CRIT, "%suidgid is %s\n", argerror, a);
							++err;
						}
					}
					break;
				
				case 'M':
					if (i + 1 >= argc)
					{
						syslog(LOG_CRIT, "%s-M requires a value\n", argerror);
						++err;
					}
					else if (strncmp(a = argv[++i],
						"rcptfilter" , sizeof "rcptfilter" - 1) != 0)
					{
						syslog(LOG_CRIT, "%s-M with value %s\n", argerror, a);
						++err;
					}
					else
					{
						ext = strchr(a, '-');
						if (ext)
							++ext;
						else
							ext = "";
					}
					break;
					
				case 'h':
					puts(usage);
					return 0;
				
				default:
					pass_it = 1;
					break;
			}
		}
		
		if (pass_it && xargc + 1 < sizeof xargv/ sizeof xargv[0])
		{
			xargv[xargc++] = a;
		}
	}
	
	xargv[xargc] = NULL;

	if (geteuid() == 0 && (uid || gid))
	{
		if (gid) setgid(gid);
		setuid(uid);
	}

	set_signal();
	if (home && ext)
	{
		struct stat buf;
		uid_t const me = geteuid();
		gid_t const myg = getegid();
		
		if (chdir(home))
		{
			syslog(LOG_CRIT, "Cannot chdir to %s: %s\n",
				home, strerror(errno));
		}
		else if (stat(SCRIPTFILE, &buf) != 0)
		{
			if (errno != ENOENT)
				syslog(LOG_CRIT, "Cannot stat %s/" SCRIPTFILE ": %s\n",
					home, strerror(errno));
		}
		else if (!S_ISREG(buf.st_mode) || !(
			((S_IXUSR & buf.st_mode) && (me == 0 || me == buf.st_uid)) ||
			((S_IXGRP & buf.st_mode) && (me == 0 || myg == buf.st_gid)) ||
			(S_IXOTH & buf.st_mode)))
		{
			syslog(LOG_INFO, "%s/" SCRIPTFILE " not executable by %d/%d\n",
				home, (int)me, (int)myg);
		}
		else
		{
			rtc = run_script(ext, xargv, home);
		}
	}
	else
	{
		syslog(LOG_CRIT, "%smissing %s%s%s\n", argerror,
			home ? "" : "HOME env variable",
			home || ext ? "" : " and ",
			ext ? "" : "-M argument");
	}
		
	return rtc;
}

