/*	$OpenBSD: spamd.c,v 1.103 2007/11/03 19:16:07 beck Exp $	*/

/*
 * Copyright (c) 2002-2007 Bob Beck.  All rights reserved.
 * Copyright (c) 2002 Theo de Raadt.  All rights reserved.
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <sys/param.h>
#include <sys/file.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <sys/sysctl.h>
#include <sys/resource.h>

#include <netinet/in.h>
#include <arpa/inet.h>

#include <err.h>
#include <errno.h>
#include <getopt.h>
#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>

#include <netdb.h>

#include "sdl.h"
#include "grey.h"
#include "sync.h"

extern int server_lookup(struct sockaddr *, struct sockaddr *,
    struct sockaddr *);

struct con {
	int fd;
	int state;
	int laststate;
	int af;
	struct sockaddr_storage ss;
	void *ia;
	char addr[32];
	char caddr[32];
	char helo[MAX_MAIL], mail[MAX_MAIL], rcpt[MAX_MAIL];
	struct sdlist **blacklists;

	/*
	 * we will do stuttering by changing these to time_t's of
	 * now + n, and only advancing when the time is in the past/now
	 */
	time_t r;
	time_t w;
	time_t s;

	char ibuf[8192];
	char *ip;
	int il;
	char rend[5];	/* any chars in here causes input termination */

	char *obuf;
	char *lists;
	size_t osize;
	char *op;
	int ol;
	int data_lines;
	int data_body;
	int stutter;
	int sr;
} *con;

void     usage(void);
char    *grow_obuf(struct con *, int);
int      parse_configline(char *);
void     parse_configs(void);
void     do_config(void);
int      append_error_string (struct con *, size_t, char *, int, void *);
void     build_reply(struct  con *);
void     doreply(struct con *);
void     setlog(char *, size_t, char *);
void     initcon(struct con *, int, struct sockaddr *);
void     closecon(struct con *);
int      match(const char *, const char *);
void     nextstate(struct con *);
void     handler(struct con *);
void     handlew(struct con *, int one);

char hostname[MAXHOSTNAMELEN];
struct syslog_data sdata = SYSLOG_DATA_INIT;
char *nreply = "450";
char *spamd = "spamd IP-based SPAM blocker";
int greypipe[2];
int trappipe[2];
FILE *grey;
FILE *trapcfg;
time_t passtime = PASSTIME;
time_t greyexp = GREYEXP;
time_t whiteexp = WHITEEXP;
time_t trapexp = TRAPEXP;
struct passwd *pw;
pid_t jail_pid = -1;
u_short cfg_port;
u_short sync_port;

extern struct sdlist *blacklists;
extern int pfdev;
extern char *low_prio_mx_ip; 

int conffd = -1;
int trapfd = -1;
char *cb;
size_t cbs, cbu;

time_t t;

#define MAXCON 800
int maxfiles;
int maxcon = MAXCON;
int maxblack = MAXCON;
int blackcount;
int clients;
int debug;
int greylist = 1;
int grey_stutter = 10;
int verbose;
int stutter = 1;
int window;
int syncrecv;
int syncsend;
#define MAXTIME 400

void
usage(void)
{
	extern char *__progname;

	fprintf(stderr,
	    "usage: %s [-45bdv] [-B maxblack] [-c maxcon] "
	    "[-G passtime:greyexp:whiteexp]\n"
	    "\t[-h hostname] [-l address] [-M address] [-n name] [-p port]\n"
	    "\t[-S secs] [-s secs] "
	    "[-w window] [-Y synctarget] [-y synclisten]\n",
	    __progname);

	exit(1);
}

char *
grow_obuf(struct con *cp, int off)
{
	char *tmp;

	tmp = realloc(cp->obuf, cp->osize + 8192);
	if (tmp == NULL) {
		free(cp->obuf);
		cp->obuf = NULL;
		cp->osize = 0;
		return (NULL);
	} else {
		cp->osize += 8192;
		cp->obuf = tmp;
		return (cp->obuf + off);
	}
}

int
parse_configline(char *line)
{
	char *cp, prev, *name, *msg;
	static char **av = NULL;
	static size_t ac = 0;
	size_t au = 0;
	int mdone = 0;

	name = line;

	for (cp = name; *cp && *cp != ';'; cp++)
		;
	if (*cp != ';')
		goto parse_error;
	*cp++ = '\0';
	if (!*cp) {
		sdl_del(name);
		return (0);
	}
	msg = cp;
	if (*cp++ != '"')
		goto parse_error;
	prev = '\0';
	for (; !mdone; cp++) {
		switch (*cp) {
		case '\\':
			if (!prev)
				prev = *cp;
			else
				prev = '\0';
			break;
		case '"':
			if (prev != '\\') {
				cp++;
				if (*cp == ';') {
					mdone = 1;
					*cp = '\0';
				} else
					goto parse_error;
			}
			break;
		case '\0':
			goto parse_error;
		default:
			prev = '\0';
			break;
		}
	}

	do {
		if (ac == au) {
			char **tmp;

			tmp = realloc(av, (ac + 2048) * sizeof(char *));
			if (tmp == NULL) {
				free(av);
				av = NULL;
				ac = 0;
				return (-1);
			}
			av = tmp;
			ac += 2048;
		}
	} while ((av[au++] = strsep(&cp, ";")) != NULL);

	/* toss empty last entry to allow for trailing ; */
	while (au > 0 && (av[au - 1] == NULL || av[au - 1][0] == '\0'))
		au--;

	if (au < 1)
		goto parse_error;
	else
		sdl_add(name, msg, av, au);
	return (0);

parse_error:
	if (debug > 0)
		printf("bogus config line - need 'tag;message;a/m;a/m;a/m...'\n");
	return (-1);
}

void
parse_configs(void)
{
	char *start, *end;
	int i;

	if (cbu == cbs) {
		char *tmp;

		tmp = realloc(cb, cbs + 8192);
		if (tmp == NULL) {
			if (debug > 0)
				perror("malloc()");
			free(cb);
			cb = NULL;
			cbs = cbu = 0;
			return;
		}
		cbs += 8192;
		cb = tmp;
	}
	cb[cbu++] = '\0';

	start = cb;
	end = start;
	for (i = 0; i < cbu; i++) {
		if (*end == '\n') {
			*end = '\0';
			if (end > start + 1)
				parse_configline(start);
			start = ++end;
		} else
			++end;
	}
	if (end > start + 1)
		parse_configline(start);
}

void
do_config(void)
{
	int n;

	if (debug > 0)
		printf("got configuration connection\n");

	if (cbu == cbs) {
		char *tmp;

		tmp = realloc(cb, cbs + 8192);
		if (tmp == NULL) {
			if (debug > 0)
				perror("malloc()");
			free(cb);
			cb = NULL;
			cbs = 0;
			goto configdone;
		}
		cbs += 8192;
		cb = tmp;
	}

	n = read(conffd, cb + cbu, cbs - cbu);
	if (debug > 0)
		printf("read %d config bytes\n", n);
	if (n == 0) {
		parse_configs();
		goto configdone;
	} else if (n == -1) {
		if (debug > 0)
			perror("read()");
		goto configdone;
	} else
		cbu += n;
	return;

configdone:
	cbu = 0;
	close(conffd);
	conffd = -1;
}

int
read_configline(FILE *config)
{
	char *buf;
	size_t len;

	if ((buf = fgetln(config, &len))) {
		if (buf[len - 1] == '\n')
			buf[len - 1] = '\0';
		else
			return (-1);	/* all valid lines end in \n */
		parse_configline(buf);
	} else {
		syslog_r(LOG_DEBUG, &sdata, "read_configline: fgetln (%m)");
		return (-1);
	}
	return (0);
}

int
append_error_string(struct con *cp, size_t off, char *fmt, int af, void *ia)
{
	char sav = '\0';
	static int lastcont = 0;
	char *c = cp->obuf + off;
	char *s = fmt;
	size_t len = cp->osize - off;
	int i = 0;

	if (off == 0)
		lastcont = 0;

	if (lastcont != 0)
		cp->obuf[lastcont] = '-';
	snprintf(c, len, "%s ", nreply);
	i += strlen(c);
	lastcont = off + i - 1;
	if (*s == '"')
		s++;
	while (*s) {
		/*
		 * Make sure we at minimum, have room to add a
		 * format code (4 bytes), and a v6 address(39 bytes)
		 * and a byte saved in sav.
		 */
		if (i >= len - 46) {
			c = grow_obuf(cp, off);
			if (c == NULL)
				return (-1);
			len = cp->osize - (off + i);
		}

		if (c[i-1] == '\n') {
			if (lastcont != 0)
				cp->obuf[lastcont] = '-';
			snprintf(c + i, len, "%s ", nreply);
			i += strlen(c);
			lastcont = off + i - 1;
		}

		switch (*s) {
		case '\\':
		case '%':
			if (!sav)
				sav = *s;
			else {
				c[i++] = sav;
				sav = '\0';
				c[i] = '\0';
			}
			break;
		case '"':
		case 'A':
		case 'n':
			if (*(s+1) == '\0') {
				break;
			}
			if (sav == '\\' && *s == 'n') {
				c[i++] = '\n';
				sav = '\0';
				c[i] = '\0';
				break;
			} else if (sav == '\\' && *s == '"') {
				c[i++] = '"';
				sav = '\0';
				c[i] = '\0';
				break;
			} else if (sav == '%' && *s == 'A') {
				inet_ntop(af, ia, c + i, (len - i));
				i += strlen(c + i);
				sav = '\0';
				break;
			}
			/* FALLTHROUGH */
		default:
			if (sav)
				c[i++] = sav;
			c[i++] = *s;
			sav = '\0';
			c[i] = '\0';
			break;
		}
		s++;
	}
	return (i);
}

char *
loglists(struct con *cp)
{
	static char matchlists[80];
	struct sdlist **matches;
	int s = sizeof(matchlists) - 4;

	matchlists[0] = '\0';
	matches = cp->blacklists;
	if (matches == NULL)
		return (NULL);
	for (; *matches; matches++) {

		/* don't report an insane amount of lists in the logs.
		 * just truncate and indicate with ...
		 */
		if (strlen(matchlists) + strlen(matches[0]->tag) + 1 >= s)
			strlcat(matchlists, " ...", sizeof(matchlists));
		else {
			strlcat(matchlists, " ", s);
			strlcat(matchlists, matches[0]->tag, s);
		}
	}
	return matchlists;
}

void
build_reply(struct con *cp)
{
	struct sdlist **matches;
	int off = 0;

	matches = cp->blacklists;
	if (matches == NULL)
		goto nomatch;
	for (; *matches; matches++) {
		int used = 0;
		char *c = cp->obuf + off;
		int left = cp->osize - off;

		used = append_error_string(cp, off, matches[0]->string,
		    cp->af, cp->ia);
		if (used == -1)
			goto bad;
		off += used;
		left -= used;
		if (cp->obuf[off - 1] != '\n') {
			if (left < 1) {
				c = grow_obuf(cp, off);
				if (c == NULL)
					goto bad;
			}
			cp->obuf[off++] = '\n';
			cp->obuf[off] = '\0';
		}
	}
	return;
nomatch:
	/* No match. give generic reply */
	free(cp->obuf);
	cp->obuf = NULL;
	cp->osize = 0;
	if (cp->blacklists != NULL)
		asprintf(&cp->obuf,
		    "%s-Sorry %s\n"
		    "%s-You are trying to send mail from an address "
		    "listed by one\n"
		    "%s or more IP-based registries as being a SPAM source.\n",
		    nreply, cp->addr, nreply, nreply);
	else
		asprintf(&cp->obuf,
		    "451 Temporary failure, please try again later.\r\n");
	if (cp->obuf != NULL)
		cp->osize = strlen(cp->obuf) + 1;
	else
		cp->osize = 0;
	return;
bad:
	if (cp->obuf != NULL) {
		free(cp->obuf);
		cp->obuf = NULL;
		cp->osize = 0;
	}
}

void
doreply(struct con *cp)
{
	build_reply(cp);
}

void
setlog(char *p, size_t len, char *f)
{
	char *s;

	s = strsep(&f, ":");
	if (!f)
		return;
	while (*f == ' ' || *f == '\t')
		f++;
	s = strsep(&f, " \t");
	if (s == NULL)
		return;
	strlcpy(p, s, len);
	s = strsep(&p, " \t\n\r");
	if (s == NULL)
		return;
	s = strsep(&p, " \t\n\r");
	if (s)
		*s = '\0';
}

/* 
 * Get address client connected to, by doing a DIOCNATLOOK call.
 * Uses server_lookup code from ftp-proxy.
 */
void
getcaddr(struct con *cp) {
	struct sockaddr_storage spamd_end;
	struct sockaddr *sep = (struct sockaddr *) &spamd_end;
	struct sockaddr_storage original_destination;
	struct sockaddr *odp = (struct sockaddr *) &original_destination;
	socklen_t len = sizeof(struct sockaddr_storage);
	int error;

	cp->caddr[0] = '\0';
	if (getsockname(cp->fd, sep, &len) == -1)
		return;
	if (server_lookup((struct sockaddr *)&cp->ss, sep, odp) != 0)
		return;
	error = getnameinfo(odp, odp->sa_len, cp->caddr, sizeof(cp->caddr),
	    NULL, 0, NI_NUMERICHOST);
	if (error) 
		cp->caddr[0] = '\0';
}
	

void
gethelo(char *p, size_t len, char *f)
{
	char *s;

	/* skip HELO/EHLO */
	f+=4;
	/* skip whitespace */
	while (*f == ' ' || *f == '\t')
		f++;
	s = strsep(&f, " \t");
	if (s == NULL)
		return;
	strlcpy(p, s, len);
	s = strsep(&p, " \t\n\r");
	if (s == NULL)
		return;
	s = strsep(&p, " \t\n\r");
	if (s)
		*s = '\0';
}

void
initcon(struct con *cp, int fd, struct sockaddr *sa)
{
	time_t tt;
	char *tmp;
	int error;

	time(&tt);
	free(cp->obuf);
	cp->obuf = NULL;
	cp->osize = 0;
	free(cp->blacklists);
	cp->blacklists = NULL;
	free(cp->lists);
	cp->lists = NULL;
	bzero(cp, sizeof(struct con));
	if (grow_obuf(cp, 0) == NULL)
		err(1, "malloc");
	cp->fd = fd;
	if (sa->sa_len > sizeof(cp->ss))
		errx(1, "sockaddr size");
	if (sa->sa_family != AF_INET)
		errx(1, "not supported yet");
	memcpy(&cp->ss, sa, sa->sa_len);
	cp->af = sa->sa_family;
	cp->ia = &((struct sockaddr_in *)&cp->ss)->sin_addr;
	cp->blacklists = sdl_lookup(blacklists, cp->af, cp->ia);
	cp->stutter = (greylist && !grey_stutter && cp->blacklists == NULL) ?
	    0 : stutter;
	error = getnameinfo(sa, sa->sa_len, cp->addr, sizeof(cp->addr), NULL, 0,
	    NI_NUMERICHOST);
	if (error)
		errx(1, "%s", gai_strerror(error));
	tmp = strdup(ctime(&t));
	if (tmp == NULL)
		err(1, "malloc");
	tmp[strlen(tmp) - 1] = '\0'; /* nuke newline */
	snprintf(cp->obuf, cp->osize, "220 %s ESMTP %s; %s\r\n",
	    hostname, spamd, tmp);
	free(tmp);
	cp->op = cp->obuf;
	cp->ol = strlen(cp->op);
	cp->w = tt + cp->stutter;
	cp->s = tt;
	strlcpy(cp->rend, "\n", sizeof cp->rend);
	clients++;
	if (cp->blacklists != NULL) {
		blackcount++;
		if (greylist && blackcount > maxblack)
			cp->stutter = 0;
		cp->lists = strdup(loglists(cp));
	}
	else
		cp->lists = NULL;
}

void
closecon(struct con *cp)
{
	time_t tt;

	time(&tt);
	syslog_r(LOG_INFO, &sdata, "%s: disconnected after %ld seconds.%s%s",
	    cp->addr, (long)(tt - cp->s),
	    ((cp->lists == NULL) ? "" : " lists:"),
	    ((cp->lists == NULL) ? "": cp->lists));
	if (debug > 0)
		printf("%s connected for %ld seconds.\n", cp->addr,
		    (long)(tt - cp->s));
	if (cp->lists != NULL) {
		free(cp->lists);
		cp->lists = NULL;
	}
	if (cp->blacklists != NULL) {
		blackcount--;
		free(cp->blacklists);
		cp->blacklists = NULL;
	}
	if (cp->obuf != NULL) {
		free(cp->obuf);
		cp->obuf = NULL;
		cp->osize = 0;
	}
	close(cp->fd);
	clients--;
	cp->fd = -1;
}

int
match(const char *s1, const char *s2)
{
	return (strncasecmp(s1, s2, strlen(s2)) == 0);
}

void
nextstate(struct con *cp)
{
	if (match(cp->ibuf, "QUIT") && cp->state < 99) {
		snprintf(cp->obuf, cp->osize, "221 %s\r\n", hostname);
		cp->op = cp->obuf;
		cp->ol = strlen(cp->op);
		cp->w = t + cp->stutter;
		cp->laststate = cp->state;
		cp->state = 99;
		return;
	}

	if (match(cp->ibuf, "RSET") && cp->state > 2 && cp->state < 50) {
		snprintf(cp->obuf, cp->osize,
		    "250 Ok to start over.\r\n");
		cp->op = cp->obuf;
		cp->ol = strlen(cp->op);
		cp->w = t + cp->stutter;
		cp->laststate = cp->state;
		cp->state = 2;
		return;
	}
	switch (cp->state) {
	case 0:
		/* banner sent; wait for input */
		cp->ip = cp->ibuf;
		cp->il = sizeof(cp->ibuf) - 1;
		cp->laststate = cp->state;
		cp->state = 1;
		cp->r = t;
		break;
	case 1:
		/* received input: parse, and select next state */
		if (match(cp->ibuf, "HELO") ||
		    match(cp->ibuf, "EHLO")) {
			int nextstate = 2;
			cp->helo[0] = '\0'; 
			gethelo(cp->helo, sizeof cp->helo, cp->ibuf);
			if (cp->helo[0] == '\0') {
				nextstate = 0;
				snprintf(cp->obuf, cp->osize,
				    "501 helo requires domain name.\r\n");
			} else {
				snprintf(cp->obuf, cp->osize,
				    "250 Hello, spam sender. "
				    "Pleased to be wasting your time.\r\n");
			}
			cp->op = cp->obuf;
			cp->ol = strlen(cp->op);
			cp->laststate = cp->state;
			cp->state = nextstate;
			cp->w = t + cp->stutter;
			break;
		}
		goto mail;
	case 2:
		/* sent 250 Hello, wait for input */
		cp->ip = cp->ibuf;
		cp->il = sizeof(cp->ibuf) - 1;
		cp->laststate = cp->state;
		cp->state = 3;
		cp->r = t;
		break;
	case 3:
	mail:
		if (match(cp->ibuf, "MAIL")) {
			setlog(cp->mail, sizeof cp->mail, cp->ibuf);
			snprintf(cp->obuf, cp->osize,
			    "250 You are about to try to deliver spam. "
			    "Your time will be spent, for nothing.\r\n");
			cp->op = cp->obuf;
			cp->ol = strlen(cp->op);
			cp->laststate = cp->state;
			cp->state = 4;
			cp->w = t + cp->stutter;
			break;
		}
		goto rcpt;
	case 4:
		/* sent 250 Sender ok */
		cp->ip = cp->ibuf;
		cp->il = sizeof(cp->ibuf) - 1;
		cp->laststate = cp->state;
		cp->state = 5;
		cp->r = t;
		break;
	case 5:
	rcpt:
		if (match(cp->ibuf, "RCPT")) {
			setlog(cp->rcpt, sizeof(cp->rcpt), cp->ibuf);
			snprintf(cp->obuf, cp->osize,
			    "250 This is hurting you more than it is "
			    "hurting me.\r\n");
			cp->op = cp->obuf;
			cp->ol = strlen(cp->op);
			cp->laststate = cp->state;
			cp->state = 6;
			cp->w = t + cp->stutter;
			if (cp->mail[0] && cp->rcpt[0]) {
				if (verbose)
					syslog_r(LOG_INFO, &sdata,
					    "(%s) %s: %s -> %s",
					    cp->blacklists ? "BLACK" : "GREY",
					    cp->addr, cp->mail,
					    cp->rcpt);
				if (debug)
					fprintf(stderr, "(%s) %s: %s -> %s\n",
					    cp->blacklists ? "BLACK" : "GREY",
					    cp->addr, cp->mail, cp->rcpt);
				if (greylist && cp->blacklists == NULL) {
					/* send this info to the greylister */
					getcaddr(cp);
					fprintf(grey,
					    "CO:%s\nHE:%s\nIP:%s\nFR:%s\nTO:%s\n",
					    cp->caddr, cp->helo, cp->addr,
					    cp->mail, cp->rcpt);
					fflush(grey);
				}
			}
			break;
		}
		goto spam;
	case 6:
		/* sent 250 blah */
		cp->ip = cp->ibuf;
		cp->il = sizeof(cp->ibuf) - 1;
		cp->laststate = cp->state;
		cp->state = 5;
		cp->r = t;
		break;

	case 50:
	spam:
		if (match(cp->ibuf, "DATA")) {
			snprintf(cp->obuf, cp->osize,
			    "354 Enter spam, end with \".\" on a line by "
			    "itself\r\n");
			cp->state = 60;
			if (window && setsockopt(cp->fd, SOL_SOCKET, SO_RCVBUF,
			    &window, sizeof(window)) == -1) {
				syslog_r(LOG_DEBUG, &sdata,"setsockopt: %m");
				/* don't fail if this doesn't work. */
			}
			cp->ip = cp->ibuf;
			cp->il = sizeof(cp->ibuf) - 1;
			cp->op = cp->obuf;
			cp->ol = strlen(cp->op);
			cp->w = t + cp->stutter;
			if (greylist && cp->blacklists == NULL) {
				cp->laststate = cp->state;
				cp->state = 98;
				goto done;
			}
		} else {
			if (match(cp->ibuf, "NOOP")) 
				snprintf(cp->obuf, cp->osize,
				    "250 2.0.0 OK I did nothing\r\n");
			else
                        	snprintf(cp->obuf, cp->osize,
				    "500 5.5.1 Command unrecognized\r\n");
			cp->state = cp->laststate;
			cp->ip = cp->ibuf;
			cp->il = sizeof(cp->ibuf) - 1;
			cp->op = cp->obuf;
			cp->ol = strlen(cp->op);
			cp->w = t + cp->stutter;
		}
		break;
	case 60:
		/* sent 354 blah */
		cp->ip = cp->ibuf;
		cp->il = sizeof(cp->ibuf) - 1;
		cp->laststate = cp->state;
		cp->state = 70;
		cp->r = t;
		break;
	case 70: {
		char *p, *q;

		for (p = q = cp->ibuf; q <= cp->ip; ++q)
			if (*q == '\n' || q == cp->ip) {
				*q = 0;
				if (q > p && q[-1] == '\r')
					q[-1] = 0;
				if (!strcmp(p, ".") ||
				    (cp->data_body && ++cp->data_lines >= 10)) {
					cp->laststate = cp->state;
					cp->state = 98;
					goto done;
				}
				if (!cp->data_body && !*p)
					cp->data_body = 1;
				if (verbose && cp->data_body && *p)
					syslog_r(LOG_DEBUG, &sdata, "%s: "
					    "Body: %s", cp->addr, p);
				else if (verbose && (match(p, "FROM:") ||
				    match(p, "TO:") || match(p, "SUBJECT:")))
					syslog_r(LOG_INFO, &sdata, "%s: %s",
					    cp->addr, p);
				p = ++q;
			}
		cp->ip = cp->ibuf;
		cp->il = sizeof(cp->ibuf) - 1;
		cp->r = t;
		break;
	}
	case 98:
	done:
		doreply(cp);
		cp->op = cp->obuf;
		cp->ol = strlen(cp->op);
		cp->w = t + cp->stutter;
		cp->laststate = cp->state;
		cp->state = 99;
		break;
	case 99:
		closecon(cp);
		break;
	default:
		errx(1, "illegal state %d", cp->state);
		break;
	}
}

void
handler(struct con *cp)
{
	int end = 0;
	int n;

	if (cp->r) {
		n = read(cp->fd, cp->ip, cp->il);
		if (n == 0)
			closecon(cp);
		else if (n == -1) {
			if (debug > 0)
				perror("read()");
			closecon(cp);
		} else {
			cp->ip[n] = '\0';
			if (cp->rend[0])
				if (strpbrk(cp->ip, cp->rend))
					end = 1;
			cp->ip += n;
			cp->il -= n;
		}
	}
	if (end || cp->il == 0) {
		while (cp->ip > cp->ibuf &&
		    (cp->ip[-1] == '\r' || cp->ip[-1] == '\n'))
			cp->ip--;
		*cp->ip = '\0';
		cp->r = 0;
		nextstate(cp);
	}
}

void
handlew(struct con *cp, int one)
{
	int n;

	/* kill stutter on greylisted connections after initial delay */
	if (cp->stutter && greylist && cp->blacklists == NULL &&
	    (t - cp->s) > grey_stutter)
		cp->stutter=0;

	if (cp->w) {
		if (*cp->op == '\n' && !cp->sr) {
			/* insert \r before \n */
			n = write(cp->fd, "\r", 1);
			if (n == 0) {
				closecon(cp);
				goto handled;
			} else if (n == -1) {
				if (debug > 0 && errno != EPIPE)
					perror("write()");
				closecon(cp);
				goto handled;
			}
		}
		if (*cp->op == '\r')
			cp->sr = 1;
		else
			cp->sr = 0;
		n = write(cp->fd, cp->op, (one && cp->stutter) ? 1 : cp->ol);
		if (n == 0)
			closecon(cp);
		else if (n == -1) {
			if (debug > 0 && errno != EPIPE)
				perror("write()");
			closecon(cp);
		} else {
			cp->op += n;
			cp->ol -= n;
		}
	}
handled:
	cp->w = t + cp->stutter;
	if (cp->ol == 0) {
		cp->w = 0;
		nextstate(cp);
	}
}

static int
get_maxfiles(void)
{
	int mib[2], maxfiles;
	size_t len;

	mib[0] = CTL_KERN;
	mib[1] = KERN_MAXFILES;
	len = sizeof(maxfiles);
	if (sysctl(mib, 2, &maxfiles, &len, NULL, 0) == -1)
		return(MAXCON);
	if ((maxfiles - 200) < 10)
		errx(1, "kern.maxfiles is only %d, can not continue\n",
		    maxfiles);
	else
		return(maxfiles - 200);
}

int
main(int argc, char *argv[])
{
	fd_set *fdsr = NULL, *fdsw = NULL;
	struct sockaddr_in sin;
	struct sockaddr_in lin;
	int ch, s, s2, conflisten = 0, syncfd = 0, i, omax = 0, one = 1;
	socklen_t sinlen;
	u_short port;
	struct servent *ent;
	struct rlimit rlp;
	char *bind_address = NULL;
	const char *errstr;
	char *sync_iface = NULL;
	char *sync_baddr = NULL;

	tzset();
	openlog_r("spamd", LOG_PID | LOG_NDELAY, LOG_DAEMON, &sdata);

	if ((ent = getservbyname("spamd", "tcp")) == NULL)
		errx(1, "Can't find service \"spamd\" in /etc/services");
	port = ntohs(ent->s_port);
	if ((ent = getservbyname("spamd-cfg", "tcp")) == NULL)
		errx(1, "Can't find service \"spamd-cfg\" in /etc/services");
	cfg_port = ntohs(ent->s_port);
	if ((ent = getservbyname("spamd-sync", "udp")) == NULL)
		errx(1, "Can't find service \"spamd-sync\" in /etc/services");
	sync_port = ntohs(ent->s_port);

	if (gethostname(hostname, sizeof hostname) == -1)
		err(1, "gethostname");
	maxfiles = get_maxfiles();
	if (maxcon > maxfiles)
		maxcon = maxfiles;
	if (maxblack > maxfiles)
		maxblack = maxfiles;
	while ((ch =
	    getopt(argc, argv, "45l:c:B:p:bdG:h:r:s:S:M:n:vw:y:Y:")) != -1) {
		switch (ch) {
		case '4':
			nreply = "450";
			break;
		case '5':
			nreply = "550";
			break;
		case 'l':
			bind_address = optarg;
			break;
		case 'B':
			i = atoi(optarg);
			maxblack = i;
			break;
		case 'c':
			i = atoi(optarg);
			if (i > maxfiles) {
				fprintf(stderr,
				    "%d > system max of %d connections\n",
				    i, maxfiles);
				usage();
			}
			maxcon = i;
			break;
		case 'p':
			i = atoi(optarg);
			port = i;
			break;
		case 'd':
			debug = 1;
			break;
		case 'b':
			greylist = 0;
			break;
		case 'G':
			if (sscanf(optarg, "%d:%d:%d", &passtime, &greyexp,
			    &whiteexp) != 3)
				usage();
			/* convert to seconds from minutes */
			passtime *= 60;
			/* convert to seconds from hours */
			whiteexp *= (60 * 60);
			/* convert to seconds from hours */
			greyexp *= (60 * 60);
			break;
		case 'h':
			bzero(&hostname, sizeof(hostname));
			if (strlcpy(hostname, optarg, sizeof(hostname)) >=
			    sizeof(hostname))
				errx(1, "-h arg too long");
			break;
		case 's':
			i = atoi(optarg);
			if (i < 0 || i > 10)
				usage();
			stutter = i;
			break;
		case 'S':
			i = strtonum(optarg, 0, 90, &errstr);
			if (errstr)
				usage();
			grey_stutter = i;
			break;
		case 'M':
			low_prio_mx_ip = optarg;
			break;
		case 'n':
			spamd = optarg;
			break;
		case 'v':
			verbose = 1;
			break;
		case 'w':
			window = atoi(optarg);
			if (window <= 0)
				usage();
			break;
		case 'Y':
			if (sync_addhost(optarg, sync_port) != 0)
				sync_iface = optarg;
			syncsend++;
			break;
		case 'y':
			sync_baddr = optarg;
			syncrecv++;
			break;
		default:
			usage();
			break;
		}
	}

	setproctitle("[priv]%s%s",
	    greylist ? " (greylist)" : "",
	    (syncrecv || syncsend) ? " (sync)" : "");

	if (!greylist)
		maxblack = maxcon;
	else if (maxblack > maxcon)
		usage();

	rlp.rlim_cur = rlp.rlim_max = maxcon + 15;
	if (setrlimit(RLIMIT_NOFILE, &rlp) == -1)
		err(1, "setrlimit");

	con = calloc(maxcon, sizeof(*con));
	if (con == NULL)
		err(1, "calloc");

	con->obuf = malloc(8192);

	if (con->obuf == NULL)
		err(1, "malloc");
	con->osize = 8192;

	for (i = 0; i < maxcon; i++)
		con[i].fd = -1;

	signal(SIGPIPE, SIG_IGN);

	s = socket(AF_INET, SOCK_STREAM, 0);
	if (s == -1)
		err(1, "socket");

	if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one,
	    sizeof(one)) == -1)
		return (-1);

	conflisten = socket(AF_INET, SOCK_STREAM, 0);
	if (conflisten == -1)
		err(1, "socket");

	if (setsockopt(conflisten, SOL_SOCKET, SO_REUSEADDR, &one,
	    sizeof(one)) == -1)
		return (-1);

	memset(&sin, 0, sizeof sin);
	sin.sin_len = sizeof(sin);
	if (bind_address) {
		if (inet_pton(AF_INET, bind_address, &sin.sin_addr) != 1)
			err(1, "inet_pton");
	} else
		sin.sin_addr.s_addr = htonl(INADDR_ANY);
	sin.sin_family = AF_INET;
	sin.sin_port = htons(port);

	if (bind(s, (struct sockaddr *)&sin, sizeof sin) == -1)
		err(1, "bind");

	memset(&lin, 0, sizeof sin);
	lin.sin_len = sizeof(sin);
	lin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
	lin.sin_family = AF_INET;
	lin.sin_port = htons(cfg_port);

	if (bind(conflisten, (struct sockaddr *)&lin, sizeof lin) == -1)
		err(1, "bind local");

	if (syncsend || syncrecv) {
		syncfd = sync_init(sync_iface, sync_baddr, sync_port);
		if (syncfd == -1)
			err(1, "sync init");
	}

	pw = getpwnam("_spamd");
	if (!pw)
		pw = getpwnam("nobody");

	if (debug == 0) {
		if (daemon(1, 1) == -1)
			err(1, "daemon");
	}

	if (greylist) {
		pfdev = open("/dev/pf", O_RDWR);
		if (pfdev == -1) {
			syslog_r(LOG_ERR, &sdata, "open /dev/pf: %m");
			exit(1);
		}

		maxblack = (maxblack >= maxcon) ? maxcon - 100 : maxblack;
		if (maxblack < 0)
			maxblack = 0;

		/* open pipe to talk to greylister */
		if (pipe(greypipe) == -1) {
			syslog(LOG_ERR, "pipe (%m)");
			exit(1);
		}
		/* open pipe to recieve spamtrap configs */
		if (pipe(trappipe) == -1) {
			syslog(LOG_ERR, "pipe (%m)");
			exit(1);
		}
		jail_pid = fork();
		switch (jail_pid) {
		case -1:
			syslog(LOG_ERR, "fork (%m)");
			exit(1);
		case 0:
			/* child - continue */
			signal(SIGPIPE, SIG_IGN);
			grey = fdopen(greypipe[1], "w");
			if (grey == NULL) {
				syslog(LOG_ERR, "fdopen (%m)");
				_exit(1);
			}
			close(greypipe[0]);
			trapfd = trappipe[0];
			trapcfg = fdopen(trappipe[0], "r");
			if (trapcfg == NULL) {
				syslog(LOG_ERR, "fdopen (%m)");
				_exit(1);
			}
			close(trappipe[1]);
			goto jail;
		}
		/* parent - run greylister */
		grey = fdopen(greypipe[0], "r");
		if (grey == NULL) {
			syslog(LOG_ERR, "fdopen (%m)");
			exit(1);
		}
		close(greypipe[1]);
		trapcfg = fdopen(trappipe[1], "w");
		if (trapcfg == NULL) {
			syslog(LOG_ERR, "fdopen (%m)");
			exit(1);
		}
		close(trappipe[0]);
		return (greywatcher());
		/* NOTREACHED */
	}

jail:
	if (chroot("/var/empty") == -1 || chdir("/") == -1) {
		syslog(LOG_ERR, "cannot chdir to /var/empty.");
		exit(1);
	}

	if (pw)
		if (setgroups(1, &pw->pw_gid) ||
		    setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) ||
		    setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid))
			err(1, "failed to drop privs");

	if (listen(s, 10) == -1)
		err(1, "listen");

	if (listen(conflisten, 10) == -1)
		err(1, "listen");

	if (debug != 0)
		printf("listening for incoming connections.\n");
	syslog_r(LOG_WARNING, &sdata, "listening for incoming connections.");

	while (1) {
		struct timeval tv, *tvp;
		int max, n;
		int writers;

		max = MAX(s, conflisten);
		if (syncrecv)
			max = MAX(max, syncfd);
		max = MAX(max, conffd);
		max = MAX(max, trapfd);

		time(&t);
		for (i = 0; i < maxcon; i++)
			if (con[i].fd != -1)
				max = MAX(max, con[i].fd);

		if (max > omax) {
			free(fdsr);
			fdsr = NULL;
			free(fdsw);
			fdsw = NULL;
			fdsr = (fd_set *)calloc(howmany(max+1, NFDBITS),
			    sizeof(fd_mask));
			if (fdsr == NULL)
				err(1, "calloc");
			fdsw = (fd_set *)calloc(howmany(max+1, NFDBITS),
			    sizeof(fd_mask));
			if (fdsw == NULL)
				err(1, "calloc");
			omax = max;
		} else {
			memset(fdsr, 0, howmany(max+1, NFDBITS) *
			    sizeof(fd_mask));
			memset(fdsw, 0, howmany(max+1, NFDBITS) *
			    sizeof(fd_mask));
		}

		writers = 0;
		for (i = 0; i < maxcon; i++) {
			if (con[i].fd != -1 && con[i].r) {
				if (con[i].r + MAXTIME <= t) {
					closecon(&con[i]);
					continue;
				}
				FD_SET(con[i].fd, fdsr);
			}
			if (con[i].fd != -1 && con[i].w) {
				if (con[i].w + MAXTIME <= t) {
					closecon(&con[i]);
					continue;
				}
				if (con[i].w <= t)
					FD_SET(con[i].fd, fdsw);
				writers = 1;
			}
		}
		FD_SET(s, fdsr);

		/* only one active config conn at a time */
		if (conffd == -1)
			FD_SET(conflisten, fdsr);
		else
			FD_SET(conffd, fdsr);
		if (trapfd != -1)
			FD_SET(trapfd, fdsr);
		if (syncrecv)
			FD_SET(syncfd, fdsr);

		if (writers == 0) {
			tvp = NULL;
		} else {
			tv.tv_sec = 1;
			tv.tv_usec = 0;
			tvp = &tv;
		}

		n = select(max+1, fdsr, fdsw, NULL, tvp);
		if (n == -1) {
			if (errno != EINTR)
				err(1, "select");
			continue;
		}
		if (n == 0)
			continue;

		for (i = 0; i < maxcon; i++) {
			if (con[i].fd != -1 && FD_ISSET(con[i].fd, fdsr))
				handler(&con[i]);
			if (con[i].fd != -1 && FD_ISSET(con[i].fd, fdsw))
				handlew(&con[i], clients + 5 < maxcon);
		}
		if (FD_ISSET(s, fdsr)) {
			sinlen = sizeof(sin);
			s2 = accept(s, (struct sockaddr *)&sin, &sinlen);
			if (s2 == -1)
				/* accept failed, they may try again */
				continue;
			for (i = 0; i < maxcon; i++)
				if (con[i].fd == -1)
					break;
			if (i == maxcon)
				close(s2);
			else {
				initcon(&con[i], s2, (struct sockaddr *)&sin);
				syslog_r(LOG_INFO, &sdata,
				    "%s: connected (%d/%d)%s%s",
				    con[i].addr, clients, blackcount,
				    ((con[i].lists == NULL) ? "" :
				    ", lists:"),
				    ((con[i].lists == NULL) ? "":
				    con[i].lists));
			}
		}
		if (FD_ISSET(conflisten, fdsr)) {
			sinlen = sizeof(lin);
			conffd = accept(conflisten, (struct sockaddr *)&lin,
			    &sinlen);
			if (conffd == -1)
				/* accept failed, they may try again */
				continue;
			else if (ntohs(lin.sin_port) >= IPPORT_RESERVED) {
				close(conffd);
				conffd = -1;
			}
		}
		if (conffd != -1 && FD_ISSET(conffd, fdsr))
			do_config();
		if (trapfd != -1 && FD_ISSET(trapfd, fdsr))
			read_configline(trapcfg);
		if (syncrecv && FD_ISSET(syncfd, fdsr))
			sync_recv();
	}
	exit(1);
}