/*	$OpenBSD: smtpd.c,v 1.325 2019/09/03 04:48:20 martijn Exp $	*/

/*
 * Copyright (c) 2008 Gilles Chehade <gilles@poolp.org>
 * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org>
 * Copyright (c) 2009 Jacek Masiulaniec <jacekm@dobremiasto.net>
 *
 * 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 "includes.h"

#include <sys/file.h> /* Needed for flock */
#include <sys/types.h>
#include <sys/queue.h>
#include <sys/tree.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include <sys/mman.h>

#ifdef BSD_AUTH
#include <bsd_auth.h>
#endif

#ifdef USE_PAM
#if defined(HAVE_SECURITY_PAM_APPL_H)
#include <security/pam_appl.h>
#elif defined (HAVE_PAM_PAM_APPL_H)
#include <pam/pam_appl.h>
#endif
#endif

#ifdef HAVE_CRYPT_H
#include <crypt.h> /* needed for crypt() */
#endif
#include <dirent.h>
#include <err.h>
#include <errno.h>
#include <event.h>
#include <fcntl.h>
#include <grp.h> /* needed for setgroups */
#include <fts.h>
#include <grp.h>
#include <imsg.h>
#include <inttypes.h>
#include <libgen.h>
#ifdef HAVE_LOGIN_CAP_H
#include <login_cap.h>
#endif
#ifdef HAVE_PATHS_H
#include <paths.h>
#endif
#include <poll.h>
#include <pwd.h>
#include <signal.h>
#ifdef HAVE_SHADOW_H
#include <shadow.h> /* needed for getspnam() */
#endif
#include <stdio.h>
#include <syslog.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <sysexits.h>
#include <time.h>
#include <unistd.h>
#ifdef HAVE_UTIL_H
#include <util.h>
#endif

#include <openssl/ssl.h>
#include <openssl/evp.h>

#include "smtpd.h"
#include "log.h"
#include "ssl.h"

extern char *__progname;

#define SMTPD_MAXARG 32

static void parent_imsg(struct mproc *, struct imsg *);
static void usage(void);
static int smtpd(void);
static void parent_shutdown(void);
static void parent_send_config(int, short, void *);
static void parent_send_config_lka(void);
static void parent_send_config_pony(void);
static void parent_send_config_ca(void);
static void parent_sig_handler(int, short, void *);
static void forkmda(struct mproc *, uint64_t, struct deliver *);
static int parent_forward_open(char *, char *, uid_t, gid_t);
static struct child *child_add(pid_t, int, const char *);
static struct mproc *start_child(int, char **, char *);
static struct mproc *setup_peer(enum smtp_proc_type, pid_t, int);
static void setup_peers(struct mproc *, struct mproc *);
static void setup_done(struct mproc *);
static void setup_proc(void);
static struct mproc *setup_peer(enum smtp_proc_type, pid_t, int);
static int imsg_wait(struct imsgbuf *, struct imsg *, int);

static void	offline_scan(int, short, void *);
static int	offline_add(char *, uid_t, gid_t);
static void	offline_done(void);
static int	offline_enqueue(char *, uid_t, gid_t);

static void	purge_task(void);
static int	parent_auth_user(const char *, const char *);
static void	load_pki_tree(void);
static void	load_pki_keys(void);

static void	fork_processors(void);
static void	fork_processor(const char *, const char *, const char *, const char *, const char *);

enum child_type {
	CHILD_DAEMON,
	CHILD_MDA,
	CHILD_PROCESSOR,
	CHILD_ENQUEUE_OFFLINE,
};

struct child {
	pid_t			 pid;
	enum child_type		 type;
	const char		*title;
	int			 mda_out;
	uint64_t		 mda_id;
	char			*path;
	char			*cause;
};

struct offline {
	TAILQ_ENTRY(offline)	 entry;
	uid_t			 uid;
	gid_t			 gid;
	char			*path;
};

#define OFFLINE_READMAX		20
#define OFFLINE_QUEUEMAX	5
static size_t			offline_running = 0;
TAILQ_HEAD(, offline)		offline_q;

static struct event		config_ev;
static struct event		offline_ev;
static struct timeval		offline_timeout;

static pid_t			purge_pid = -1;

extern char	**environ;
void		(*imsg_callback)(struct mproc *, struct imsg *);

enum smtp_proc_type	smtpd_process;

struct smtpd	*env = NULL;

struct mproc	*p_control = NULL;
struct mproc	*p_lka = NULL;
struct mproc	*p_parent = NULL;
struct mproc	*p_queue = NULL;
struct mproc	*p_scheduler = NULL;
struct mproc	*p_pony = NULL;
struct mproc	*p_ca = NULL;

const char	*backend_queue = "fs";
const char	*backend_scheduler = "ramqueue";
const char	*backend_stat = "ram";

int	profiling = 0;
int	debug = 0;
int	foreground = 0;
int	control_socket = -1;

struct tree	 children;

/* Saved arguments to main(). */
char **saved_argv;
int saved_argc;

static void
parent_imsg(struct mproc *p, struct imsg *imsg)
{
	struct forward_req	*fwreq;
	struct processor	*processor;
	struct deliver		 deliver;
	struct child		*c;
	struct msg		 m;
	const void		*data;
	const char		*username, *password, *cause, *procname;
	uint64_t		 reqid;
	size_t			 sz;
	void			*i;
	int			 fd, n, v, ret;

	if (imsg == NULL)
		fatalx("process %s socket closed", p->name);

	switch (imsg->hdr.type) {
	case IMSG_LKA_OPEN_FORWARD:
		CHECK_IMSG_DATA_SIZE(imsg, sizeof *fwreq);
		fwreq = imsg->data;
		fd = parent_forward_open(fwreq->user, fwreq->directory,
		    fwreq->uid, fwreq->gid);
		fwreq->status = 0;
		if (fd == -1 && errno != ENOENT) {
			if (errno == EAGAIN)
				fwreq->status = -1;
		}
		else
			fwreq->status = 1;
		m_compose(p, IMSG_LKA_OPEN_FORWARD, 0, 0, fd,
		    fwreq, sizeof *fwreq);
		return;

	case IMSG_LKA_AUTHENTICATE:
		/*
		 * If we reached here, it means we want root to lookup
		 * system user.
		 */
		m_msg(&m, imsg);
		m_get_id(&m, &reqid);
		m_get_string(&m, &username);
		m_get_string(&m, &password);
		m_end(&m);

		ret = parent_auth_user(username, password);

		m_create(p, IMSG_LKA_AUTHENTICATE, 0, 0, -1);
		m_add_id(p, reqid);
		m_add_int(p, ret);
		m_close(p);
		return;

	case IMSG_MDA_FORK:
		m_msg(&m, imsg);
		m_get_id(&m, &reqid);
		m_get_data(&m, &data, &sz);
		m_end(&m);
		if (sz != sizeof(deliver))
			fatalx("expected deliver");
		memmove(&deliver, data, sz);
		forkmda(p, reqid, &deliver);
		return;

	case IMSG_MDA_KILL:
		m_msg(&m, imsg);
		m_get_id(&m, &reqid);
		m_get_string(&m, &cause);
		m_end(&m);

		i = NULL;
		while ((n = tree_iter(&children, &i, NULL, (void**)&c)))
			if (c->type == CHILD_MDA &&
			    c->mda_id == reqid &&
			    c->cause == NULL)
				break;
		if (!n) {
			log_debug("debug: smtpd: "
			    "kill request: proc not found");
			return;
		}

		c->cause = xstrdup(cause);
		log_debug("debug: smtpd: kill requested for %u: %s",
		    c->pid, c->cause);
		kill(c->pid, SIGTERM);
		return;

	case IMSG_CTL_VERBOSE:
		m_msg(&m, imsg);
		m_get_int(&m, &v);
		m_end(&m);
		log_trace_verbose(v);
		return;

	case IMSG_CTL_PROFILE:
		m_msg(&m, imsg);
		m_get_int(&m, &v);
		m_end(&m);
		profiling = v;
		return;

	case IMSG_LKA_PROCESSOR_ERRFD:
		m_msg(&m, imsg);
		m_get_string(&m, &procname);
		m_end(&m);

		processor = dict_xget(env->sc_processors_dict, procname);
		m_create(p_lka, IMSG_LKA_PROCESSOR_ERRFD, 0, 0, processor->errfd);
		m_add_string(p_lka, procname);
		m_close(p_lka);
		return;
	}

	errx(1, "parent_imsg: unexpected %s imsg from %s",
	    imsg_to_str(imsg->hdr.type), proc_title(p->proc));
}

static void
usage(void)
{
	extern char	*__progname;

	fprintf(stderr, "usage: %s [-dFhnv] [-D macro=value] "
	    "[-f file] [-P system] [-T trace]\n", __progname);
	exit(1);
}

static void
parent_shutdown(void)
{
	pid_t pid;

	mproc_clear(p_ca);
	mproc_clear(p_pony);
	mproc_clear(p_control);
	mproc_clear(p_lka);
	mproc_clear(p_scheduler);
	mproc_clear(p_queue);

	do {
		pid = waitpid(WAIT_MYPGRP, NULL, 0);
	} while (pid != -1 || (pid == -1 && errno == EINTR));

	unlink(SMTPD_SOCKET);

	log_info("Exiting");
	exit(0);
}

static void
parent_send_config(int fd, short event, void *p)
{
	parent_send_config_lka();
	parent_send_config_pony();
	parent_send_config_ca();
	purge_config(PURGE_PKI);
}

static void
parent_send_config_pony(void)
{
	log_debug("debug: parent_send_config: configuring pony process");
	m_compose(p_pony, IMSG_CONF_START, 0, 0, -1, NULL, 0);
	m_compose(p_pony, IMSG_CONF_END, 0, 0, -1, NULL, 0);
}

void
parent_send_config_lka()
{
	log_debug("debug: parent_send_config_ruleset: reloading");
	m_compose(p_lka, IMSG_CONF_START, 0, 0, -1, NULL, 0);
	m_compose(p_lka, IMSG_CONF_END, 0, 0, -1, NULL, 0);
}

static void
parent_send_config_ca(void)
{
	log_debug("debug: parent_send_config: configuring ca process");
	m_compose(p_ca, IMSG_CONF_START, 0, 0, -1, NULL, 0);
	m_compose(p_ca, IMSG_CONF_END, 0, 0, -1, NULL, 0);
}

static void
parent_sig_handler(int sig, short event, void *p)
{
	struct child	*child;
	int		 status, fail;
	pid_t		 pid;
	char		*cause;

	switch (sig) {
	case SIGTERM:
	case SIGINT:
		log_debug("debug: got signal %d", sig);
		parent_shutdown();
		/* NOT REACHED */

	case SIGCHLD:
		do {
			int len;
			enum mda_resp_status mda_status;
			int mda_sysexit;
			
			pid = waitpid(-1, &status, WNOHANG);
			if (pid <= 0)
				continue;

			fail = 0;
			if (WIFSIGNALED(status)) {
				fail = 1;
				len = asprintf(&cause, "terminated; signal %d",
				    WTERMSIG(status));
				mda_status = MDA_TEMPFAIL;
				mda_sysexit = 0;
			} else if (WIFEXITED(status)) {
				if (WEXITSTATUS(status) != 0) {
					fail = 1;
					len = asprintf(&cause,
					    "exited abnormally");
					mda_sysexit = WEXITSTATUS(status);
					if (mda_sysexit == EX_OSERR ||
					    mda_sysexit == EX_TEMPFAIL)
						mda_status = MDA_TEMPFAIL;
					else
						mda_status = MDA_PERMFAIL;
				} else {
					len = asprintf(&cause, "exited okay");
					mda_status = MDA_OK;
					mda_sysexit = 0;
				}
			} else
				/* WIFSTOPPED or WIFCONTINUED */
				continue;

			if (len == -1)
				fatal("asprintf");

			if (pid == purge_pid)
				purge_pid = -1;

			child = tree_pop(&children, pid);
			if (child == NULL)
				goto skip;

			switch (child->type) {
			case CHILD_PROCESSOR:
				if (fail) {
					log_warnx("warn: lost processor: %s %s",
					    child->title, cause);
					parent_shutdown();
				}
				break;

			case CHILD_DAEMON:
				if (fail)
					log_warnx("warn: lost child: %s %s",
					    child->title, cause);
				break;

			case CHILD_MDA:
				if (WIFSIGNALED(status) &&
				    WTERMSIG(status) == SIGALRM) {
					char *tmp;
					if (asprintf(&tmp,
					    "terminated; timeout") != -1) {
						free(cause);
						cause = tmp;
					}
				}
				else if (child->cause &&
				    WIFSIGNALED(status) &&
				    WTERMSIG(status) == SIGTERM) {
					free(cause);
					cause = child->cause;
					child->cause = NULL;
				}
				free(child->cause);
				log_debug("debug: smtpd: mda process done "
				    "for session %016"PRIx64 ": %s",
				    child->mda_id, cause);

				m_create(p_pony, IMSG_MDA_DONE, 0, 0,
				    child->mda_out);
				m_add_id(p_pony, child->mda_id);
				m_add_int(p_pony, mda_status);
				m_add_int(p_pony, mda_sysexit);
				m_add_string(p_pony, cause);
				m_close(p_pony);

				break;

			case CHILD_ENQUEUE_OFFLINE:
				if (fail)
					log_warnx("warn: smtpd: "
					    "couldn't enqueue offline "
					    "message %s; smtpctl %s",
					    child->path, cause);
				else
					unlink(child->path);
				free(child->path);
				offline_done();
				break;

			default:
				fatalx("smtpd: unexpected child type");
			}
			free(child);
    skip:
			free(cause);
		} while (pid > 0 || (pid == -1 && errno == EINTR));

		break;
	default:
		fatalx("smtpd: unexpected signal");
	}
}

int
main(int argc, char *argv[])
{
	int		 c, i;
	int		 opts, flags;
	const char	*conffile = CONF_FILE;
	int		 save_argc = argc;
	char		**save_argv = argv;
	char		*rexec = NULL;
	struct smtpd	*conf;

	__progname = ssh_get_progname(argv[0]);

	/* Save argv. Duplicate so setproctitle emulation doesn't clobber it */
	saved_argc = argc;
	saved_argv = __xcalloc(argc + 1, sizeof(*saved_argv));
	for (i = 0; i < argc; i++)
		saved_argv[i] = __xstrdup(argv[i]);
	saved_argv[i] = NULL;

#ifndef HAVE_SETPROCTITLE
	/* Prepare for later setproctitle emulation */
	compat_init_setproctitle(argc, argv);
	argv = saved_argv;
#endif

	/* this is to work around GNU getopt + portable setproctitle() fuckery */
	save_argc = saved_argc;
	save_argv = saved_argv;

	if ((conf = config_default()) == NULL)
		err(1, NULL);

	env = conf;

	flags = 0;
	opts = 0;
	debug = 0;
	tracing = 0;

	log_init(1, LOG_MAIL);

	TAILQ_INIT(&offline_q);

	while ((c = getopt(argc, argv, "B:dD:hnP:f:FT:vx:")) != -1) {
		switch (c) {
		case 'B':
			if (strstr(optarg, "queue=") == optarg)
				backend_queue = strchr(optarg, '=') + 1;
			else if (strstr(optarg, "scheduler=") == optarg)
				backend_scheduler = strchr(optarg, '=') + 1;
			else if (strstr(optarg, "stat=") == optarg)
				backend_stat = strchr(optarg, '=') + 1;
			else
				log_warnx("warn: "
				    "invalid backend specifier %s",
				    optarg);
			break;
		case 'd':
			foreground = 1;
			foreground_log = 1;
			break;
		case 'D':
			if (cmdline_symset(optarg) < 0)
				log_warnx("warn: "
				    "could not parse macro definition %s",
				    optarg);
			break;
		case 'h':
			log_info("version: " SMTPD_NAME " " SMTPD_VERSION);
			usage();
			break;
		case 'n':
			debug = 2;
			opts |= SMTPD_OPT_NOACTION;
			break;
		case 'f':
			conffile = optarg;
			break;
		case 'F':
			foreground = 1;
			break;

		case 'T':
			if (!strcmp(optarg, "imsg"))
				tracing |= TRACE_IMSG;
			else if (!strcmp(optarg, "io"))
				tracing |= TRACE_IO;
			else if (!strcmp(optarg, "smtp"))
				tracing |= TRACE_SMTP;
			else if (!strcmp(optarg, "filters"))
				tracing |= TRACE_FILTERS;
			else if (!strcmp(optarg, "mta") ||
			    !strcmp(optarg, "transfer"))
				tracing |= TRACE_MTA;
			else if (!strcmp(optarg, "bounce") ||
			    !strcmp(optarg, "bounces"))
				tracing |= TRACE_BOUNCE;
			else if (!strcmp(optarg, "scheduler"))
				tracing |= TRACE_SCHEDULER;
			else if (!strcmp(optarg, "lookup"))
				tracing |= TRACE_LOOKUP;
			else if (!strcmp(optarg, "stat") ||
			    !strcmp(optarg, "stats"))
				tracing |= TRACE_STAT;
			else if (!strcmp(optarg, "rules"))
				tracing |= TRACE_RULES;
			else if (!strcmp(optarg, "mproc"))
				tracing |= TRACE_MPROC;
			else if (!strcmp(optarg, "expand"))
				tracing |= TRACE_EXPAND;
			else if (!strcmp(optarg, "table") ||
			    !strcmp(optarg, "tables"))
				tracing |= TRACE_TABLES;
			else if (!strcmp(optarg, "queue"))
				tracing |= TRACE_QUEUE;
			else if (!strcmp(optarg, "all"))
				tracing |= ~TRACE_DEBUG;
			else if (!strcmp(optarg, "profstat"))
				profiling |= PROFILE_TOSTAT;
			else if (!strcmp(optarg, "profile-imsg"))
				profiling |= PROFILE_IMSG;
			else if (!strcmp(optarg, "profile-queue"))
				profiling |= PROFILE_QUEUE;
			else
				log_warnx("warn: unknown trace flag \"%s\"",
				    optarg);
			break;
		case 'P':
			if (!strcmp(optarg, "smtp"))
				flags |= SMTPD_SMTP_PAUSED;
			else if (!strcmp(optarg, "mta"))
				flags |= SMTPD_MTA_PAUSED;
			else if (!strcmp(optarg, "mda"))
				flags |= SMTPD_MDA_PAUSED;
			break;
		case 'v':
			tracing |=  TRACE_DEBUG;
			break;
		case 'x':
			rexec = optarg;
			break;
		default:
			usage();
		}
	}

	argv += optind;
	argc -= optind;

	if (argc || *argv)
		usage();

	env->sc_opts |= opts;

	ssl_init();

	if (parse_config(conf, conffile, opts))
		exit(1);

	seed_rng();

	if (strlcpy(env->sc_conffile, conffile, PATH_MAX)
	    >= PATH_MAX)
		errx(1, "config file exceeds PATH_MAX");

	if (env->sc_opts & SMTPD_OPT_NOACTION) {
		if (env->sc_queue_key &&
		    crypto_setup(env->sc_queue_key,
		    strlen(env->sc_queue_key)) == 0) {
			fatalx("crypto_setup:"
			    "invalid key for queue encryption");
		}
		load_pki_tree();
		load_pki_keys();
		fprintf(stderr, "configuration OK\n");
		exit(0);
	}

	env->sc_flags |= flags;

	/* check for root privileges */
	if (geteuid())
		errx(1, "need root privileges");

	log_init(foreground_log, LOG_MAIL);
	log_trace_verbose(tracing);
	load_pki_tree();
	load_pki_keys();

	log_debug("debug: using \"%s\" queue backend", backend_queue);
	log_debug("debug: using \"%s\" scheduler backend", backend_scheduler);
	log_debug("debug: using \"%s\" stat backend", backend_stat);

	if (env->sc_hostname[0] == '\0')
		errx(1, "machine does not have a hostname set");
	env->sc_uptime = time(NULL);

	if (rexec == NULL) {
		smtpd_process = PROC_PARENT;

		if (env->sc_queue_flags & QUEUE_ENCRYPTION) {
			if (env->sc_queue_key == NULL) {
				char	*password;

				password = getpass("queue key: ");
				if (password == NULL)
					err(1, "getpass");

				env->sc_queue_key = strdup(password);
				explicit_bzero(password, strlen(password));
				if (env->sc_queue_key == NULL)
					err(1, "strdup");
			}
			else {
				char   *buf = NULL;
				size_t	sz = 0;
				ssize_t	len;

				if (strcasecmp(env->sc_queue_key, "stdin") == 0) {
					if ((len = getline(&buf, &sz, stdin)) == -1)
						err(1, "getline");
					if (buf[len - 1] == '\n')
						buf[len - 1] = '\0';
					env->sc_queue_key = buf;
				}
			}
		}

		log_info("info: %s %s starting", SMTPD_NAME, SMTPD_VERSION);

		if (!foreground)
			if (daemon(0, 0) == -1)
				err(1, "failed to daemonize");

		/* setup all processes */

		p_ca = start_child(save_argc, save_argv, "ca");
		p_ca->proc = PROC_CA;

		p_control = start_child(save_argc, save_argv, "control");
		p_control->proc = PROC_CONTROL;

		p_lka = start_child(save_argc, save_argv, "lka");
		p_lka->proc = PROC_LKA;

		p_pony = start_child(save_argc, save_argv, "pony");
		p_pony->proc = PROC_PONY;

		p_queue = start_child(save_argc, save_argv, "queue");
		p_queue->proc = PROC_QUEUE;

		p_scheduler = start_child(save_argc, save_argv, "scheduler");
		p_scheduler->proc = PROC_SCHEDULER;

		setup_peers(p_control, p_ca);
		setup_peers(p_control, p_lka);
		setup_peers(p_control, p_pony);
		setup_peers(p_control, p_queue);
		setup_peers(p_control, p_scheduler);
		setup_peers(p_pony, p_ca);
		setup_peers(p_pony, p_lka);
		setup_peers(p_pony, p_queue);
		setup_peers(p_queue, p_lka);
		setup_peers(p_queue, p_scheduler);

		if (env->sc_queue_key) {
			if (imsg_compose(&p_queue->imsgbuf, IMSG_SETUP_KEY, 0,
			    0, -1, env->sc_queue_key, strlen(env->sc_queue_key)
			    + 1) == -1)
				fatal("imsg_compose");
			if (imsg_flush(&p_queue->imsgbuf) == -1)
				fatal("imsg_flush");
		}

		setup_done(p_ca);
		setup_done(p_control);
		setup_done(p_lka);
		setup_done(p_pony);
		setup_done(p_queue);
		setup_done(p_scheduler);

		log_debug("smtpd: setup done");

		return smtpd();
	}

	if (!strcmp(rexec, "ca")) {
		smtpd_process = PROC_CA;
		setup_proc();

		return ca();
	}

	else if (!strcmp(rexec, "control")) {
		smtpd_process = PROC_CONTROL;
		setup_proc();

		/* the control socket ensures that only one smtpd instance is running */
		control_socket = control_create_socket();

		env->sc_stat = stat_backend_lookup(backend_stat);
		if (env->sc_stat == NULL)
			errx(1, "could not find stat backend \"%s\"", backend_stat);

		return control();
	}

	else if (!strcmp(rexec, "lka")) {
		smtpd_process = PROC_LKA;
		setup_proc();

		return lka();
	}

	else if (!strcmp(rexec, "pony")) {
		smtpd_process = PROC_PONY;
		setup_proc();

		return pony();
	}

	else if (!strcmp(rexec, "queue")) {
		smtpd_process = PROC_QUEUE;
		setup_proc();

		if (env->sc_queue_flags & QUEUE_COMPRESSION)
			env->sc_comp = compress_backend_lookup("gzip");

		if (!queue_init(backend_queue, 1))
			errx(1, "could not initialize queue backend");

		return queue();
	}

	else if (!strcmp(rexec, "scheduler")) {
		smtpd_process = PROC_SCHEDULER;
		setup_proc();

		for (i = 0; i < MAX_BOUNCE_WARN; i++) {
			if (env->sc_bounce_warn[i] == 0)
				break;
			log_debug("debug: bounce warning after %s",
			    duration_to_text(env->sc_bounce_warn[i]));
		}

		return scheduler();
	}

	fatalx("bad rexec: %s", rexec);

	return (1);
}

static struct mproc *
start_child(int save_argc, char **save_argv, char *rexec)
{
	struct mproc *p;
	char *argv[SMTPD_MAXARG];
	int sp[2], argc = 0;
	pid_t pid;

	if (save_argc >= SMTPD_MAXARG - 2)
		fatalx("too many arguments");

	if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, sp) == -1)
		fatal("socketpair");

	io_set_nonblocking(sp[0]);
	io_set_nonblocking(sp[1]);

	switch (pid = fork()) {
	case -1:
		fatal("%s: fork", save_argv[0]);
	case 0:
		break;
	default:
		close(sp[0]);
		p = calloc(1, sizeof(*p));
		if (p == NULL)
			fatal("calloc");
		if((p->name = strdup(rexec)) == NULL)
			fatal("strdup");
		mproc_init(p, sp[1]);
		p->pid = pid;
		p->handler = parent_imsg;
		return p;
	}

	if (sp[0] != 3) {
		if (dup2(sp[0], 3) == -1)
			fatal("%s: dup2", rexec);
	} else if (fcntl(sp[0], F_SETFD, 0) == -1)
		fatal("%s: fcntl", rexec);

	xclosefrom(4);

	for (argc = 0; argc < save_argc; argc++)
		argv[argc] = save_argv[argc];
	argv[argc++] = "-x";
	argv[argc++] = rexec;
	argv[argc++] = NULL;

	execvp(argv[0], argv);
	fatal("%s: execvp", rexec);
}

static void
setup_peers(struct mproc *a, struct mproc *b)
{
	int sp[2];

	if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, sp) == -1)
		fatal("socketpair");

	io_set_nonblocking(sp[0]);
	io_set_nonblocking(sp[1]);

	if (imsg_compose(&a->imsgbuf, IMSG_SETUP_PEER, b->proc, b->pid, sp[0],
	    NULL, 0) == -1)
		fatal("imsg_compose");
	if (imsg_flush(&a->imsgbuf) == -1)
		fatal("imsg_flush");

	if (imsg_compose(&b->imsgbuf, IMSG_SETUP_PEER, a->proc, a->pid, sp[1],
	    NULL, 0) == -1)
		fatal("imsg_compose");
	if (imsg_flush(&b->imsgbuf) == -1)
		fatal("imsg_flush");
}

static void
setup_done(struct mproc *p)
{
	struct imsg imsg;

	if (imsg_compose(&p->imsgbuf, IMSG_SETUP_DONE, 0, 0, -1, NULL, 0) == -1)
		fatal("imsg_compose");
	if (imsg_flush(&p->imsgbuf) == -1)
		fatal("imsg_flush");

	if (imsg_wait(&p->imsgbuf, &imsg, 10000) == -1)
		fatal("imsg_wait");

	if (imsg.hdr.type != IMSG_SETUP_DONE)
		fatalx("expect IMSG_SETUP_DONE");

	log_debug("setup_done: %s[%d] done", p->name, p->pid);

	imsg_free(&imsg);
}

static void
setup_proc(void)
{
	struct imsgbuf *ibuf;
	struct imsg imsg;
        int setup = 1;

	log_procinit(proc_title(smtpd_process));

	p_parent = calloc(1, sizeof(*p_parent));
	if (p_parent == NULL)
		fatal("calloc");
	if((p_parent->name = strdup("parent")) == NULL)
		fatal("strdup");
	p_parent->proc = PROC_PARENT;
	p_parent->handler = imsg_dispatch;
	mproc_init(p_parent, 3);

	ibuf = &p_parent->imsgbuf;

	while (setup) {
		if (imsg_wait(ibuf, &imsg, 10000) == -1)
			fatal("imsg_wait");

		switch (imsg.hdr.type) {
		case IMSG_SETUP_KEY:
			env->sc_queue_key = strdup(imsg.data);
			break;
		case IMSG_SETUP_PEER:
			setup_peer(imsg.hdr.peerid, imsg.hdr.pid, imsg.fd);
			break;
		case IMSG_SETUP_DONE:
			setup = 0;
			break;
		default:
			fatal("bad imsg %d", imsg.hdr.type);
		}
		imsg_free(&imsg);
	}

	if (imsg_compose(ibuf, IMSG_SETUP_DONE, 0, 0, -1, NULL, 0) == -1)
		fatal("imsg_compose");

	if (imsg_flush(ibuf) == -1)
		fatal("imsg_flush");

	log_debug("setup_proc: %s done", proc_title(smtpd_process));
}

static struct mproc *
setup_peer(enum smtp_proc_type proc, pid_t pid, int sock)
{
	struct mproc *p, **pp;

	log_debug("setup_peer: %s -> %s[%u] fd=%d", proc_title(smtpd_process),
	    proc_title(proc), pid, sock);

	if (sock == -1)
		fatalx("peer socket not received");

	switch (proc) {
	case PROC_LKA:
		pp = &p_lka;
		break;
	case PROC_QUEUE:
		pp = &p_queue;
		break;
	case PROC_CONTROL:
		pp = &p_control;
		break;
	case PROC_SCHEDULER:
		pp = &p_scheduler;
		break;
	case PROC_PONY:
		pp = &p_pony;
		break;
	case PROC_CA:
		pp = &p_ca;
		break;
	default:
		fatalx("unknown peer");
	}

	if (*pp)
		fatalx("peer already set");

	p = calloc(1, sizeof(*p));
	if (p == NULL)
		fatal("calloc");
	if((p->name = strdup(proc_title(proc))) == NULL)
		fatal("strdup");
	mproc_init(p, sock);
	p->pid = pid;
	p->proc = proc;
	p->handler = imsg_dispatch;

	*pp = p;

	return p;
}

static int
imsg_wait(struct imsgbuf *ibuf, struct imsg *imsg, int timeout)
{
	struct pollfd pfd[1];
	ssize_t n;

	pfd[0].fd = ibuf->fd;
	pfd[0].events = POLLIN;

	while (1) {
		if ((n = imsg_get(ibuf, imsg)) == -1)
			return -1;
		if (n)
			return 1;

		n = poll(pfd, 1, timeout);
		if (n == -1)
			return -1;
		if (n == 0) {
			errno = ETIMEDOUT;
			return -1;
		}

		if (((n = imsg_read(ibuf)) == -1 && errno != EAGAIN) || n == 0)
			return -1;
	}
}

int
smtpd(void) {
	struct event	 ev_sigint;
	struct event	 ev_sigterm;
	struct event	 ev_sigchld;
	struct event	 ev_sighup;
	struct timeval	 tv;

	imsg_callback = parent_imsg;

	tree_init(&children);

	child_add(p_queue->pid, CHILD_DAEMON, proc_title(PROC_QUEUE));
	child_add(p_control->pid, CHILD_DAEMON, proc_title(PROC_CONTROL));
	child_add(p_lka->pid, CHILD_DAEMON, proc_title(PROC_LKA));
	child_add(p_scheduler->pid, CHILD_DAEMON, proc_title(PROC_SCHEDULER));
	child_add(p_pony->pid, CHILD_DAEMON, proc_title(PROC_PONY));
	child_add(p_ca->pid, CHILD_DAEMON, proc_title(PROC_CA));

	event_init();

	signal_set(&ev_sigint, SIGINT, parent_sig_handler, NULL);
	signal_set(&ev_sigterm, SIGTERM, parent_sig_handler, NULL);
	signal_set(&ev_sigchld, SIGCHLD, parent_sig_handler, NULL);
	signal_set(&ev_sighup, SIGHUP, parent_sig_handler, NULL);
	signal_add(&ev_sigint, NULL);
	signal_add(&ev_sigterm, NULL);
	signal_add(&ev_sigchld, NULL);
	signal_add(&ev_sighup, NULL);
	signal(SIGPIPE, SIG_IGN);

	config_peer(PROC_CONTROL);
	config_peer(PROC_LKA);
	config_peer(PROC_QUEUE);
	config_peer(PROC_CA);
	config_peer(PROC_PONY);

	evtimer_set(&config_ev, parent_send_config, NULL);
	memset(&tv, 0, sizeof(tv));
	evtimer_add(&config_ev, &tv);

	/* defer offline scanning for a second */
	evtimer_set(&offline_ev, offline_scan, NULL);
	offline_timeout.tv_sec = 1;
	offline_timeout.tv_usec = 0;
	evtimer_add(&offline_ev, &offline_timeout);

	if (pidfile(NULL) < 0)
		err(1, "pidfile");

	fork_processors();

	purge_task();

#if HAVE_PLEDGE
	if (pledge("stdio rpath wpath cpath fattr tmppath "
	    "getpw sendfd proc exec id inet unix", NULL) == -1)
		err(1, "pledge");
#endif

	event_dispatch();
	fatalx("exited event loop");

	return (0);
}

static void
load_pki_tree(void)
{
	struct pki	*pki;
	struct ca	*sca;
	const char	*k;
	void		*iter_dict;

	log_debug("debug: init ssl-tree");
	iter_dict = NULL;
	while (dict_iter(env->sc_pki_dict, &iter_dict, &k, (void **)&pki)) {
		log_debug("info: loading pki information for %s", k);
		if (pki->pki_cert_file == NULL)
			fatalx("load_pki_tree: missing certificate file");
		if (pki->pki_key_file == NULL)
			fatalx("load_pki_tree: missing key file");

		if (!ssl_load_certificate(pki, pki->pki_cert_file))
			fatalx("load_pki_tree: failed to load certificate file");
	}

	log_debug("debug: init ca-tree");
	iter_dict = NULL;
	while (dict_iter(env->sc_ca_dict, &iter_dict, &k, (void **)&sca)) {
		log_debug("info: loading CA information for %s", k);
		if (!ssl_load_cafile(sca, sca->ca_cert_file))
			fatalx("load_pki_tree: failed to load CA file");
	}
}

void
load_pki_keys(void)
{
	struct pki	*pki;
	const char	*k;
	void		*iter_dict;

	log_debug("debug: init ssl-tree");
	iter_dict = NULL;
	while (dict_iter(env->sc_pki_dict, &iter_dict, &k, (void **)&pki)) {
		log_debug("info: loading pki keys for %s", k);

		if (!ssl_load_keyfile(pki, pki->pki_key_file, k))
			fatalx("load_pki_keys: failed to load key file");
	}
}

int
fork_proc_backend(const char *key, const char *conf, const char *procname)
{
	pid_t		pid;
	int		sp[2];
	char		path[PATH_MAX];
	char		name[PATH_MAX];
	char		*arg;

	if (strlcpy(name, conf, sizeof(name)) >= sizeof(name)) {
		log_warnx("warn: %s-proc: conf too long", key);
		return (0);
	}

	arg = strchr(name, ':');
	if (arg)
		*arg++ = '\0';

	if (snprintf(path, sizeof(path), PATH_LIBEXEC "/%s-%s", key, name) >=
	    (ssize_t)sizeof(path)) {
		log_warn("warn: %s-proc: exec path too long", key);
		return (-1);
	}

	if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, sp) == -1) {
		log_warn("warn: %s-proc: socketpair", key);
		return (-1);
	}

	if ((pid = fork()) == -1) {
		log_warn("warn: %s-proc: fork", key);
		close(sp[0]);
		close(sp[1]);
		return (-1);
	}

	if (pid == 0) {
		/* child process */
		dup2(sp[0], STDIN_FILENO);
		closefrom(STDERR_FILENO + 1);

		if (procname == NULL)
			procname = name;

		execl(path, procname, arg, (char *)NULL);
		err(1, "execl: %s", path);
	}

	/* parent process */
	close(sp[0]);

	return (sp[1]);
}

struct child *
child_add(pid_t pid, int type, const char *title)
{
	struct child	*child;

	if ((child = calloc(1, sizeof(*child))) == NULL)
		fatal("smtpd: child_add: calloc");

	child->pid = pid;
	child->type = type;
	child->title = title;

	tree_xset(&children, pid, child);

	return (child);
}

static void
purge_task(void)
{
	struct passwd	*pw;
	DIR		*d;
	int		 n;
	uid_t		 uid;
	gid_t		 gid;

	n = 0;
	if ((d = opendir(PATH_SPOOL PATH_PURGE))) {
		while (readdir(d) != NULL)
			n++;
		closedir(d);
	} else
		log_warn("warn: purge_task: opendir");

	if (n > 2) {
		switch (purge_pid = fork()) {
		case -1:
			log_warn("warn: purge_task: fork");
			break;
		case 0:
			if ((pw = getpwnam(SMTPD_QUEUE_USER)) == NULL)
				fatalx("unknown user " SMTPD_QUEUE_USER);
			if (chroot(PATH_SPOOL PATH_PURGE) == -1)
				fatal("smtpd: chroot");
			if (chdir("/") == -1)
				fatal("smtpd: chdir");
			uid = pw->pw_uid;
			gid = pw->pw_gid;
			if (setgroups(1, &gid) ||
			    setresgid(gid, gid, gid) ||
			    setresuid(uid, uid, uid))
				fatal("smtpd: cannot drop privileges");
			rmtree("/", 1);
			_exit(0);
			break;
		default:
			break;
		}
	}
}

static void
fork_processors(void)
{
	const char	*name;
	struct processor	*processor;
	void		*iter;

	iter = NULL;
	while (dict_iter(env->sc_processors_dict, &iter, &name, (void **)&processor))
		fork_processor(name, processor->command, processor->user, processor->group, processor->chroot);
}

static void
fork_processor(const char *name, const char *command, const char *user, const char *group, const char *chroot_path)
{
	pid_t		 pid;
	struct processor	*processor;
	char		 buf;
	int		 sp[2], errfd[2];
	struct passwd	*pw;
	struct group	*gr;
	char		 exec[_POSIX_ARG_MAX];
	int		 execr;

	if (user == NULL)
		user = SMTPD_USER;
	if ((pw = getpwnam(user)) == NULL)
		err(1, "getpwnam");

	if (group) {
		if ((gr = getgrnam(group)) == NULL)
			err(1, "getgrnam");
	}
	else {
		if ((gr = getgrgid(pw->pw_gid)) == NULL)
			err(1, "getgrgid");
	}

	if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, sp) == -1)
		err(1, "socketpair");
	if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, errfd) == -1)
		err(1, "socketpair");

	if ((pid = fork()) == -1)
		err(1, "fork");

	/* parent passes the child fd over to lka */
	if (pid > 0) {
		processor = dict_xget(env->sc_processors_dict, name);
		processor->errfd = errfd[1];
		child_add(pid, CHILD_PROCESSOR, name);
		close(sp[0]);
		close(errfd[0]);
		m_create(p_lka, IMSG_LKA_PROCESSOR_FORK, 0, 0, sp[1]);
		m_add_string(p_lka, name);
		m_close(p_lka);
		return;
	}

	close(sp[1]);
	close(errfd[1]);
	dup2(sp[0], STDIN_FILENO);
	dup2(sp[0], STDOUT_FILENO);
	dup2(errfd[0], STDERR_FILENO);

	if (chroot_path) {
		if (chroot(chroot_path) != 0 || chdir("/") != 0)
			err(1, "chroot: %s", chroot_path);
	}

	if (setgroups(1, &gr->gr_gid) ||
	    setresgid(gr->gr_gid, gr->gr_gid, gr->gr_gid) ||
	    setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid))
		err(1, "fork_processor: cannot drop privileges");

	xclosefrom(STDERR_FILENO + 1);

	if (setsid() < 0)
		err(1, "setsid");
	if (signal(SIGPIPE, SIG_DFL) == SIG_ERR ||
	    signal(SIGINT, SIG_DFL) == SIG_ERR ||
	    signal(SIGTERM, SIG_DFL) == SIG_ERR ||
	    signal(SIGCHLD, SIG_DFL) == SIG_ERR ||
	    signal(SIGHUP, SIG_DFL) == SIG_ERR)
		err(1, "signal");

	if (command[0] == '/')
		execr = snprintf(exec, sizeof(exec), "exec %s", command);
	else
		execr = snprintf(exec, sizeof(exec), "exec %s/%s", 
		    PATH_LIBEXEC, command);
	if (execr >= (int) sizeof(exec))
		errx(1, "%s: exec path too long", name);

	/*
	 * Wait for lka to acknowledge that it received the fd.
	 * This prevents a race condition between the filter sending an error
	 * message, and exiting and lka not being able to log it because of
	 * SIGCHLD.
	 * (Ab)use read to determine if the fd is installed; since stderr is
	 * never going to be read from we can shutdown(2) the write-end in lka.
	 */
	if (read(STDERR_FILENO, &buf, 1) != 0)
		errx(1, "lka didn't properly close write end of error socket");
	if (system(exec) == -1)
		err(1, NULL);

	/* there's no successful exit from a processor */
	_exit(1);
}

static void
forkmda(struct mproc *p, uint64_t id, struct deliver *deliver)
{
	char		 ebuf[128], sfn[32];
	struct dispatcher	*dsp;
	struct child	*child;
	pid_t		 pid;
	int		 allout, pipefd[2];
	struct passwd	*pw;
	const char	*pw_name;
	uid_t	pw_uid;
	gid_t	pw_gid;
	const char	*pw_dir;

	dsp = dict_xget(env->sc_dispatchers, deliver->dispatcher);

	log_debug("debug: smtpd: forking mda for session %016"PRIx64
	    ": %s as %s", id, deliver->userinfo.username,
	    dsp->u.local.user ? dsp->u.local.user : deliver->userinfo.username);

	if (dsp->u.local.user) {
		if ((pw = getpwnam(dsp->u.local.user)) == NULL) {
			(void)snprintf(ebuf, sizeof ebuf,
			    "delivery user '%s' does not exist",
			    dsp->u.local.user);
			m_create(p_pony, IMSG_MDA_DONE, 0, 0, -1);
			m_add_id(p_pony, id);
			m_add_int(p_pony, MDA_PERMFAIL);
			m_add_int(p_pony, EX_NOUSER);
			m_add_string(p_pony, ebuf);
			m_close(p_pony);
			return;
		}
		pw_name = pw->pw_name;
		pw_uid = pw->pw_uid;
		pw_gid = pw->pw_gid;
		pw_dir = pw->pw_dir;
	}
	else {
		pw_name = deliver->userinfo.username;
		pw_uid = deliver->userinfo.uid;
		pw_gid = deliver->userinfo.gid;
		pw_dir = deliver->userinfo.directory;
	}

	if (pw_uid == 0 && deliver->mda_exec[0]) {
		pw_name = deliver->userinfo.username;
		pw_uid = deliver->userinfo.uid;
		pw_gid = deliver->userinfo.gid;
		pw_dir = deliver->userinfo.directory;
	}

	if (pw_uid == 0 && !dsp->u.local.is_mbox) {
		(void)snprintf(ebuf, sizeof ebuf, "not allowed to deliver to: %s",
		    deliver->userinfo.username);
		m_create(p_pony, IMSG_MDA_DONE, 0, 0, -1);
		m_add_id(p_pony, id);
		m_add_int(p_pony, MDA_PERMFAIL);
		m_add_int(p_pony, EX_NOPERM);
		m_add_string(p_pony, ebuf);
		m_close(p_pony);
		return;
	}

	if (pipe(pipefd) == -1) {
		(void)snprintf(ebuf, sizeof ebuf, "pipe: %s", strerror(errno));
		m_create(p_pony, IMSG_MDA_DONE, 0, 0, -1);
		m_add_id(p_pony, id);
		m_add_int(p_pony, MDA_TEMPFAIL);
		m_add_int(p_pony, EX_OSERR);
		m_add_string(p_pony, ebuf);
		m_close(p_pony);
		return;
	}

	/* prepare file which captures stdout and stderr */
	(void)strlcpy(sfn, "/tmp/smtpd.out.XXXXXXXXXXX", sizeof(sfn));
	allout = mkstemp(sfn);
	if (allout == -1) {
		(void)snprintf(ebuf, sizeof ebuf, "mkstemp: %s", strerror(errno));
		m_create(p_pony, IMSG_MDA_DONE, 0, 0, -1);
		m_add_id(p_pony, id);
		m_add_int(p_pony, MDA_TEMPFAIL);
		m_add_int(p_pony, EX_OSERR);
		m_add_string(p_pony, ebuf);
		m_close(p_pony);
		close(pipefd[0]);
		close(pipefd[1]);
		return;
	}
	unlink(sfn);

	pid = fork();
	if (pid == -1) {
		(void)snprintf(ebuf, sizeof ebuf, "fork: %s", strerror(errno));
		m_create(p_pony, IMSG_MDA_DONE, 0, 0, -1);
		m_add_id(p_pony, id);
		m_add_int(p_pony, MDA_TEMPFAIL);
		m_add_int(p_pony, EX_OSERR);
		m_add_string(p_pony, ebuf);
		m_close(p_pony);
		close(pipefd[0]);
		close(pipefd[1]);
		close(allout);
		return;
	}

	/* parent passes the child fd over to mda */
	if (pid > 0) {
		child = child_add(pid, CHILD_MDA, NULL);
		child->mda_out = allout;
		child->mda_id = id;
		close(pipefd[0]);
		m_create(p, IMSG_MDA_FORK, 0, 0, pipefd[1]);
		m_add_id(p, id);
		m_close(p);
		return;
	}

        /* mbox helper, create mailbox before privdrop if it doesn't exist */                                                                            
        if (dsp->u.local.is_mbox)                                            
                mda_mbox_init(deliver);

	if (chdir(pw_dir) == -1 && chdir("/") == -1)
		err(1, "chdir");
	if (setgroups(1, &pw_gid) ||
	    setresgid(pw_gid, pw_gid, pw_gid) ||
	    setresuid(pw_uid, pw_uid, pw_uid))
		err(1, "forkmda: cannot drop privileges");
	if (dup2(pipefd[0], STDIN_FILENO) == -1 ||
	    dup2(allout, STDOUT_FILENO) == -1 ||
	    dup2(allout, STDERR_FILENO) == -1)
		err(1, "forkmda: dup2");
	closefrom(STDERR_FILENO + 1);
	if (setsid() < 0)
		err(1, "setsid");
	if (signal(SIGPIPE, SIG_DFL) == SIG_ERR ||
	    signal(SIGINT, SIG_DFL) == SIG_ERR ||
	    signal(SIGTERM, SIG_DFL) == SIG_ERR ||
	    signal(SIGCHLD, SIG_DFL) == SIG_ERR ||
	    signal(SIGHUP, SIG_DFL) == SIG_ERR)
		err(1, "signal");

	/* avoid hangs by setting 5m timeout */
	alarm(300);

        if (dsp->u.local.is_mbox &&                                          
            dsp->u.local.mda_wrapper == NULL &&                              
            deliver->mda_exec[0] == '\0')                                    
                mda_mbox(deliver);                                           
        else                                                             
                mda_unpriv(dsp, deliver, pw_name, pw_dir);
}

static void
offline_scan(int fd, short ev, void *arg)
{
	char		*path_argv[2];
	FTS		*fts = arg;
	FTSENT		*e;
	int		 n = 0;

	path_argv[0] = PATH_SPOOL PATH_OFFLINE;
	path_argv[1] = NULL;

	if (fts == NULL) {
		log_debug("debug: smtpd: scanning offline queue...");
		fts = fts_open(path_argv, FTS_PHYSICAL | FTS_NOCHDIR, NULL);
		if (fts == NULL) {
			log_warn("fts_open: %s", path_argv[0]);
			return;
		}
	}

	while ((e = fts_read(fts)) != NULL) {
		if (e->fts_info != FTS_F)
			continue;

		/* offline files must be at depth 1 */
		if (e->fts_level != 1)
			continue;

		/* offline file group must match parent directory group */
		if (e->fts_statp->st_gid != e->fts_parent->fts_statp->st_gid)
			continue;

		if (e->fts_statp->st_size == 0) {
			if (unlink(e->fts_accpath) == -1)
				log_warnx("warn: smtpd: could not unlink %s", e->fts_accpath);
			continue;
		}

		if (offline_add(e->fts_name, e->fts_statp->st_uid,
		    e->fts_statp->st_gid)) {
			log_warnx("warn: smtpd: "
			    "could not add offline message %s", e->fts_name);
			continue;
		}

		if ((n++) == OFFLINE_READMAX) {
			evtimer_set(&offline_ev, offline_scan, fts);
			offline_timeout.tv_sec = 0;
			offline_timeout.tv_usec = 100000;
			evtimer_add(&offline_ev, &offline_timeout);
			return;
		}
	}

	log_debug("debug: smtpd: offline scanning done");
	fts_close(fts);
}

static int
offline_enqueue(char *name, uid_t uid, gid_t gid)
{
	char		*path;
	struct stat	 sb;
	pid_t		 pid;
	struct child	*child;
	struct passwd	*pw;
	int		 pathlen;

	pathlen = asprintf(&path, "%s/%s", PATH_SPOOL PATH_OFFLINE, name);
	if (pathlen == -1) {
		log_warnx("warn: smtpd: asprintf");
		return (-1);
	}

	if (pathlen >= PATH_MAX) {
		log_warnx("warn: smtpd: pathname exceeds PATH_MAX");
		free(path);
		return (-1);
	}

	log_debug("debug: smtpd: enqueueing offline message %s", path);

	if ((pid = fork()) == -1) {
		log_warn("warn: smtpd: fork");
		free(path);
		return (-1);
	}

	if (pid == 0) {
		char	*envp[2], *p = NULL, *tmp;
		int	 fd;
		FILE	*fp;
		size_t	 sz = 0;
		ssize_t	 len;
		arglist	 args;

		closefrom(STDERR_FILENO + 1);

		memset(&args, 0, sizeof(args));

		if ((fd = open(path, O_RDONLY|O_NOFOLLOW|O_NONBLOCK)) == -1) {
			log_warn("warn: smtpd: open: %s", path);
			_exit(1);
		}

		if (fstat(fd, &sb) == -1) {
			log_warn("warn: smtpd: fstat: %s", path);
			_exit(1);
		}

		if (!S_ISREG(sb.st_mode)) {
			log_warnx("warn: smtpd: file %s (uid %d) not regular",
			    path, sb.st_uid);
			_exit(1);
		}

		if (sb.st_nlink != 1) {
			log_warnx("warn: smtpd: file %s is hard-link", path);
			_exit(1);
		}

		if (sb.st_uid != uid) {
			log_warnx("warn: smtpd: file %s has bad uid %d",
			    path, sb.st_uid);
			_exit(1);
		}

		if (sb.st_gid != gid) {
			log_warnx("warn: smtpd: file %s has bad gid %d",
			    path, sb.st_gid);
			_exit(1);
		}

		pw = getpwuid(sb.st_uid);
		if (pw == NULL) {
			log_warnx("warn: smtpd: getpwuid for uid %d failed",
			    sb.st_uid);
			_exit(1);
		}

		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))
			_exit(1);

		if ((fp = fdopen(fd, "r")) == NULL)
			_exit(1);

		if (chdir(pw->pw_dir) == -1 && chdir("/") == -1)
			_exit(1);

		if (setsid() == -1 ||
		    signal(SIGPIPE, SIG_DFL) == SIG_ERR ||
		    dup2(fileno(fp), STDIN_FILENO) == -1)
			_exit(1);

		if ((len = getline(&p, &sz, fp)) == -1)
			_exit(1);

		if (p[len - 1] != '\n')
			_exit(1);
		p[len - 1] = '\0';

		addargs(&args, "%s", "sendmail");
		addargs(&args, "%s", "-S");

		while ((tmp = strsep(&p, "|")) != NULL)
			addargs(&args, "%s", tmp);

		free(p);
		if (lseek(fileno(fp), len, SEEK_SET) == -1)
			_exit(1);

		envp[0] = "PATH=" _PATH_DEFPATH;
		envp[1] = (char *)NULL;
		environ = envp;

		execvp(PATH_SMTPCTL, args.list);
		_exit(1);
	}

	offline_running++;
	child = child_add(pid, CHILD_ENQUEUE_OFFLINE, NULL);
	child->path = path;

	return (0);
}

static int
offline_add(char *path, uid_t uid, gid_t gid)
{
	struct offline	*q;

	if (offline_running < OFFLINE_QUEUEMAX)
		/* skip queue */
		return offline_enqueue(path, uid, gid);

	q = malloc(sizeof(*q) + strlen(path) + 1);
	if (q == NULL)
		return (-1);
	q->uid = uid;
	q->gid = gid;
	q->path = (char *)q + sizeof(*q);
	memmove(q->path, path, strlen(path) + 1);
	TAILQ_INSERT_TAIL(&offline_q, q, entry);

	return (0);
}

static void
offline_done(void)
{
	struct offline	*q;

	offline_running--;

	while (offline_running < OFFLINE_QUEUEMAX) {
		if ((q = TAILQ_FIRST(&offline_q)) == NULL)
			break; /* all done */
		TAILQ_REMOVE(&offline_q, q, entry);
		offline_enqueue(q->path, q->uid, q->gid);
		free(q);
	}
}

static int
parent_forward_open(char *username, char *directory, uid_t uid, gid_t gid)
{
	char		pathname[PATH_MAX];
	int		fd;
	struct stat	sb;

	if (!bsnprintf(pathname, sizeof (pathname), "%s/.forward",
		directory)) {
		log_warnx("warn: smtpd: %s: pathname too large", pathname);
		return -1;
	}

	if (stat(directory, &sb) == -1) {
		log_warn("warn: smtpd: parent_forward_open: %s", directory);
		return -1;
	}
	if (sb.st_mode & S_ISVTX) {
		log_warnx("warn: smtpd: parent_forward_open: %s is sticky",
		    directory);
		errno = EAGAIN;
		return -1;
	}

	do {
		fd = open(pathname, O_RDONLY|O_NOFOLLOW|O_NONBLOCK);
	} while (fd == -1 && errno == EINTR);
	if (fd == -1) {
		if (errno == ENOENT)
			return -1;
		if (errno == EMFILE || errno == ENFILE || errno == EIO) {
			errno = EAGAIN;
			return -1;
		}
		if (errno == ELOOP)
			log_warnx("warn: smtpd: parent_forward_open: %s: "
			    "cannot follow symbolic links", pathname);
		else
			log_warn("warn: smtpd: parent_forward_open: %s", pathname);
		return -1;
	}

	if (!secure_file(fd, pathname, directory, uid, 1)) {
		log_warnx("warn: smtpd: %s: unsecure file", pathname);
		close(fd);
		return -1;
	}

	return fd;
}

void
imsg_dispatch(struct mproc *p, struct imsg *imsg)
{
	struct timespec	t0, t1, dt;
	int		msg;

	if (imsg == NULL) {
		imsg_callback(p, imsg);
		return;
	}

	log_imsg(smtpd_process, p->proc, imsg);

	if (profiling & PROFILE_IMSG)
		clock_gettime(CLOCK_MONOTONIC, &t0);

	msg = imsg->hdr.type;
	imsg_callback(p, imsg);

	if (profiling & PROFILE_IMSG) {
		clock_gettime(CLOCK_MONOTONIC, &t1);
		timespecsub(&t1, &t0, &dt);

		log_debug("profile-imsg: %s %s %s %d %lld.%09ld",
		    proc_name(smtpd_process),
		    proc_name(p->proc),
		    imsg_to_str(msg),
		    (int)imsg->hdr.len,
		    (long long)dt.tv_sec,
		    dt.tv_nsec);

		if (profiling & PROFILE_TOSTAT) {
			char	key[STAT_KEY_SIZE];
			/* can't profstat control process yet */
			if (smtpd_process == PROC_CONTROL)
				return;

			if (!bsnprintf(key, sizeof key,
				"profiling.imsg.%s.%s.%s",
				proc_name(smtpd_process),
				proc_name(p->proc),
				imsg_to_str(msg)))
				return;
			stat_set(key, stat_timespec(&dt));
		}
	}
}

void
log_imsg(int to, int from, struct imsg *imsg)
{

	if (to == PROC_CONTROL && imsg->hdr.type == IMSG_STAT_SET)
		return;

	if (imsg->fd != -1)
		log_trace(TRACE_IMSG, "imsg: %s <- %s: %s (len=%zu, fd=%d)",
		    proc_name(to),
		    proc_name(from),
		    imsg_to_str(imsg->hdr.type),
		    imsg->hdr.len - IMSG_HEADER_SIZE,
		    imsg->fd);
	else
		log_trace(TRACE_IMSG, "imsg: %s <- %s: %s (len=%zu)",
		    proc_name(to),
		    proc_name(from),
		    imsg_to_str(imsg->hdr.type),
		    imsg->hdr.len - IMSG_HEADER_SIZE);
}

const char *
proc_title(enum smtp_proc_type proc)
{
	switch (proc) {
	case PROC_PARENT:
		return "[priv]";
	case PROC_LKA:
		return "lookup";
	case PROC_QUEUE:
		return "queue";
	case PROC_CONTROL:
		return "control";
	case PROC_SCHEDULER:
		return "scheduler";
	case PROC_PONY:
		return "pony express";
	case PROC_CA:
		return "klondike";
	case PROC_CLIENT:
		return "client";
	case PROC_PROCESSOR:
		return "processor";
	}
	return "unknown";
}

const char *
proc_name(enum smtp_proc_type proc)
{
	switch (proc) {
	case PROC_PARENT:
		return "parent";
	case PROC_LKA:
		return "lka";
	case PROC_QUEUE:
		return "queue";
	case PROC_CONTROL:
		return "control";
	case PROC_SCHEDULER:
		return "scheduler";
	case PROC_PONY:
		return "pony";
	case PROC_CA:
		return "ca";
	case PROC_CLIENT:
		return "client-proc";
	default:
		return "unknown";
	}
}

#define CASE(x) case x : return #x

const char *
imsg_to_str(int type)
{
	static char	 buf[32];

	switch (type) {
	CASE(IMSG_NONE);

	CASE(IMSG_CTL_OK);
	CASE(IMSG_CTL_FAIL);

	CASE(IMSG_CTL_GET_DIGEST);
	CASE(IMSG_CTL_GET_STATS);
	CASE(IMSG_CTL_LIST_MESSAGES);
	CASE(IMSG_CTL_LIST_ENVELOPES);
	CASE(IMSG_CTL_MTA_SHOW_HOSTS);
	CASE(IMSG_CTL_MTA_SHOW_RELAYS);
	CASE(IMSG_CTL_MTA_SHOW_ROUTES);
	CASE(IMSG_CTL_MTA_SHOW_HOSTSTATS);
	CASE(IMSG_CTL_MTA_BLOCK);
	CASE(IMSG_CTL_MTA_UNBLOCK);
	CASE(IMSG_CTL_MTA_SHOW_BLOCK);
	CASE(IMSG_CTL_PAUSE_EVP);
	CASE(IMSG_CTL_PAUSE_MDA);
	CASE(IMSG_CTL_PAUSE_MTA);
	CASE(IMSG_CTL_PAUSE_SMTP);
	CASE(IMSG_CTL_PROFILE);
	CASE(IMSG_CTL_PROFILE_DISABLE);
	CASE(IMSG_CTL_PROFILE_ENABLE);
	CASE(IMSG_CTL_RESUME_EVP);
	CASE(IMSG_CTL_RESUME_MDA);
	CASE(IMSG_CTL_RESUME_MTA);
	CASE(IMSG_CTL_RESUME_SMTP);
	CASE(IMSG_CTL_RESUME_ROUTE);
	CASE(IMSG_CTL_REMOVE);
	CASE(IMSG_CTL_SCHEDULE);
	CASE(IMSG_CTL_SHOW_STATUS);
	CASE(IMSG_CTL_TRACE_DISABLE);
	CASE(IMSG_CTL_TRACE_ENABLE);
	CASE(IMSG_CTL_UPDATE_TABLE);
	CASE(IMSG_CTL_VERBOSE);
	CASE(IMSG_CTL_DISCOVER_EVPID);
	CASE(IMSG_CTL_DISCOVER_MSGID);

	CASE(IMSG_CTL_SMTP_SESSION);

	CASE(IMSG_GETADDRINFO);
	CASE(IMSG_GETADDRINFO_END);
	CASE(IMSG_GETNAMEINFO);
	CASE(IMSG_RES_QUERY);

	CASE(IMSG_CERT_INIT);
	CASE(IMSG_CERT_CERTIFICATE);
	CASE(IMSG_CERT_VERIFY);

	CASE(IMSG_SETUP_KEY);
	CASE(IMSG_SETUP_PEER);
	CASE(IMSG_SETUP_DONE);

	CASE(IMSG_CONF_START);
	CASE(IMSG_CONF_END);

	CASE(IMSG_STAT_INCREMENT);
	CASE(IMSG_STAT_DECREMENT);
	CASE(IMSG_STAT_SET);

	CASE(IMSG_LKA_AUTHENTICATE);
	CASE(IMSG_LKA_OPEN_FORWARD);
	CASE(IMSG_LKA_ENVELOPE_SUBMIT);
	CASE(IMSG_LKA_ENVELOPE_COMMIT);

	CASE(IMSG_QUEUE_DELIVER);
	CASE(IMSG_QUEUE_DELIVERY_OK);
	CASE(IMSG_QUEUE_DELIVERY_TEMPFAIL);
	CASE(IMSG_QUEUE_DELIVERY_PERMFAIL);
	CASE(IMSG_QUEUE_DELIVERY_LOOP);
	CASE(IMSG_QUEUE_DISCOVER_EVPID);
	CASE(IMSG_QUEUE_DISCOVER_MSGID);
	CASE(IMSG_QUEUE_ENVELOPE_ACK);
	CASE(IMSG_QUEUE_ENVELOPE_COMMIT);
	CASE(IMSG_QUEUE_ENVELOPE_REMOVE);
	CASE(IMSG_QUEUE_ENVELOPE_SCHEDULE);
	CASE(IMSG_QUEUE_ENVELOPE_SUBMIT);
	CASE(IMSG_QUEUE_HOLDQ_HOLD);
	CASE(IMSG_QUEUE_HOLDQ_RELEASE);
	CASE(IMSG_QUEUE_MESSAGE_COMMIT);
	CASE(IMSG_QUEUE_MESSAGE_ROLLBACK);
	CASE(IMSG_QUEUE_SMTP_SESSION);
	CASE(IMSG_QUEUE_TRANSFER);

	CASE(IMSG_MDA_DELIVERY_OK);
	CASE(IMSG_MDA_DELIVERY_TEMPFAIL);
	CASE(IMSG_MDA_DELIVERY_PERMFAIL);
	CASE(IMSG_MDA_DELIVERY_LOOP);
	CASE(IMSG_MDA_DELIVERY_HOLD);
	CASE(IMSG_MDA_DONE);
	CASE(IMSG_MDA_FORK);
	CASE(IMSG_MDA_HOLDQ_RELEASE);
	CASE(IMSG_MDA_LOOKUP_USERINFO);
	CASE(IMSG_MDA_KILL);
	CASE(IMSG_MDA_OPEN_MESSAGE);

	CASE(IMSG_MTA_DELIVERY_OK);
	CASE(IMSG_MTA_DELIVERY_TEMPFAIL);
	CASE(IMSG_MTA_DELIVERY_PERMFAIL);
	CASE(IMSG_MTA_DELIVERY_LOOP);
	CASE(IMSG_MTA_DELIVERY_HOLD);
	CASE(IMSG_MTA_DNS_HOST);
	CASE(IMSG_MTA_DNS_HOST_END);
	CASE(IMSG_MTA_DNS_MX);
	CASE(IMSG_MTA_DNS_MX_PREFERENCE);
	CASE(IMSG_MTA_HOLDQ_RELEASE);
	CASE(IMSG_MTA_LOOKUP_CREDENTIALS);
	CASE(IMSG_MTA_LOOKUP_SOURCE);
	CASE(IMSG_MTA_LOOKUP_HELO);
	CASE(IMSG_MTA_LOOKUP_SMARTHOST);
	CASE(IMSG_MTA_OPEN_MESSAGE);
	CASE(IMSG_MTA_SCHEDULE);

	CASE(IMSG_SCHED_ENVELOPE_BOUNCE);
	CASE(IMSG_SCHED_ENVELOPE_DELIVER);
	CASE(IMSG_SCHED_ENVELOPE_EXPIRE);
	CASE(IMSG_SCHED_ENVELOPE_INJECT);
	CASE(IMSG_SCHED_ENVELOPE_REMOVE);
	CASE(IMSG_SCHED_ENVELOPE_TRANSFER);

	CASE(IMSG_SMTP_AUTHENTICATE);
	CASE(IMSG_SMTP_MESSAGE_COMMIT);
	CASE(IMSG_SMTP_MESSAGE_CREATE);
	CASE(IMSG_SMTP_MESSAGE_ROLLBACK);
	CASE(IMSG_SMTP_MESSAGE_OPEN);
	CASE(IMSG_SMTP_CHECK_SENDER);
	CASE(IMSG_SMTP_EXPAND_RCPT);
	CASE(IMSG_SMTP_LOOKUP_HELO);

	CASE(IMSG_SMTP_REQ_CONNECT);
	CASE(IMSG_SMTP_REQ_HELO);
	CASE(IMSG_SMTP_REQ_MAIL);
	CASE(IMSG_SMTP_REQ_RCPT);
	CASE(IMSG_SMTP_REQ_DATA);
	CASE(IMSG_SMTP_REQ_EOM);
	CASE(IMSG_SMTP_EVENT_RSET);
	CASE(IMSG_SMTP_EVENT_COMMIT);
	CASE(IMSG_SMTP_EVENT_ROLLBACK);
	CASE(IMSG_SMTP_EVENT_DISCONNECT);

	CASE(IMSG_LKA_PROCESSOR_FORK);
	CASE(IMSG_LKA_PROCESSOR_ERRFD);

	CASE(IMSG_REPORT_SMTP_LINK_CONNECT);
	CASE(IMSG_REPORT_SMTP_LINK_DISCONNECT);
	CASE(IMSG_REPORT_SMTP_LINK_TLS);

	CASE(IMSG_REPORT_SMTP_TX_RESET);
	CASE(IMSG_REPORT_SMTP_TX_BEGIN);
	CASE(IMSG_REPORT_SMTP_TX_ENVELOPE);
	CASE(IMSG_REPORT_SMTP_TX_COMMIT);
	CASE(IMSG_REPORT_SMTP_TX_ROLLBACK);

	CASE(IMSG_REPORT_SMTP_PROTOCOL_CLIENT);
	CASE(IMSG_REPORT_SMTP_PROTOCOL_SERVER);

	CASE(IMSG_FILTER_SMTP_BEGIN);
	CASE(IMSG_FILTER_SMTP_END);
	CASE(IMSG_FILTER_SMTP_PROTOCOL);
	CASE(IMSG_FILTER_SMTP_DATA_BEGIN);
	CASE(IMSG_FILTER_SMTP_DATA_END);

	CASE(IMSG_CA_RSA_PRIVENC);
	CASE(IMSG_CA_RSA_PRIVDEC);
	CASE(IMSG_CA_ECDSA_SIGN);
	default:
		(void)snprintf(buf, sizeof(buf), "IMSG_??? (%d)", type);

		return buf;
	}
}

#ifdef BSD_AUTH
int
parent_auth_bsd(const char *username, const char *password)
{
	char	user[LOGIN_NAME_MAX];
	char	pass[LINE_MAX];
	int	ret;

	(void)strlcpy(user, username, sizeof(user));
	(void)strlcpy(pass, password, sizeof(pass));

	ret = auth_userokay(user, NULL, "auth-smtp", pass);
	if (ret)
		return LKA_OK;
	return LKA_PERMFAIL;
}
#endif

#ifdef USE_PAM
int 
pam_conv_password(int num_msg, const struct pam_message **msg,
    struct pam_response **respp, void *password)
{
	struct pam_response *response;

	if (num_msg != 1)
		return PAM_CONV_ERR;

	response = calloc(1, sizeof(struct pam_response));
	if (response == NULL || (response->resp = strdup(password)) == NULL) {
		free(response);
		return PAM_BUF_ERR;
	}

	*respp = response;
	return PAM_SUCCESS;
}
int
parent_auth_pam(const char *username, const char *password)
{
	int rc;
	pam_handle_t *pamh = NULL;
	struct pam_conv conv = { pam_conv_password, (char *)password };

	if ((rc = pam_start(USE_PAM_SERVICE, username, &conv, &pamh)) != PAM_SUCCESS)
		goto end;
	if ((rc = pam_authenticate(pamh, 0)) != PAM_SUCCESS)
		goto end;
	if ((rc = pam_acct_mgmt(pamh, 0)) != PAM_SUCCESS)
		goto end;

end:
	pam_end(pamh, rc);
	
	switch (rc) {
		case PAM_SUCCESS:
			return LKA_OK;
		case PAM_SYSTEM_ERR:
		case PAM_ABORT:
		case PAM_AUTHINFO_UNAVAIL:
			return LKA_TEMPFAIL;
		default:
			return LKA_PERMFAIL;
	}
}
#endif

#ifdef HAVE_GETSPNAM
int
parent_auth_getspnam(const char *username, const char *password)
{
       struct spwd *pw;
       char *ep;

       errno = 0;
       do {
               pw = getspnam(username);
       } while (pw == NULL && errno == EINTR);

       if (pw == NULL) {
               if (errno)
                       return LKA_TEMPFAIL;
               return LKA_PERMFAIL;
       }

       if ((ep = crypt(password, pw->sp_pwdp)) == NULL)
	       return LKA_PERMFAIL;

       if (strcmp(pw->sp_pwdp, ep) == 0)
               return LKA_OK;

       return LKA_PERMFAIL;
}
#endif

int
parent_auth_pwd(const char *username, const char *password)
{
       struct passwd *pw;
       char *ep;

       errno = 0;
       do {
               pw = getpwnam(username);
       } while (pw == NULL && errno == EINTR);

       if (pw == NULL) {
               if (errno)
                       return LKA_TEMPFAIL;
               return LKA_PERMFAIL;
       }

       if ((ep = crypt(password, pw->pw_passwd)) == NULL)
	       return LKA_PERMFAIL;

       if (strcmp(pw->pw_passwd, ep) == 0)
               return LKA_OK;

       return LKA_PERMFAIL;
}

int
parent_auth_user(const char *username, const char *password)
{
#if defined(BSD_AUTH)
	return (parent_auth_bsd(username, password));
#elif defined(USE_PAM)
	return (parent_auth_pam(username, password));
#elif defined(HAVE_GETSPNAM)
	return (parent_auth_getspnam(username, password));
#else
	return (parent_auth_pwd(username, password));
#endif
}
