OpenBSD-4.6/usr.sbin/smtpd/smtpd.c
/* $OpenBSD: smtpd.c,v 1.75 2009/06/06 04:14:21 pyr Exp $ */
/*
* Copyright (c) 2008 Gilles Chehade <gilles@openbsd.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 <sys/types.h>
#include <sys/queue.h>
#include <sys/tree.h>
#include <sys/param.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include <sys/resource.h>
#include <sys/mman.h>
#include <bsd_auth.h>
#include <err.h>
#include <errno.h>
#include <event.h>
#include <fcntl.h>
#include <login_cap.h>
#include <paths.h>
#include <paths.h>
#include <pwd.h>
#include <regex.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sysexits.h>
#include <time.h>
#include <unistd.h>
#include <keynote.h>
#include "smtpd.h"
__dead void usage(void);
void parent_shutdown(struct smtpd *);
void parent_send_config(int, short, void *);
void parent_send_config_listeners(struct smtpd *);
void parent_send_config_client_certs(struct smtpd *);
void parent_send_config_ruleset(struct smtpd *, int);
void parent_dispatch_lka(int, short, void *);
void parent_dispatch_mda(int, short, void *);
void parent_dispatch_mfa(int, short, void *);
void parent_dispatch_mta(int, short, void *);
void parent_dispatch_smtp(int, short, void *);
void parent_dispatch_runner(int, short, void *);
void parent_dispatch_control(int, short, void *);
void parent_sig_handler(int, short, void *);
int parent_open_message_file(struct batch *);
int parent_mailbox_open(char *, struct passwd *, struct batch *);
int parent_filename_open(char *, struct passwd *, struct batch *);
int parent_mailfile_rename(struct batch *, struct path *);
int parent_maildir_open(char *, struct passwd *, struct batch *);
int parent_maildir_init(struct passwd *, char *);
int parent_external_mda(char *, struct passwd *, struct batch *);
int parent_enqueue_offline(struct smtpd *, char *);
int parent_forward_open(char *);
int setup_spool(uid_t, gid_t);
int path_starts_with(char *, char *);
void fork_peers(struct smtpd *);
void child_add(struct smtpd *, pid_t, int, int);
void child_del(struct smtpd *, pid_t);
struct child *child_lookup(struct smtpd *, pid_t);
extern char **environ;
int __b64_pton(char const *, unsigned char *, size_t);
__dead void
usage(void)
{
extern char *__progname;
fprintf(stderr, "usage: %s [-dnv] [-D macro=value] "
"[-f file]\n", __progname);
exit(1);
}
void
parent_shutdown(struct smtpd *env)
{
struct child *child;
pid_t pid;
SPLAY_FOREACH(child, childtree, &env->children)
if (child->type == CHILD_DAEMON)
kill(child->pid, SIGTERM);
do {
pid = waitpid(WAIT_MYPGRP, NULL, 0);
} while (pid != -1 || (pid == -1 && errno == EINTR));
log_warnx("parent terminating");
exit(0);
}
void
parent_send_config(int fd, short event, void *p)
{
parent_send_config_listeners(p);
parent_send_config_client_certs(p);
parent_send_config_ruleset(p, PROC_MFA);
parent_send_config_ruleset(p, PROC_LKA);
}
void
parent_send_config_listeners(struct smtpd *env)
{
struct listener *l;
struct ssl *s;
struct iovec iov[3];
int opt;
log_debug("parent_send_config: configuring smtp");
imsg_compose_event(env->sc_ievs[PROC_SMTP], IMSG_CONF_START,
0, 0, -1, NULL, 0);
SPLAY_FOREACH(s, ssltree, env->sc_ssl) {
if (!(s->flags & F_SCERT))
continue;
iov[0].iov_base = s;
iov[0].iov_len = sizeof(*s);
iov[1].iov_base = s->ssl_cert;
iov[1].iov_len = s->ssl_cert_len;
iov[2].iov_base = s->ssl_key;
iov[2].iov_len = s->ssl_key_len;
imsg_composev(&env->sc_ievs[PROC_SMTP]->ibuf,
IMSG_CONF_SSL, 0, 0, -1, iov, nitems(iov));
imsg_event_add(env->sc_ievs[PROC_SMTP]);
}
TAILQ_FOREACH(l, env->sc_listeners, entry) {
if ((l->fd = socket(l->ss.ss_family, SOCK_STREAM, 0)) == -1)
fatal("socket");
opt = 1;
if (setsockopt(l->fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0)
fatal("setsockopt");
if (bind(l->fd, (struct sockaddr *)&l->ss, l->ss.ss_len) == -1)
fatal("bind");
imsg_compose_event(env->sc_ievs[PROC_SMTP], IMSG_CONF_LISTENER,
0, 0, l->fd, l, sizeof(*l));
}
imsg_compose_event(env->sc_ievs[PROC_SMTP], IMSG_CONF_END,
0, 0, -1, NULL, 0);
}
void
parent_send_config_client_certs(struct smtpd *env)
{
struct ssl *s;
struct iovec iov[3];
log_debug("parent_send_config_client_certs: configuring smtp");
imsg_compose_event(env->sc_ievs[PROC_MTA], IMSG_CONF_START,
0, 0, -1, NULL, 0);
SPLAY_FOREACH(s, ssltree, env->sc_ssl) {
if (!(s->flags & F_CCERT))
continue;
iov[0].iov_base = s;
iov[0].iov_len = sizeof(*s);
iov[1].iov_base = s->ssl_cert;
iov[1].iov_len = s->ssl_cert_len;
iov[2].iov_base = s->ssl_key;
iov[2].iov_len = s->ssl_key_len;
imsg_composev(&env->sc_ievs[PROC_MTA]->ibuf, IMSG_CONF_SSL,
0, 0, -1, iov, nitems(iov));
imsg_event_add(env->sc_ievs[PROC_MTA]);
}
imsg_compose_event(env->sc_ievs[PROC_MTA], IMSG_CONF_END,
0, 0, -1, NULL, 0);
}
void
parent_send_config_ruleset(struct smtpd *env, int proc)
{
struct rule *r;
struct cond *cond;
struct map *m;
struct mapel *mapel;
log_debug("parent_send_config_ruleset: reloading rules and maps");
imsg_compose_event(env->sc_ievs[proc], IMSG_CONF_START,
0, 0, -1, NULL, 0);
TAILQ_FOREACH(m, env->sc_maps, m_entry) {
imsg_compose_event(env->sc_ievs[proc], IMSG_CONF_MAP,
0, 0, -1, m, sizeof(*m));
TAILQ_FOREACH(mapel, &m->m_contents, me_entry) {
imsg_compose_event(env->sc_ievs[proc], IMSG_CONF_MAP_CONTENT,
0, 0, -1, mapel, sizeof(*mapel));
}
}
TAILQ_FOREACH(r, env->sc_rules, r_entry) {
imsg_compose_event(env->sc_ievs[proc], IMSG_CONF_RULE,
0, 0, -1, r, sizeof(*r));
imsg_compose_event(env->sc_ievs[proc], IMSG_CONF_RULE_SOURCE,
0, 0, -1, &r->r_sources->m_name, sizeof(r->r_sources->m_name));
TAILQ_FOREACH(cond, &r->r_conditions, c_entry) {
imsg_compose_event(env->sc_ievs[proc], IMSG_CONF_CONDITION,
0, 0, -1, cond, sizeof(*cond));
}
}
imsg_compose_event(env->sc_ievs[proc], IMSG_CONF_END,
0, 0, -1, NULL, 0);
}
void
parent_dispatch_lka(int fd, short event, void *p)
{
struct smtpd *env = p;
struct imsgev *iev;
struct imsgbuf *ibuf;
struct imsg imsg;
ssize_t n;
iev = env->sc_ievs[PROC_LKA];
ibuf = &iev->ibuf;
if (event & EV_READ) {
if ((n = imsg_read(ibuf)) == -1)
fatal("imsg_read_error");
if (n == 0) {
/* this pipe is dead, so remove the event handler */
event_del(&iev->ev);
event_loopexit(NULL);
return;
}
}
if (event & EV_WRITE) {
if (msgbuf_write(&ibuf->w) == -1)
fatal("msgbuf_write");
}
for (;;) {
if ((n = imsg_get(ibuf, &imsg)) == -1)
fatalx("parent_dispatch_lka: imsg_get error");
if (n == 0)
break;
switch (imsg.hdr.type) {
case IMSG_PARENT_FORWARD_OPEN: {
struct forward_req *fwreq = imsg.data;
int ret;
IMSG_SIZE_CHECK(fwreq);
ret = parent_forward_open(fwreq->pw_name);
fwreq->status = 0;
if (ret == -1) {
if (errno == ENOENT)
fwreq->status = 1;
}
imsg_compose_event(iev, IMSG_PARENT_FORWARD_OPEN, 0, 0, ret, fwreq, sizeof(*fwreq));
break;
}
default:
log_warnx("parent_dispatch_lka: got imsg %d",
imsg.hdr.type);
fatalx("parent_dispatch_lka: unexpected imsg");
}
imsg_free(&imsg);
}
imsg_event_add(iev);
}
void
parent_dispatch_mfa(int fd, short event, void *p)
{
struct smtpd *env = p;
struct imsgev *iev;
struct imsgbuf *ibuf;
struct imsg imsg;
ssize_t n;
iev = env->sc_ievs[PROC_MFA];
ibuf = &iev->ibuf;
if (event & EV_READ) {
if ((n = imsg_read(ibuf)) == -1)
fatal("imsg_read_error");
if (n == 0) {
/* this pipe is dead, so remove the event handler */
event_del(&iev->ev);
event_loopexit(NULL);
return;
}
}
if (event & EV_WRITE) {
if (msgbuf_write(&ibuf->w) == -1)
fatal("msgbuf_write");
}
for (;;) {
if ((n = imsg_get(ibuf, &imsg)) == -1)
fatalx("parent_dispatch_mfa: imsg_get error");
if (n == 0)
break;
switch (imsg.hdr.type) {
default:
log_warnx("parent_dispatch_mfa: got imsg %d",
imsg.hdr.type);
fatalx("parent_dispatch_mfa: unexpected imsg");
}
imsg_free(&imsg);
}
imsg_event_add(iev);
}
void
parent_dispatch_mta(int fd, short event, void *p)
{
struct smtpd *env = p;
struct imsgev *iev;
struct imsgbuf *ibuf;
struct imsg imsg;
ssize_t n;
iev = env->sc_ievs[PROC_MTA];
ibuf = &iev->ibuf;
if (event & EV_READ) {
if ((n = imsg_read(ibuf)) == -1)
fatal("imsg_read_error");
if (n == 0) {
/* this pipe is dead, so remove the event handler */
event_del(&iev->ev);
event_loopexit(NULL);
return;
}
}
if (event & EV_WRITE) {
if (msgbuf_write(&ibuf->w) == -1)
fatal("msgbuf_write");
}
for (;;) {
if ((n = imsg_get(ibuf, &imsg)) == -1)
fatalx("parent_dispatch_mta: imsg_get error");
if (n == 0)
break;
switch (imsg.hdr.type) {
default:
log_warnx("parent_dispatch_mta: got imsg %d",
imsg.hdr.type);
fatalx("parent_dispatch_mta: unexpected imsg");
}
imsg_free(&imsg);
}
imsg_event_add(iev);
}
void
parent_dispatch_mda(int fd, short event, void *p)
{
struct smtpd *env = p;
struct imsgev *iev;
struct imsgbuf *ibuf;
struct imsg imsg;
ssize_t n;
iev = env->sc_ievs[PROC_MDA];
ibuf = &iev->ibuf;
if (event & EV_READ) {
if ((n = imsg_read(ibuf)) == -1)
fatal("imsg_read_error");
if (n == 0) {
/* this pipe is dead, so remove the event handler */
event_del(&iev->ev);
event_loopexit(NULL);
return;
}
}
if (event & EV_WRITE) {
if (msgbuf_write(&ibuf->w) == -1)
fatal("msgbuf_write");
}
for (;;) {
if ((n = imsg_get(ibuf, &imsg)) == -1)
fatalx("parent_dispatch_mda: imsg_get error");
if (n == 0)
break;
switch (imsg.hdr.type) {
case IMSG_PARENT_MAILBOX_OPEN: {
struct batch *batchp = imsg.data;
struct path *path;
struct passwd *pw;
char *pw_name;
char *file;
u_int8_t i;
int desc;
struct action_handler {
enum action_type action;
int (*handler)(char *, struct passwd *, struct batch *);
} action_hdl_table[] = {
{ A_MBOX, parent_mailbox_open },
{ A_MAILDIR, parent_maildir_open },
{ A_EXT, parent_external_mda },
{ A_FILENAME, parent_filename_open }
};
IMSG_SIZE_CHECK(batchp);
path = &batchp->message.recipient;
if (batchp->type & T_DAEMON_BATCH) {
path = &batchp->message.sender;
}
for (i = 0; i < nitems(action_hdl_table); ++i)
if (action_hdl_table[i].action == path->rule.r_action)
break;
if (i == nitems(action_hdl_table))
fatalx("parent_dispatch_mda: unknown action");
file = path->rule.r_value.path;
pw_name = path->pw_name;
if (path->rule.r_action == A_FILENAME) {
file = path->u.filename;
pw_name = SMTPD_USER;
}
errno = 0;
pw = getpwnam(pw_name);
if (pw == NULL) {
if (errno)
batchp->message.status |= S_MESSAGE_TEMPFAILURE;
else
batchp->message.status |= S_MESSAGE_PERMFAILURE;
imsg_compose_event(iev, IMSG_MDA_MAILBOX_FILE, 0, 0,
-1, batchp, sizeof(struct batch));
break;
}
if (setegid(pw->pw_gid) || seteuid(pw->pw_uid))
fatal("privdrop failed");
desc = action_hdl_table[i].handler(file, pw, batchp);
imsg_compose_event(iev, IMSG_MDA_MAILBOX_FILE, 0, 0,
desc, batchp, sizeof(struct batch));
if (setegid(0) || seteuid(0))
fatal("privdrop failed");
break;
}
case IMSG_PARENT_MESSAGE_OPEN: {
struct batch *batchp = imsg.data;
int desc;
IMSG_SIZE_CHECK(batchp);
desc = parent_open_message_file(batchp);
imsg_compose_event(iev, IMSG_MDA_MESSAGE_FILE, 0, 0,
desc, batchp, sizeof(struct batch));
break;
}
case IMSG_PARENT_MAILBOX_RENAME: {
struct batch *batchp = imsg.data;
struct path *path;
struct passwd *pw;
IMSG_SIZE_CHECK(batchp);
path = &batchp->message.recipient;
if (batchp->type & T_DAEMON_BATCH) {
path = &batchp->message.sender;
}
pw = getpwnam(path->pw_name);
if (pw == NULL)
break;
if (seteuid(pw->pw_uid) == -1)
fatal("privdrop failed");
parent_mailfile_rename(batchp, path);
if (seteuid(0) == -1)
fatal("privraise failed");
break;
}
default:
log_warnx("parent_dispatch_mfa: got imsg %d",
imsg.hdr.type);
fatalx("parent_dispatch_mda: unexpected imsg");
}
imsg_free(&imsg);
}
imsg_event_add(iev);
}
void
parent_dispatch_smtp(int fd, short event, void *p)
{
struct smtpd *env = p;
struct imsgev *iev;
struct imsgbuf *ibuf;
struct imsg imsg;
ssize_t n;
iev = env->sc_ievs[PROC_SMTP];
ibuf = &iev->ibuf;
if (event & EV_READ) {
if ((n = imsg_read(ibuf)) == -1)
fatal("imsg_read_error");
if (n == 0) {
/* this pipe is dead, so remove the event handler */
event_del(&iev->ev);
event_loopexit(NULL);
return;
}
}
if (event & EV_WRITE) {
if (msgbuf_write(&ibuf->w) == -1)
fatal("msgbuf_write");
}
for (;;) {
if ((n = imsg_get(ibuf, &imsg)) == -1)
fatalx("parent_dispatch_smtp: imsg_get error");
if (n == 0)
break;
switch (imsg.hdr.type) {
case IMSG_PARENT_SEND_CONFIG: {
parent_send_config_listeners(env);
break;
}
case IMSG_PARENT_AUTHENTICATE: {
struct auth *req = imsg.data;
IMSG_SIZE_CHECK(req);
req->success = auth_userokay(req->user, NULL,
"auth-smtp", req->pass);
imsg_compose_event(iev, IMSG_PARENT_AUTHENTICATE, 0, 0,
-1, req, sizeof(*req));
break;
}
default:
log_warnx("parent_dispatch_smtp: got imsg %d",
imsg.hdr.type);
fatalx("parent_dispatch_smtp: unexpected imsg");
}
imsg_free(&imsg);
}
imsg_event_add(iev);
}
void
parent_dispatch_runner(int sig, short event, void *p)
{
struct smtpd *env = p;
struct imsgev *iev;
struct imsgbuf *ibuf;
struct imsg imsg;
ssize_t n;
iev = env->sc_ievs[PROC_RUNNER];
ibuf = &iev->ibuf;
if (event & EV_READ) {
if ((n = imsg_read(ibuf)) == -1)
fatal("imsg_read_error");
if (n == 0) {
/* this pipe is dead, so remove the event handler */
event_del(&iev->ev);
event_loopexit(NULL);
return;
}
}
if (event & EV_WRITE) {
if (msgbuf_write(&ibuf->w) == -1)
fatal("msgbuf_write");
}
for (;;) {
if ((n = imsg_get(ibuf, &imsg)) == -1)
fatalx("parent_dispatch_runner: imsg_get error");
if (n == 0)
break;
switch (imsg.hdr.type) {
case IMSG_PARENT_ENQUEUE_OFFLINE:
if (! parent_enqueue_offline(env, imsg.data))
imsg_compose_event(iev, IMSG_PARENT_ENQUEUE_OFFLINE,
0, 0, -1, NULL, 0);
break;
default:
log_warnx("parent_dispatch_runner: got imsg %d",
imsg.hdr.type);
fatalx("parent_dispatch_runner: unexpected imsg");
}
imsg_free(&imsg);
}
imsg_event_add(iev);
}
void
parent_dispatch_control(int sig, short event, void *p)
{
struct smtpd *env = p;
struct imsgev *iev;
struct imsgbuf *ibuf;
struct imsg imsg;
ssize_t n;
iev = env->sc_ievs[PROC_CONTROL];
ibuf = &iev->ibuf;
if (event & EV_READ) {
if ((n = imsg_read(ibuf)) == -1)
fatal("imsg_read_error");
if (n == 0) {
/* this pipe is dead, so remove the event handler */
event_del(&iev->ev);
event_loopexit(NULL);
return;
}
}
if (event & EV_WRITE) {
if (msgbuf_write(&ibuf->w) == -1)
fatal("msgbuf_write");
}
for (;;) {
if ((n = imsg_get(ibuf, &imsg)) == -1)
fatalx("parent_dispatch_control: imsg_get error");
if (n == 0)
break;
switch (imsg.hdr.type) {
case IMSG_CONF_RELOAD: {
struct reload *r = imsg.data;
struct smtpd newenv;
r->ret = 0;
if (parse_config(&newenv, env->sc_conffile, 0) == 0) {
(void)strlcpy(env->sc_hostname, newenv.sc_hostname,
sizeof(env->sc_hostname));
env->sc_listeners = newenv.sc_listeners;
env->sc_maps = newenv.sc_maps;
env->sc_rules = newenv.sc_rules;
env->sc_rules = newenv.sc_rules;
env->sc_ssl = newenv.sc_ssl;
parent_send_config_client_certs(env);
parent_send_config_ruleset(env, PROC_MFA);
parent_send_config_ruleset(env, PROC_LKA);
imsg_compose_event(env->sc_ievs[PROC_SMTP],
IMSG_CONF_RELOAD, 0, 0, -1, NULL, 0);
r->ret = 1;
}
imsg_compose_event(iev, IMSG_CONF_RELOAD, 0, 0, -1, r, sizeof(*r));
break;
}
default:
log_warnx("parent_dispatch_control: got imsg %d",
imsg.hdr.type);
fatalx("parent_dispatch_control: unexpected imsg");
}
imsg_free(&imsg);
}
imsg_event_add(iev);
}
void
parent_sig_handler(int sig, short event, void *p)
{
struct smtpd *env = p;
struct child *child;
int die = 0, status, fail;
pid_t pid;
char *cause;
switch (sig) {
case SIGTERM:
case SIGINT:
die = 1;
/* FALLTHROUGH */
case SIGCHLD:
do {
pid = waitpid(-1, &status, WNOHANG);
if (pid <= 0)
continue;
child = child_lookup(env, pid);
if (child == NULL)
fatalx("unexpected SIGCHLD");
fail = 0;
if (WIFSIGNALED(status)) {
fail = 1;
asprintf(&cause, "terminated; signal %d",
WTERMSIG(status));
} else if (WIFEXITED(status)) {
if (WEXITSTATUS(status) != 0) {
fail = 1;
asprintf(&cause, "exited abnormally");
} else
asprintf(&cause, "exited okay");
} else
fatalx("unexpected cause of SIGCHLD");
switch (child->type) {
case CHILD_DAEMON:
die = 1;
if (fail)
log_warnx("lost child: %s %s",
env->sc_title[child->title], cause);
break;
case CHILD_MDA:
if (fail)
log_warnx("external mda %s", cause);
else
log_debug("external mda %s", cause);
break;
case CHILD_ENQUEUE_OFFLINE:
if (fail)
log_warnx("couldn't enqueue offline "
"message; smtpctl %s", cause);
else
log_debug("offline message enqueued");
imsg_compose_event(env->sc_ievs[PROC_RUNNER],
IMSG_PARENT_ENQUEUE_OFFLINE, 0, 0, -1,
NULL, 0);
break;
default:
fatalx("unexpected child type");
}
child_del(env, child->pid);
free(cause);
} while (pid > 0 || (pid == -1 && errno == EINTR));
if (die)
parent_shutdown(env);
break;
default:
fatalx("unexpected signal");
}
}
int
main(int argc, char *argv[])
{
int c;
int debug;
int opts;
const char *conffile = CONF_FILE;
struct smtpd env;
struct event ev_sigint;
struct event ev_sigterm;
struct event ev_sigchld;
struct event ev_sighup;
struct timeval tv;
struct rlimit rl;
struct peer peers[] = {
{ PROC_CONTROL, parent_dispatch_control },
{ PROC_LKA, parent_dispatch_lka },
{ PROC_MDA, parent_dispatch_mda },
{ PROC_MFA, parent_dispatch_mfa },
{ PROC_MTA, parent_dispatch_mta },
{ PROC_SMTP, parent_dispatch_smtp },
{ PROC_RUNNER, parent_dispatch_runner }
};
opts = 0;
debug = 0;
log_init(1);
while ((c = getopt(argc, argv, "dD:nf:v")) != -1) {
switch (c) {
case 'd':
debug = 2;
break;
case 'D':
if (cmdline_symset(optarg) < 0)
log_warnx("could not parse macro definition %s",
optarg);
break;
case 'n':
debug = 2;
opts |= SMTPD_OPT_NOACTION;
break;
case 'f':
conffile = optarg;
break;
case 'v':
opts |= SMTPD_OPT_VERBOSE;
break;
default:
usage();
}
}
argv += optind;
argc -= optind;
if (parse_config(&env, conffile, opts))
exit(1);
if (strlcpy(env.sc_conffile, conffile, MAXPATHLEN) >= MAXPATHLEN)
errx(1, "config file exceeds MAXPATHLEN");
if (env.sc_opts & SMTPD_OPT_NOACTION) {
fprintf(stderr, "configuration OK\n");
exit(0);
}
/* check for root privileges */
if (geteuid())
errx(1, "need root privileges");
if ((env.sc_pw = getpwnam(SMTPD_USER)) == NULL)
errx(1, "unknown user %s", SMTPD_USER);
if (!setup_spool(env.sc_pw->pw_uid, 0))
errx(1, "invalid directory permissions");
log_init(debug);
if (!debug)
if (daemon(0, 0) == -1)
err(1, "failed to daemonize");
log_info("startup%s", (debug > 1)?" [debug mode]":"");
env.stats = mmap(NULL, sizeof(struct stats), PROT_WRITE|PROT_READ,
MAP_ANON|MAP_SHARED, -1, (off_t)0);
if (env.stats == MAP_FAILED)
fatal("mmap");
bzero(env.stats, sizeof(struct stats));
env.stats->parent.start = time(NULL);
if (getrlimit(RLIMIT_NOFILE, &rl) == -1)
fatal("smtpd: failed to get resource limit");
log_debug("smtpd: max open files %lld", rl.rlim_max);
/*
* Allow the maximum number of open file descriptors for this
* login class (which should be the class "daemon" by default).
*/
rl.rlim_cur = rl.rlim_max;
if (setrlimit(RLIMIT_NOFILE, &rl) == -1)
fatal("smtpd: failed to set resource limit");
env.sc_maxconn = (rl.rlim_cur / 4) * 3;
log_debug("smtpd: will accept at most %d clients", env.sc_maxconn);
fork_peers(&env);
event_init();
signal_set(&ev_sigint, SIGINT, parent_sig_handler, &env);
signal_set(&ev_sigterm, SIGTERM, parent_sig_handler, &env);
signal_set(&ev_sigchld, SIGCHLD, parent_sig_handler, &env);
signal_set(&ev_sighup, SIGHUP, parent_sig_handler, &env);
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_pipes(&env, peers, nitems(peers));
config_peers(&env, peers, nitems(peers));
evtimer_set(&env.sc_ev, parent_send_config, &env);
bzero(&tv, sizeof(tv));
evtimer_add(&env.sc_ev, &tv);
event_dispatch();
return (0);
}
void
fork_peers(struct smtpd *env)
{
SPLAY_INIT(&env->children);
env->sc_instances[PROC_CONTROL] = 1;
env->sc_instances[PROC_LKA] = 1;
env->sc_instances[PROC_MDA] = 1;
env->sc_instances[PROC_MFA] = 1;
env->sc_instances[PROC_MTA] = 1;
env->sc_instances[PROC_PARENT] = 1;
env->sc_instances[PROC_QUEUE] = 1;
env->sc_instances[PROC_RUNNER] = 1;
env->sc_instances[PROC_SMTP] = 1;
init_pipes(env);
env->sc_title[PROC_CONTROL] = "control";
env->sc_title[PROC_LKA] = "lookup agent";
env->sc_title[PROC_MDA] = "mail delivery agent";
env->sc_title[PROC_MFA] = "mail filter agent";
env->sc_title[PROC_MTA] = "mail transfer agent";
env->sc_title[PROC_QUEUE] = "queue";
env->sc_title[PROC_RUNNER] = "runner";
env->sc_title[PROC_SMTP] = "smtp server";
child_add(env, control(env), CHILD_DAEMON, PROC_CONTROL);
child_add(env, lka(env), CHILD_DAEMON, PROC_LKA);
child_add(env, mda(env), CHILD_DAEMON, PROC_MDA);
child_add(env, mfa(env), CHILD_DAEMON, PROC_MFA);
child_add(env, mta(env), CHILD_DAEMON, PROC_MTA);
child_add(env, queue(env), CHILD_DAEMON, PROC_QUEUE);
child_add(env, runner(env), CHILD_DAEMON, PROC_RUNNER);
child_add(env, smtp(env), CHILD_DAEMON, PROC_SMTP);
}
void
child_add(struct smtpd *env, pid_t pid, int type, int title)
{
struct child *child;
if ((child = calloc(1, sizeof(*child))) == NULL)
fatal(NULL);
child->pid = pid;
child->type = type;
child->title = title;
if (SPLAY_INSERT(childtree, &env->children, child) != NULL)
fatalx("child_add: double insert");
}
void
child_del(struct smtpd *env, pid_t pid)
{
struct child *p;
p = child_lookup(env, pid);
if (p == NULL)
fatalx("child_del: unknown child");
if (SPLAY_REMOVE(childtree, &env->children, p) == NULL)
fatalx("child_del: tree remove failed");
free(p);
}
struct child *
child_lookup(struct smtpd *env, pid_t pid)
{
struct child key;
key.pid = pid;
return SPLAY_FIND(childtree, &env->children, &key);
}
int
setup_spool(uid_t uid, gid_t gid)
{
unsigned int n;
char *paths[] = { PATH_INCOMING, PATH_ENQUEUE, PATH_QUEUE,
PATH_RUNQUEUE, PATH_RUNQUEUELOW,
PATH_RUNQUEUEHIGH, PATH_PURGE,
PATH_OFFLINE };
char pathname[MAXPATHLEN];
struct stat sb;
int ret;
if (! bsnprintf(pathname, sizeof(pathname), "%s", PATH_SPOOL))
fatal("snprintf");
if (stat(pathname, &sb) == -1) {
if (errno != ENOENT) {
warn("stat: %s", pathname);
return 0;
}
if (mkdir(pathname, 0711) == -1) {
warn("mkdir: %s", pathname);
return 0;
}
if (chown(pathname, 0, 0) == -1) {
warn("chown: %s", pathname);
return 0;
}
if (stat(pathname, &sb) == -1)
err(1, "stat: %s", pathname);
}
/* check if it's a directory */
if (!S_ISDIR(sb.st_mode)) {
warnx("%s is not a directory", pathname);
return 0;
}
/* check that it is owned by uid/gid */
if (sb.st_uid != 0 || sb.st_gid != 0) {
warnx("%s must be owned by root:wheel", pathname);
return 0;
}
/* check permission */
if ((sb.st_mode & (S_IRUSR|S_IWUSR|S_IXUSR)) != (S_IRUSR|S_IWUSR|S_IXUSR) ||
(sb.st_mode & (S_IRGRP|S_IWGRP|S_IXGRP)) != S_IXGRP ||
(sb.st_mode & (S_IROTH|S_IWOTH|S_IXOTH)) != S_IXOTH) {
warnx("%s must be rwx--x--x (0711)", pathname);
return 0;
}
ret = 1;
for (n = 0; n < nitems(paths); n++) {
mode_t mode;
uid_t owner;
gid_t group;
if (paths[n] == PATH_OFFLINE) {
mode = 01777;
owner = 0;
group = 0;
} else {
mode = 0700;
owner = uid;
group = gid;
}
if (! bsnprintf(pathname, sizeof(pathname), "%s%s", PATH_SPOOL,
paths[n]))
fatal("snprintf");
if (stat(pathname, &sb) == -1) {
if (errno != ENOENT) {
warn("stat: %s", pathname);
ret = 0;
continue;
}
/* chmod is deffered to avoid umask effect */
if (mkdir(pathname, 0) == -1) {
ret = 0;
warn("mkdir: %s", pathname);
}
if (chown(pathname, owner, group) == -1) {
ret = 0;
warn("chown: %s", pathname);
}
if (chmod(pathname, mode) == -1) {
ret = 0;
warn("chmod: %s", pathname);
}
if (stat(pathname, &sb) == -1)
err(1, "stat: %s", pathname);
}
/* check if it's a directory */
if (!S_ISDIR(sb.st_mode)) {
ret = 0;
warnx("%s is not a directory", pathname);
}
/* check that it is owned by owner/group */
if (sb.st_uid != owner) {
ret = 0;
warnx("%s is not owned by uid %d", pathname, owner);
}
if (sb.st_gid != group) {
ret = 0;
warnx("%s is not owned by gid %d", pathname, group);
}
/* check permission */
if ((sb.st_mode & 07777) != mode) {
char mode_str[12];
ret = 0;
strmode(mode, mode_str);
mode_str[10] = '\0';
warnx("%s must be %s (%o)", pathname, mode_str + 1, mode);
}
}
return ret;
}
void
imsg_event_add(struct imsgev *iev)
{
if (iev->handler == NULL) {
imsg_flush(&iev->ibuf);
return;
}
iev->events = EV_READ;
if (iev->ibuf.w.queued)
iev->events |= EV_WRITE;
event_del(&iev->ev);
event_set(&iev->ev, iev->ibuf.fd, iev->events, iev->handler, iev->data);
event_add(&iev->ev, NULL);
}
int
imsg_compose_event(struct imsgev *iev, u_int16_t type, u_int32_t peerid,
pid_t pid, int fd, void *data, u_int16_t datalen)
{
int ret;
if ((ret = imsg_compose(&iev->ibuf, type, peerid,
pid, fd, data, datalen)) != -1)
imsg_event_add(iev);
return (ret);
}
int
parent_open_message_file(struct batch *batchp)
{
int fd;
char pathname[MAXPATHLEN];
u_int16_t hval;
struct message *messagep;
messagep = &batchp->message;
hval = queue_hash(messagep->message_id);
if (! bsnprintf(pathname, sizeof(pathname), "%s%s/%d/%s/message",
PATH_SPOOL, PATH_QUEUE, hval, batchp->message_id)) {
batchp->message.status |= S_MESSAGE_PERMFAILURE;
return -1;
}
fd = open(pathname, O_RDONLY);
return fd;
}
int
parent_mailbox_open(char *path, struct passwd *pw, struct batch *batchp)
{
pid_t pid;
int pipefd[2];
char sender[MAX_PATH_SIZE];
/* This can never happen, but better safe than sorry. */
if (! bsnprintf(sender, MAX_PATH_SIZE, "%s@%s",
batchp->message.sender.user,
batchp->message.sender.domain)) {
batchp->message.status |= S_MESSAGE_PERMFAILURE;
return -1;
}
log_debug("executing mail.local");
if (socketpair(AF_UNIX, SOCK_STREAM, 0, pipefd) == -1) {
batchp->message.status |= S_MESSAGE_PERMFAILURE;
return -1;
}
/* raise privileges because mail.local needs root to
* deliver to user mailboxes.
*/
if (seteuid(0) == -1)
fatal("privraise failed");
pid = fork();
if (pid == -1) {
close(pipefd[0]);
close(pipefd[1]);
batchp->message.status |= S_MESSAGE_PERMFAILURE;
return -1;
}
if (pid == 0) {
close(pipefd[0]);
close(STDOUT_FILENO);
close(STDERR_FILENO);
dup2(pipefd[1], 0);
execlp(PATH_MAILLOCAL, "mail.local", "-f", sender, pw->pw_name, (void *)NULL);
_exit(1);
}
if (seteuid(pw->pw_uid) == -1)
fatal("privdrop failed");
child_add(batchp->env, pid, CHILD_MDA, -1);
close(pipefd[1]);
return pipefd[0];
}
int
parent_maildir_init(struct passwd *pw, char *root)
{
u_int8_t i;
char pathname[MAXPATHLEN];
char *subdir[] = { "/", "/tmp", "/cur", "/new" };
for (i = 0; i < nitems(subdir); ++i) {
if (! bsnprintf(pathname, sizeof(pathname), "%s%s", root,
subdir[i]))
return 0;
if (mkdir(pathname, 0700) == -1)
if (errno != EEXIST)
return 0;
}
return 1;
}
int
parent_maildir_open(char *path, struct passwd *pw, struct batch *batchp)
{
int fd;
char tmp[MAXPATHLEN];
int mode = O_CREAT|O_RDWR|O_TRUNC|O_SYNC;
if (! parent_maildir_init(pw, path)) {
batchp->message.status |= S_MESSAGE_TEMPFAILURE;
return -1;
}
if (! bsnprintf(tmp, sizeof(tmp), "%s/tmp/%s", path,
batchp->message.message_uid)) {
batchp->message.status |= S_MESSAGE_TEMPFAILURE;
return -1;
}
fd = open(tmp, mode, 0600);
if (fd == -1) {
batchp->message.status |= S_MESSAGE_TEMPFAILURE;
return -1;
}
return fd;
}
int
parent_mailfile_rename(struct batch *batchp, struct path *path)
{
char srcpath[MAXPATHLEN];
char dstpath[MAXPATHLEN];
if (! bsnprintf(srcpath, sizeof(srcpath), "%s/tmp/%s",
path->rule.r_value.path, batchp->message.message_uid) ||
! bsnprintf(dstpath, sizeof(dstpath), "%s/new/%s",
path->rule.r_value.path, batchp->message.message_uid))
return 0;
if (rename(srcpath, dstpath) == -1) {
if (unlink(srcpath) == -1)
fatal("unlink");
batchp->message.status |= S_MESSAGE_TEMPFAILURE;
return 0;
}
return 1;
}
int
parent_external_mda(char *path, struct passwd *pw, struct batch *batchp)
{
pid_t pid;
int pipefd[2];
arglist args;
char *word;
char *envp[2];
log_debug("executing filter as user: %s", pw->pw_name);
if (pipe(pipefd) == -1) {
if (errno == ENFILE) {
log_warn("parent_external_mda: pipe");
batchp->message.status |= S_MESSAGE_TEMPFAILURE;
return -1;
}
fatal("parent_external_mda: pipe");
}
pid = fork();
if (pid == -1) {
log_warn("parent_external_mda: fork");
close(pipefd[0]);
close(pipefd[1]);
batchp->message.status |= S_MESSAGE_TEMPFAILURE;
return -1;
}
if (pid == 0) {
if (seteuid(0) == -1)
fatal("privraise failed");
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))
fatal("cannot drop privileges");
bzero(&args, sizeof(args));
while ((word = strsep(&path, " \t")) != NULL)
if (*word != '\0')
addargs(&args, "%s", word);
if (setsid() == -1)
fatal("setsid");
if (signal(SIGPIPE, SIG_DFL) == SIG_ERR)
fatal("signal");
if (dup2(pipefd[0], STDIN_FILENO) == -1)
fatal("dup2");
if (chdir(pw->pw_dir) == -1 && chdir("/") == -1)
fatal("chdir");
if (closefrom(STDERR_FILENO + 1) == -1)
fatal("closefrom");
envp[0] = "PATH=" _PATH_DEFPATH;
envp[1] = (char *)NULL;
environ = envp;
execvp(args.list[0], args.list);
_exit(1);
}
child_add(batchp->env, pid, CHILD_MDA, -1);
close(pipefd[0]);
return pipefd[1];
}
int
parent_enqueue_offline(struct smtpd *env, char *runner_path)
{
char path[MAXPATHLEN];
struct passwd *pw;
struct stat sb;
pid_t pid;
log_debug("parent_enqueue_offline: path %s", runner_path);
if (! bsnprintf(path, sizeof(path), "%s%s", PATH_SPOOL, runner_path))
fatalx("parent_enqueue_offline: filename too long");
if (! path_starts_with(path, PATH_SPOOL PATH_OFFLINE))
fatalx("parent_enqueue_offline: path outside offline dir");
if (lstat(path, &sb) == -1) {
if (errno == ENOENT) {
log_warn("parent_enqueue_offline: %s", path);
return (0);
}
fatal("parent_enqueue_offline: lstat");
}
if (chflags(path, 0) == -1) {
if (errno == ENOENT) {
log_warn("parent_enqueue_offline: %s", path);
return (0);
}
fatal("parent_enqueue_offline: chflags");
}
errno = 0;
if ((pw = getpwuid(sb.st_uid)) == NULL) {
log_warn("parent_enqueue_offline: getpwuid for uid %d failed",
sb.st_uid);
unlink(path);
return (0);
}
if (! S_ISREG(sb.st_mode)) {
log_warnx("file %s (uid %d) not regular, removing", path, sb.st_uid);
if (S_ISDIR(sb.st_mode))
rmdir(path);
else
unlink(path);
return (0);
}
if ((pid = fork()) == -1)
fatal("parent_enqueue_offline: fork");
if (pid == 0) {
char *envp[2], *p, *tmp;
FILE *fp;
size_t len;
arglist args;
bzero(&args, sizeof(args));
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) ||
closefrom(STDERR_FILENO + 1) == -1) {
unlink(path);
_exit(1);
}
if ((fp = fopen(path, "r")) == NULL) {
unlink(path);
_exit(1);
}
unlink(path);
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 ((p = fgetln(fp, &len)) == NULL)
_exit(1);
if (p[len - 1] != '\n')
_exit(1);
p[len - 1] = '\0';
addargs(&args, "%s", "sendmail");
while ((tmp = strsep(&p, "|")) != NULL)
addargs(&args, "%s", tmp);
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);
}
child_add(env, pid, CHILD_ENQUEUE_OFFLINE, -1);
return (1);
}
int
parent_filename_open(char *path, struct passwd *pw, struct batch *batchp)
{
int fd;
int mode = O_CREAT|O_APPEND|O_RDWR|O_SYNC|O_NONBLOCK;
fd = open(path, mode, 0600);
if (fd == -1) {
/* XXX - this needs to be discussed ... */
switch (errno) {
case ENOTDIR:
case ENOENT:
case EACCES:
case ELOOP:
case EROFS:
case EDQUOT:
case EINTR:
case EIO:
case EMFILE:
case ENFILE:
case ENOSPC:
batchp->message.status |= S_MESSAGE_TEMPFAILURE;
break;
case EWOULDBLOCK:
goto lockfail;
default:
batchp->message.status |= S_MESSAGE_PERMFAILURE;
}
return -1;
}
if (flock(fd, LOCK_EX|LOCK_NB) == -1) {
if (errno == EWOULDBLOCK)
goto lockfail;
fatal("flock");
}
return fd;
lockfail:
if (fd != -1)
close(fd);
batchp->message.status |= S_MESSAGE_TEMPFAILURE|S_MESSAGE_LOCKFAILURE;
return -1;
}
int
parent_forward_open(char *username)
{
struct passwd *pw;
struct stat sb;
char pathname[MAXPATHLEN];
int fd;
pw = getpwnam(username);
if (pw == NULL)
return -1;
if (! bsnprintf(pathname, sizeof (pathname), "%s/.forward", pw->pw_dir))
return -1;
fd = open(pathname, O_RDONLY);
if (fd == -1) {
if (errno == ENOENT)
goto err;
return -1;
}
/* make sure ~/ is not writable by anyone but owner */
if (stat(pw->pw_dir, &sb) == -1)
goto errlog;
if (sb.st_uid != pw->pw_uid || sb.st_mode & (S_IWGRP|S_IWOTH))
goto errlog;
/* make sure ~/.forward is not writable by anyone but owner */
if (fstat(fd, &sb) == -1)
goto errlog;
if (sb.st_uid != pw->pw_uid || sb.st_mode & (S_IWGRP|S_IWOTH))
goto errlog;
return fd;
errlog:
log_info("cannot process forward file for user %s due to wrong permissions", username);
err:
return -1;
}
int
path_starts_with(char *file, char *prefix)
{
char rprefix[MAXPATHLEN];
char rfile[MAXPATHLEN];
if (realpath(file, rfile) == NULL || realpath(prefix, rprefix) == NULL)
return (-1);
return (strncmp(rfile, rprefix, strlen(rprefix)) == 0);
}
int
child_cmp(struct child *c1, struct child *c2)
{
if (c1->pid < c2->pid)
return (-1);
if (c1->pid > c2->pid)
return (1);
return (0);
}
SPLAY_GENERATE(childtree, child, entry, child_cmp);