diff options
Diffstat (limited to 'usr.sbin')
29 files changed, 12204 insertions, 0 deletions
diff --git a/usr.sbin/smtpd/ARCHITECTURE b/usr.sbin/smtpd/ARCHITECTURE new file mode 100644 index 00000000000..608d7d521a6 --- /dev/null +++ b/usr.sbin/smtpd/ARCHITECTURE @@ -0,0 +1,105 @@ ++==============================+ +|The SMTP Daemon Architecture | ++==============================+ + +Introduction +------------ + +The SMTP daemon is a complete mail architecture designed to provide +an MTA and MDA for OpenBSD systems (and others !) + +The SMTP daemon is configurable through a simple pf-like syntax to +select external or local delivery and the means to do so. + +The SMTP Daemon relies on a fully asynchronous and event based data +transfer model. The IMSG pseudo-protocol is used throughout the daemon +to provide communications between separate privilege-separated +processes. + +The following processes are currently implemented: + + * smtp: The unpriviledged SMTP server + * mfa: Mail filter agent + * queue: A queue scheduler + * mda: Local mail delivery agent + * mta: Remote mail delivery agent + * lka: Lookup agent, handles DNS, db, file and external maps + * control: External interaction gateway + +The SMTP Server +--------------- + +The SMTP Server conforms RFC 5321 and also provides interesting +extensions such as STARTTLS (RFC 2487). + + +The Mail Filter Agent +--------------------- + +The mail filter agent has two roles. First it decides if a mail is +allowed to be delivered by the daemon, it then performs resolution +to determine the delivery method that applies to the mail, either +local or remote. The second duty is to perform optionnal modifications +on the envelope content. + + +WorkFlow +-------- + +The base structure that traverses most of smtpd's architecture is a +struct msg, the typical workflow is as show below from left to right: + ++------+ +-----+ +| SMTP |======+ +===| MDA | ++------+ | +-----+ +-------+ | +-----+ + |=====| MFA |=====| Queue |=====| + +-----+ +-------+ | +-----+ + || +===| MTA | + +-----+ +-----+ + | LKA | + +-----+ + + +Client -> Server workflow: + +1- The message is initially received from either the listening service or + /usr/sbin/sendmail (for which smtpctl is a link). + +2- As envelope is gathered, the information is handed out to MFA for + validation. + +3- MFA takes decision as to wether or not the message can be delivered + locally or relayed out and notifies SMTP of its decision. + +4- If message is not rejected, a file descriptor is requested from QUEUE + by SMTP which will then write the message to the file descriptor. + +5- Once content is written, SMTP notifies QUEUE that the content is + fully written and provides a list of recipients. + +6- QUEUE constructs batches of messages with same message id and going + to same destination, and registers them to the batch queue. It then + writes the information on-disk so that it can recovert from a crash + or shutdown, and notifies SMTP that message is accepted. + +7- SMTP notifies client that message was accepted. + + +Queue workflow: + +1- A batch queue traversal is done. For each batch, QUEUE computes the + retry time based on the number of times it was tried already and + the time of last attempt. A batch may be in three states: + + a) it is either ready for processing, in which case it is + handed to MDA or MTA for delivery attempt. + + b) the delay before next retry has not yet expired, the + scheduler will simply ignore it. + + c) the batch age (currently 4 days) has expired, the + scheduler will eventually generate a mailer daemon batch + and remove the original batch from the queue. + + +XXX - needs to be completed/improved/updated diff --git a/usr.sbin/smtpd/Makefile b/usr.sbin/smtpd/Makefile new file mode 100644 index 00000000000..b59093c6236 --- /dev/null +++ b/usr.sbin/smtpd/Makefile @@ -0,0 +1,21 @@ +# $OpenBSD: Makefile,v 1.1 2008/11/01 21:35:28 gilles Exp $ + +PROG= smtpd +SRCS= parse.y atomic.c log.c config.c buffer.c imsg.c \ + smtpd.c lka.c mfa.c queue.c mta.c mda.c control.c \ + smtp.c smtp_session.c store.c debug.c \ + ssl.c ssl_privsep.c dns.c aliases.c forward.c \ + map.c +MAN= smtpd.8 smtpd.conf.5 + +LDADD= -levent -lutil -lssl -lcrypto +DPADD= ${LIBEVENT} ${LIBSSL} ${LIBCRYPTO} +CFLAGS= -g3 -ggdb -I${.CURDIR} +CFLAGS+= -Wall -Wstrict-prototypes -Wmissing-prototypes +CFLAGS+= -Wmissing-declarations +CFLAGS+= -Wshadow -Wpointer-arith -Wcast-qual +CFLAGS+= -Wsign-compare -Wbounded +#CFLAGS+= -Werror # during development phase (breaks some archs) +YFLAGS= + +.include <bsd.prog.mk> diff --git a/usr.sbin/smtpd/aliases.c b/usr.sbin/smtpd/aliases.c new file mode 100644 index 00000000000..2e66476d484 --- /dev/null +++ b/usr.sbin/smtpd/aliases.c @@ -0,0 +1,447 @@ +/* + * Copyright (c) 2008 Gilles Chehade <gilles@openbsd.org> + * + * 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 <ctype.h> +#include <db.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <paths.h> +#include <pwd.h> +#include <signal.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <util.h> + +#include "smtpd.h" + +int aliases_exist(struct smtpd *, char *); +int aliases_get(struct smtpd *, struct aliaseslist *, char *); +int aliases_virtual_exist(struct smtpd *, struct path *); +int aliases_virtual_get(struct smtpd *, struct aliaseslist *, struct path *); +int aliases_expand_include(struct aliaseslist *, char *); + +int alias_parse(struct alias *, char *); +int alias_is_filter(struct alias *, char *, size_t); +int alias_is_username(struct alias *, char *, size_t); +int alias_is_address(struct alias *, char *, size_t); +int alias_is_filename(struct alias *, char *, size_t); +int alias_is_include(struct alias *, char *, size_t); + +int +aliases_exist(struct smtpd *env, char *username) +{ + int ret; + DBT key; + DBT val; + DB *aliasesdb; + struct map *map; + + map = map_findbyname(env, "aliases"); + if (map == NULL) + return 0; + + if (map->m_src != S_DB) { + log_info("map source for \"aliases\" must be \"db\"."); + return 0; + } + + aliasesdb = dbopen(map->m_config, O_RDONLY, 0600, DB_HASH, NULL); + if (aliasesdb == NULL) + return 0; + + key.data = username; + key.size = strlen(key.data) + 1; + + if ((ret = aliasesdb->get(aliasesdb, &key, &val, 0)) == -1) { + aliasesdb->close(aliasesdb); + return 0; + } + aliasesdb->close(aliasesdb); + + return ret == 0 ? 1 : 0; +} + +int +aliases_get(struct smtpd *env, struct aliaseslist *aliases, char *username) +{ + int ret; + DBT key; + DBT val; + DB *aliasesdb; + size_t nbaliases; + struct alias alias; + struct alias *aliasp; + struct alias *nextalias; + struct map *map; + + map = map_findbyname(env, "aliases"); + if (map == NULL) + return 0; + + if (map->m_src != S_DB) { + log_info("map source for \"aliases\" must be \"db\"."); + return 0; + } + + aliasesdb = dbopen(map->m_config, O_RDONLY, 0600, DB_HASH, NULL); + if (aliasesdb == NULL) + return 0; + + key.data = username; + key.size = strlen(key.data) + 1; + + if ((ret = aliasesdb->get(aliasesdb, &key, &val, 0)) != 0) { + aliasesdb->close(aliasesdb); + return 0; + } + + nbaliases = val.size / sizeof(struct alias); + if (nbaliases == 0) { + aliasesdb->close(aliasesdb); + return 0; + } + + nextalias = (struct alias *)val.data; + do { + alias = *nextalias; + ++nextalias; + if (alias.type == ALIAS_INCLUDE) { + aliases_expand_include(aliases, alias.u.filename); + } + else { + aliasp = calloc(1, sizeof(struct alias)); + if (aliasp == NULL) + err(1, "calloc"); + *aliasp = alias; + TAILQ_INSERT_HEAD(aliases, aliasp, entry); + } + } while (--nbaliases); + aliasesdb->close(aliasesdb); + return 1; +} + +int +aliases_virtual_exist(struct smtpd *env, struct path *path) +{ + int ret; + DBT key; + DBT val; + DB *aliasesdb; + struct map *map; + char strkey[STRLEN]; + + map = map_findbyname(env, "virtual"); + if (map == NULL) + return 0; + + if (map->m_src != S_DB) { + log_info("map source for \"aliases\" must be \"db\"."); + return 0; + } + + aliasesdb = dbopen(map->m_config, O_RDONLY, 0600, DB_HASH, NULL); + if (aliasesdb == NULL) + return 0; + + if (snprintf(strkey, STRLEN, "%s@%s", path->user, path->domain) + >= STRLEN) { + aliasesdb->close(aliasesdb); + return 0; + } + + key.data = strkey; + key.size = strlen(key.data) + 1; + + if ((ret = aliasesdb->get(aliasesdb, &key, &val, 0)) != 0) { + + if (snprintf(strkey, STRLEN, "@%s", path->domain) + >= STRLEN) { + aliasesdb->close(aliasesdb); + return 0; + } + + key.data = strkey; + key.size = strlen(key.data) + 1; + + if ((ret = aliasesdb->get(aliasesdb, &key, &val, 0)) != 0) { + aliasesdb->close(aliasesdb); + return 0; + } + } + aliasesdb->close(aliasesdb); + + return ret == 0 ? 1 : 0; +} + +int +aliases_virtual_get(struct smtpd *env, struct aliaseslist *aliases, + struct path *path) +{ + int ret; + DBT key; + DBT val; + DB *aliasesdb; + size_t nbaliases; + struct alias alias; + struct alias *aliasp; + struct alias *nextalias; + struct map *map; + char strkey[STRLEN]; + + map = map_findbyname(env, "virtual"); + if (map == NULL) + return 0; + + if (map->m_src != S_DB) { + log_info("map source for \"virtual\" must be \"db\"."); + return 0; + } + + aliasesdb = dbopen(map->m_config, O_RDONLY, 0600, DB_HASH, NULL); + if (aliasesdb == NULL) + return 0; + + if (snprintf(strkey, STRLEN, "%s@%s", path->user, path->domain) + >= STRLEN) { + aliasesdb->close(aliasesdb); + return 0; + } + + key.data = strkey; + key.size = strlen(key.data) + 1; + + if ((ret = aliasesdb->get(aliasesdb, &key, &val, 0)) != 0) { + + if (snprintf(strkey, STRLEN, "@%s", path->domain) + >= STRLEN) { + aliasesdb->close(aliasesdb); + return 0; + } + + key.data = strkey; + key.size = strlen(key.data) + 1; + + if ((ret = aliasesdb->get(aliasesdb, &key, &val, 0)) != 0) { + aliasesdb->close(aliasesdb); + return 0; + } + } + + nbaliases = val.size / sizeof(struct alias); + if (nbaliases == 0) { + aliasesdb->close(aliasesdb); + return 0; + } + + nextalias = (struct alias *)val.data; + do { + alias = *nextalias; + ++nextalias; + if (alias.type == ALIAS_INCLUDE) { + aliases_expand_include(aliases, alias.u.filename); + } + else { + aliasp = calloc(1, sizeof(struct alias)); + if (aliasp == NULL) + err(1, "calloc"); + *aliasp = alias; + TAILQ_INSERT_HEAD(aliases, aliasp, entry); + } + } while (--nbaliases); + aliasesdb->close(aliasesdb); + return 1; +} + +int +aliases_expand_include(struct aliaseslist *aliases, char *filename) +{ + FILE *fp; + char *line; + size_t len; + size_t lineno = 0; + char delim[] = { '\\', '#' }; + struct alias alias; + struct alias *aliasp; + + fp = fopen(filename, "r"); + if (fp == NULL) { + warnx("failed to open include file \"%s\".", filename); + return 0; + } + + while ((line = fparseln(fp, &len, &lineno, delim, 0)) != NULL) { + if (len == 0) { + free(line); + continue; + } + if (! alias_parse(&alias, line)) { + warnx("could not parse include entry \"%s\".", line); + } + + if (alias.type == ALIAS_INCLUDE) { + warnx("nested inclusion is not supported."); + } + else { + aliasp = calloc(1, sizeof(struct alias)); + if (aliasp == NULL) + err(1, "calloc"); + *aliasp = alias; + TAILQ_INSERT_TAIL(aliases, aliasp, entry); + } + + free(line); + } + + fclose(fp); + return 1; +} + +int +alias_parse(struct alias *alias, char *line) +{ + size_t i; + int (*f[])(struct alias *, char *, size_t) = { + alias_is_include, + alias_is_filter, + alias_is_filename, + alias_is_address, + alias_is_username + }; + + for (i = 0; i < sizeof(f) / sizeof(void *); ++i) { + bzero(alias, sizeof(struct alias)); + if (f[i](alias, line, strlen(line))) + break; + } + if (i == sizeof(f) / sizeof(void *)) + return 0; + + return 1; +} + + +int +alias_is_filter(struct alias *alias, char *line, size_t len) +{ + if (strncmp(line, "\"|", 2) == 0 && + line[len - 1] == '"') { + if (strlcpy(alias->u.filter, line, MAXPATHLEN) >= + MAXPATHLEN) + return 0; + alias->type = ALIAS_FILTER; + return 1; + } + return 0; +} + +int +alias_is_username(struct alias *alias, char *line, size_t len) +{ + if (len >= MAXLOGNAME) + return 0; + + if (strlcpy(alias->u.username, line, MAXLOGNAME) >= MAXLOGNAME) + return 0; + + while (*line) { + if (!isalnum(*line) && + *line != '_' && *line != '.' && *line != '-') + return 0; + ++line; + } + + alias->type = ALIAS_USERNAME; + return 1; +} + +int +alias_is_address(struct alias *alias, char *line, size_t len) +{ + char *domain; + + if (len < 3) /* x@y */ + return 0; + + domain = strchr(line, '@'); + if (domain == NULL) + return 0; + + /* @ cannot start or end an address */ + if (domain == line || domain == line + len) + return 0; + + /* scan pre @ for disallowed chars */ + *domain++ = '\0'; + strlcpy(alias->u.path.user, line, MAXPATHLEN); + strlcpy(alias->u.path.domain, domain, MAXPATHLEN); + + while (*line) { + char allowedset[] = "!#$%*/?|^{}`~&'+-=_."; + if (!isalnum(*line) && + strchr(allowedset, *line) == NULL) + return 0; + ++line; + } + + while (*domain) { + char allowedset[] = "-."; + if (!isalnum(*domain) && + strchr(allowedset, *domain) == NULL) + return 0; + ++domain; + } + + alias->type = ALIAS_ADDRESS; + return 1; +} + +int +alias_is_filename(struct alias *alias, char *line, size_t len) +{ + if (len >= MAXPATHLEN) + return 0; + + if (*line != '/') + return 0; + + strlcpy(alias->u.filename, line, MAXPATHLEN); + alias->type = ALIAS_FILENAME; + return 1; +} + +int +alias_is_include(struct alias *alias, char *line, size_t len) +{ + if (strncasecmp(":include:", line, 9) != 0) + return 0; + + if (! alias_is_filename(alias, line + 9, len - 9)) + return 0; + + alias->type = ALIAS_INCLUDE; + return 1; +} diff --git a/usr.sbin/smtpd/atomic.c b/usr.sbin/smtpd/atomic.c new file mode 100644 index 00000000000..ce245fedaad --- /dev/null +++ b/usr.sbin/smtpd/atomic.c @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2008 Charles Longeau <chl@openbsd.org> + * + * 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/time.h> + +#include <err.h> +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <paths.h> +#include <pwd.h> +#include <signal.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "smtpd.h" + +ssize_t +atomic_write(int d, const void *buf, size_t nbytes) +{ + ssize_t ret; + size_t n = nbytes; + + do { + ret = write(d, buf, n); + if (ret == -1 && errno != EINTR) + return -1; + if (ret != -1) + n -= ret; + } while (n > 0 || (ret == -1 && errno == EINTR)); + + return nbytes; +} + +ssize_t +atomic_read(int d, void *buf, size_t nbytes) +{ + ssize_t ret; + size_t n = nbytes; + + do { + ret = read(d, buf, n); + if (ret == -1 && errno != EINTR) + return -1; + if (ret != -1) + n -= ret; + + if (ret == 0 && n != 0) + return -1; + + } while (n > 0 || (ret == -1 && errno == EINTR)); + + return nbytes; +} + +ssize_t +atomic_printfd(int d, const char *fmt, ...) +{ + int ret; + char *buf; + + va_list ap; + va_start(ap, fmt); + + if ((ret = vasprintf(&buf, fmt, ap)) == -1) { + va_end(ap); + return -1; + } + va_end(ap); + + ret = atomic_write(d, buf, ret); + free(buf); + + return ret; +} diff --git a/usr.sbin/smtpd/buffer.c b/usr.sbin/smtpd/buffer.c new file mode 100644 index 00000000000..81da8e505f5 --- /dev/null +++ b/usr.sbin/smtpd/buffer.c @@ -0,0 +1,247 @@ +/* $OpenBSD: buffer.c,v 1.1 2008/11/01 21:35:28 gilles Exp $ */ + +/* + * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org> + * + * 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/uio.h> +#include <sys/time.h> + +#include <errno.h> +#include <event.h> +#include <limits.h> +#include <pwd.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "smtpd.h" + +int buf_realloc(struct buf *, size_t); +void buf_enqueue(struct msgbuf *, struct buf *); +void buf_dequeue(struct msgbuf *, struct buf *); + +struct buf * +buf_open(size_t len) +{ + struct buf *buf; + + if ((buf = calloc(1, sizeof(struct buf))) == NULL) + return (NULL); + if ((buf->buf = malloc(len)) == NULL) { + free(buf); + return (NULL); + } + buf->size = buf->max = len; + buf->fd = -1; + + return (buf); +} + +struct buf * +buf_dynamic(size_t len, size_t max) +{ + struct buf *buf; + + if (max < len) + return (NULL); + + if ((buf = buf_open(len)) == NULL) + return (NULL); + + if (max > 0) + buf->max = max; + + return (buf); +} + +int +buf_realloc(struct buf *buf, size_t len) +{ + u_char *b; + + /* on static buffers max is eq size and so the following fails */ + if (buf->wpos + len > buf->max) { + errno = ENOMEM; + return (-1); + } + + b = realloc(buf->buf, buf->wpos + len); + if (b == NULL) + return (-1); + buf->buf = b; + buf->size = buf->wpos + len; + + return (0); +} + +int +buf_add(struct buf *buf, void *data, size_t len) +{ + if (buf->wpos + len > buf->size) + if (buf_realloc(buf, len) == -1) + return (-1); + + memcpy(buf->buf + buf->wpos, data, len); + buf->wpos += len; + return (0); +} + +void * +buf_reserve(struct buf *buf, size_t len) +{ + void *b; + + if (buf->wpos + len > buf->size) + if (buf_realloc(buf, len) == -1) + return (NULL); + + b = buf->buf + buf->wpos; + buf->wpos += len; + return (b); +} + +int +buf_close(struct msgbuf *msgbuf, struct buf *buf) +{ + buf_enqueue(msgbuf, buf); + return (1); +} + +void +buf_free(struct buf *buf) +{ + free(buf->buf); + free(buf); +} + +void +msgbuf_init(struct msgbuf *msgbuf) +{ + msgbuf->queued = 0; + msgbuf->fd = -1; + TAILQ_INIT(&msgbuf->bufs); +} + +void +msgbuf_clear(struct msgbuf *msgbuf) +{ + struct buf *buf; + + while ((buf = TAILQ_FIRST(&msgbuf->bufs)) != NULL) + buf_dequeue(msgbuf, buf); +} + +int +msgbuf_write(struct msgbuf *msgbuf) +{ + struct iovec iov[IOV_MAX]; + struct buf *buf, *next, *save = NULL; + int i = 0; + ssize_t n; + struct msghdr msg; + struct cmsghdr *cmsg; + union { + struct cmsghdr hdr; + char buf[CMSG_SPACE(sizeof(int))]; + } cmsgbuf; + + bzero(&iov, sizeof(iov)); + bzero(&msg, sizeof(msg)); + TAILQ_FOREACH(buf, &msgbuf->bufs, entry) { + if (i >= IOV_MAX) + break; + if (buf->fd != -1 && i != 0) { + buf = save; + break; + } + iov[i].iov_base = buf->buf + buf->rpos; + iov[i].iov_len = buf->size - buf->rpos; + i++; + if (buf->fd != -1) + break; + save = buf; + } + + msg.msg_iov = iov; + msg.msg_iovlen = i; + + if (buf != NULL && buf->fd != -1) { + msg.msg_control = (caddr_t)&cmsgbuf.buf; + msg.msg_controllen = sizeof(cmsgbuf.buf); + cmsg = CMSG_FIRSTHDR(&msg); + cmsg->cmsg_len = CMSG_LEN(sizeof(int)); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + *(int *)CMSG_DATA(cmsg) = buf->fd; + } + + if ((n = sendmsg(msgbuf->fd, &msg, 0)) == -1) { + if (errno == EAGAIN || errno == ENOBUFS || + errno == EINTR) /* try later */ + return (0); + else + return (-1); + } + + if (n == 0) { /* connection closed */ + errno = 0; + return (-2); + } + + if (buf != NULL && buf->fd != -1) { + close(buf->fd); + buf->fd = -1; + } + + for (buf = TAILQ_FIRST(&msgbuf->bufs); buf != NULL && n > 0; + buf = next) { + next = TAILQ_NEXT(buf, entry); + if (buf->rpos + n >= buf->size) { + n -= buf->size - buf->rpos; + buf_dequeue(msgbuf, buf); + } else { + buf->rpos += n; + n = 0; + } + } + + return (0); +} + +void +buf_enqueue(struct msgbuf *msgbuf, struct buf *buf) +{ + TAILQ_INSERT_TAIL(&msgbuf->bufs, buf, entry); + msgbuf->queued++; +} + +void +buf_dequeue(struct msgbuf *msgbuf, struct buf *buf) +{ + TAILQ_REMOVE(&msgbuf->bufs, buf, entry); + + if (buf->fd != -1) + close(buf->fd); + + msgbuf->queued--; + buf_free(buf); +} diff --git a/usr.sbin/smtpd/config.c b/usr.sbin/smtpd/config.c new file mode 100644 index 00000000000..49e5ebbd0a6 --- /dev/null +++ b/usr.sbin/smtpd/config.c @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * + * 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/time.h> + +#include <event.h> +#include <pwd.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> + +#include "smtpd.h" + +int is_peer(struct peer *, enum smtp_proc_type, u_int); + +int +is_peer(struct peer *p, enum smtp_proc_type peer, u_int peercount) +{ + u_int i; + + for (i = 0; i < peercount; i++) + if (p[i].id == peer) + return (1); + return (0); +} + +void +unconfigure(struct smtpd *env) +{ +} + +void +configure(struct smtpd *env) +{ +} + +void +purge_config(struct smtpd *env, u_int8_t what) +{ + struct listener *l; + struct map *m; + struct rule *r; + struct cond *c; + struct opt *o; + struct ssl *s; + + if (what & PURGE_LISTENERS) { + while ((l = TAILQ_FIRST(&env->sc_listeners)) != NULL) { + TAILQ_REMOVE(&env->sc_listeners, l, entry); + free(l); + } + TAILQ_INIT(&env->sc_listeners); + } + if (what & PURGE_MAPS) { + while ((m = TAILQ_FIRST(env->sc_maps)) != NULL) { + TAILQ_REMOVE(env->sc_maps, m, m_entry); + free(m); + } + free(env->sc_maps); + env->sc_maps = NULL; + } + if (what & PURGE_RULES) { + while ((r = TAILQ_FIRST(env->sc_rules)) != NULL) { + TAILQ_REMOVE(env->sc_rules, r, r_entry); + while ((c = TAILQ_FIRST(&r->r_conditions)) != NULL) { + TAILQ_REMOVE(&r->r_conditions, c, c_entry); + free(c); + } + while ((o = TAILQ_FIRST(&r->r_options)) != NULL) { + TAILQ_REMOVE(&r->r_options, o, o_entry); + free(o); + } + free(r); + } + env->sc_rules = NULL; + } + if (what & PURGE_SSL) { + while ((s = SPLAY_ROOT(&env->sc_ssl)) != NULL) { + SPLAY_REMOVE(ssltree, &env->sc_ssl, s); + free(s->ssl_cert); + free(s->ssl_key); + free(s); + } + SPLAY_INIT(&env->sc_ssl); + } +} + +void +init_peers(struct smtpd *env) +{ + int i; + int j; + + for (i = 0; i < PROC_COUNT; i++) + for (j = 0; j < PROC_COUNT; j++) { + if (i >= j) + continue; + if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, + env->sc_pipes[i][j]) == -1) + fatal("socketpair"); + session_socket_blockmode(env->sc_pipes[i][j][0], + BM_NONBLOCK); + session_socket_blockmode(env->sc_pipes[i][j][1], + BM_NONBLOCK); + } +} + +void +config_peers(struct smtpd *env, struct peer *p, u_int peercount) +{ + u_int i; + u_int j; + u_int src; + u_int dst; + u_int idx; + + /* + * close pipes + */ + for (i = 0; i < PROC_COUNT; i++) { + for (j = 0; j < PROC_COUNT; j++) { + if (i >= j) + continue; + + if ((i == smtpd_process && is_peer(p, j, peercount)) || + (j == smtpd_process && is_peer(p, i, peercount))) { + idx = (i == smtpd_process)?1:0; + close(env->sc_pipes[i][j][idx]); + } else { + close(env->sc_pipes[i][j][0]); + close(env->sc_pipes[i][j][1]); + } + } + } + + /* + * listen on appropriate pipes + */ + for (i = 0; i < peercount; i++) { + + if (p[i].id == smtpd_process) + fatal("config_peers: cannot peer with oneself"); + + src = (smtpd_process < p[i].id)?smtpd_process:p[i].id; + dst = (src == p[i].id)?smtpd_process:p[i].id; + + if ((env->sc_ibufs[p[i].id] = + calloc(1, sizeof(struct imsgbuf))) == NULL) + fatal("config_peers"); + + idx = (src == smtpd_process)?0:1; + imsg_init(env->sc_ibufs[p[i].id], + env->sc_pipes[src][dst][idx], p[i].cb); + env->sc_ibufs[p[i].id]->events = EV_READ; + env->sc_ibufs[p[i].id]->data = env; + event_set(&env->sc_ibufs[p[i].id]->ev, + env->sc_ibufs[p[i].id]->fd, + env->sc_ibufs[p[i].id]->events, + env->sc_ibufs[p[i].id]->handler, + env->sc_ibufs[p[i].id]->data); + event_add(&env->sc_ibufs[p[i].id]->ev, NULL); + } +} diff --git a/usr.sbin/smtpd/control.c b/usr.sbin/smtpd/control.c new file mode 100644 index 00000000000..6a34df67fdc --- /dev/null +++ b/usr.sbin/smtpd/control.c @@ -0,0 +1,474 @@ +/* + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org> + * + * 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/stat.h> +#include <sys/socket.h> +#include <sys/un.h> + +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <pwd.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "smtpd.h" + +#define CONTROL_BACKLOG 5 + +/* control specific headers */ +struct { + struct event ev; + int fd; +} control_state; + +__dead void control_shutdown(void); +int control_init(void); +int control_listen(struct smtpd *); +void control_cleanup(void); +void control_accept(int, short, void *); +struct ctl_conn *control_connbyfd(int); +void control_close(int); +void control_sig_handler(int, short, void *); +void control_dispatch_ext(int, short, void *); +void control_dispatch_lka(int, short, void *); +void control_dispatch_mfa(int, short, void *); +void control_dispatch_queue(int, short, void *); + +struct ctl_connlist ctl_conns; + +void +control_sig_handler(int sig, short event, void *p) +{ + switch (sig) { + case SIGINT: + case SIGTERM: + control_shutdown(); + break; + default: + fatalx("control_sig_handler: unexpected signal"); + } +} + + +pid_t +control(struct smtpd *env) +{ + struct sockaddr_un sun; + int fd; + mode_t old_umask; + pid_t pid; + struct passwd *pw; + struct event ev_sigint; + struct event ev_sigterm; + struct peer peers [] = { + { PROC_QUEUE, control_dispatch_queue }, + }; + + switch (pid = fork()) { + case -1: + fatal("control: cannot fork"); + case 0: + break; + default: + return (pid); + } + + purge_config(env, PURGE_EVERYTHING); + + pw = env->sc_pw; + + if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) + fatal("control: socket"); + + sun.sun_family = AF_UNIX; + if (strlcpy(sun.sun_path, SMTPD_SOCKET, + sizeof(sun.sun_path)) >= sizeof(sun.sun_path)) + fatal("control: socket name too long"); + + if (unlink(SMTPD_SOCKET) == -1) + if (errno != ENOENT) + fatal("control: cannot unlink socket"); + + old_umask = umask(S_IXUSR|S_IXGRP|S_IWOTH|S_IROTH|S_IXOTH); + if (bind(fd, (struct sockaddr *)&sun, sizeof(sun)) == -1) { + (void)umask(old_umask); + fatal("control: bind"); + } + (void)umask(old_umask); + + if (chmod(SMTPD_SOCKET, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP) == -1) { + (void)unlink(SMTPD_SOCKET); + fatal("control: chmod"); + } + + session_socket_blockmode(fd, BM_NONBLOCK); + control_state.fd = fd; + +#ifndef DEBUG + if (chroot(pw->pw_dir) == -1) + fatal("control: chroot"); + if (chdir("/") == -1) + fatal("control: chdir(\"/\")"); +#else +#warning disabling privilege revocation and chroot in DEBUG MODE +#endif + + setproctitle("control process"); + smtpd_process = PROC_CONTROL; + +#ifndef DEBUG + 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("control: cannot drop privileges"); +#endif + + event_init(); + + signal_set(&ev_sigint, SIGINT, control_sig_handler, env); + signal_set(&ev_sigterm, SIGTERM, control_sig_handler, env); + signal_add(&ev_sigint, NULL); + signal_add(&ev_sigterm, NULL); + signal(SIGPIPE, SIG_IGN); + signal(SIGHUP, SIG_IGN); + + TAILQ_INIT(&ctl_conns); + + config_peers(env, peers, 1); + control_listen(env); + event_dispatch(); + control_shutdown(); + + return (0); +} + +void +control_shutdown(void) +{ + log_info("control process exiting"); + _exit(0); +} + +int +control_listen(struct smtpd *env) +{ + if (listen(control_state.fd, CONTROL_BACKLOG) == -1) { + log_warn("control_listen: listen"); + return (-1); + } + + event_set(&control_state.ev, control_state.fd, EV_READ | EV_PERSIST, + control_accept, env); + event_add(&control_state.ev, NULL); + + return (0); +} + +void +control_cleanup(void) +{ + (void)unlink(SMTPD_SOCKET); +} + +/* ARGSUSED */ +void +control_accept(int listenfd, short event, void *arg) +{ + int connfd; + socklen_t len; + struct sockaddr_un sun; + struct ctl_conn *c; + struct smtpd *env = arg; + + len = sizeof(sun); + if ((connfd = accept(listenfd, + (struct sockaddr *)&sun, &len)) == -1) { + if (errno != EWOULDBLOCK && errno != EINTR) + log_warn("control_accept"); + return; + } + + session_socket_blockmode(connfd, BM_NONBLOCK); + + if ((c = malloc(sizeof(struct ctl_conn))) == NULL) { + close(connfd); + log_warn("control_accept"); + return; + } + + imsg_init(&c->ibuf, connfd, control_dispatch_ext); + c->ibuf.events = EV_READ; + event_set(&c->ibuf.ev, c->ibuf.fd, c->ibuf.events, + c->ibuf.handler, env); + event_add(&c->ibuf.ev, NULL); + + TAILQ_INSERT_TAIL(&ctl_conns, c, entry); +} + +struct ctl_conn * +control_connbyfd(int fd) +{ + struct ctl_conn *c; + + for (c = TAILQ_FIRST(&ctl_conns); c != NULL && c->ibuf.fd != fd; + c = TAILQ_NEXT(c, entry)) + ; /* nothing */ + + return (c); +} + +void +control_close(int fd) +{ + struct ctl_conn *c; + + if ((c = control_connbyfd(fd)) == NULL) + log_warn("control_close: fd %d: not found", fd); + + msgbuf_clear(&c->ibuf.w); + TAILQ_REMOVE(&ctl_conns, c, entry); + + event_del(&c->ibuf.ev); + close(c->ibuf.fd); + free(c); +} + +/* ARGSUSED */ +void +control_dispatch_ext(int fd, short event, void *arg) +{ + struct ctl_conn *c; + struct smtpd *env = arg; + struct imsg imsg; + int n; + + if ((c = control_connbyfd(fd)) == NULL) { + log_warn("control_dispatch_ext: fd %d: not found", fd); + return; + } + + switch (event) { + case EV_READ: + if ((n = imsg_read(&c->ibuf)) == -1 || n == 0) { + control_close(fd); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&c->ibuf.w) < 0) { + control_close(fd); + return; + } + imsg_event_add(&c->ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(&c->ibuf, &imsg)) == -1) { + control_close(fd); + return; + } + + if (n == 0) + break; + + switch (imsg.hdr.type) { + case IMSG_CTL_SHUTDOWN: + /* NEEDS_FIX */ + log_debug("received shutdown request"); + if (env->sc_flags & SMTPD_EXITING) { + imsg_compose(&c->ibuf, IMSG_CTL_FAIL, 0, 0, -1, + NULL, 0); + break; + } + env->sc_flags |= SMTPD_EXITING; + imsg_compose(&c->ibuf, IMSG_CTL_OK, 0, 0, -1, NULL, 0); + break; + default: + log_debug("control_dispatch_ext: " + "error handling imsg %d", imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + + imsg_event_add(&c->ibuf); +} + +void +control_dispatch_lka(int sig, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_LKA]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("control_dispatch_lka: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + default: + log_debug("control_dispatch_lka: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + +void +control_dispatch_mfa(int sig, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_MFA]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("control_dispatch_mfa: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + default: + log_debug("control_dispatch_mfa: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + +void +control_dispatch_queue(int sig, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_QUEUE]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("control_dispatch_queue: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + default: + log_debug("control_dispatch_queue: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + +void +session_socket_blockmode(int fd, enum blockmodes bm) +{ + int flags; + + if ((flags = fcntl(fd, F_GETFL, 0)) == -1) + fatal("fcntl F_GETFL"); + + if (bm == BM_NONBLOCK) + flags |= O_NONBLOCK; + else + flags &= ~O_NONBLOCK; + + if ((flags = fcntl(fd, F_SETFL, flags)) == -1) + fatal("fcntl F_SETFL"); +} diff --git a/usr.sbin/smtpd/debug.c b/usr.sbin/smtpd/debug.c new file mode 100644 index 00000000000..73f8b0f64e2 --- /dev/null +++ b/usr.sbin/smtpd/debug.c @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2008 Gilles Chehade <gilles@openbsd.org> + * + * 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/stat.h> + +#include <netinet/in.h> +#include <arpa/inet.h> + +#include <dirent.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <netdb.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include "smtpd.h" + +void debug_display_batch(struct batch *); +void debug_display_message(struct message *); + +void +debug_display_batch(struct batch *p) +{ + log_debug("batch # : %qd", p->id); + + if (p->type & T_MDA_BATCH) { + log_debug("type : MDA"); + } + else if (p->type & T_MTA_BATCH) { + log_debug("type : MTA"); + } + + if (p->type & T_DAEMON_BATCH) { + log_debug("mailer-daemon : yes"); + } + else { + log_debug("mailer-daemon : no"); + } + + log_debug("creation date : %lu", p->creation); + log_debug("flags : %s%s%s%s", + (p->flags & F_BATCH_COMPLETE) ? " [COMPLETE] " : "", + (p->flags & F_BATCH_RESOLVED) ? " [RESOLVED] " : "", + (p->flags & F_BATCH_SCHEDULED) ? " [SCHEDULED] " : "", + (p->flags == 0) ? " [NONE] " : ""); +} + +void +debug_display_message(struct message *p) +{ + log_debug("message # : %qd", p->id); + log_debug("session # : %qd", p->session_id); + log_debug("batch # : %qd", p->batch_id); + log_debug("message-id : %s", p->message_id); + log_debug("message-uid : %s", p->message_uid); + log_debug("sender : %s@%s", p->sender.user, + p->sender.domain); + log_debug("recipient : %s@%s", p->recipient.user, + p->recipient.domain); + + if (p->type & T_MDA_MESSAGE) { + log_debug("type : MDA"); + } + else if (p->type & T_MTA_MESSAGE) { + log_debug("type : MTA"); + } + + if (p->type & T_DAEMON_MESSAGE) { + log_debug("mailer-daemon : yes"); + } + else { + log_debug("mailer-daemon : no"); + } + + log_debug("creation date : %lu", p->creation); + log_debug("last attempt : %lu", p->lasttry); + log_debug("retry count : %lu", p->retry); + log_debug("flags : %lu", p->flags); + log_debug("status : %s%s%s%s%s%s%s%s", + (p->status & S_MESSAGE_PERMFAILURE) ? " [PERMFAILURE]" : "", + (p->status & S_MESSAGE_TEMPFAILURE) ? " [TEMPFAILURE]" : "", + (p->status & S_MESSAGE_REJECTED) ? " [REJECTED]" : "", + (p->status & S_MESSAGE_ACCEPTED) ? " [ACCEPTED]" : "", + (p->status & S_MESSAGE_RETRY) ? " [RETRY]" : "", + (p->status & S_MESSAGE_EDNS) ? " [DNS]" : "", + (p->status & S_MESSAGE_ECONNECT) ? " [CONNECT]" : "", + (p->status == 0) ? "[NONE]" : ""); + +} diff --git a/usr.sbin/smtpd/dns.c b/usr.sbin/smtpd/dns.c new file mode 100644 index 00000000000..08cabed9813 --- /dev/null +++ b/usr.sbin/smtpd/dns.c @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2008 Gilles Chehade <gilles@openbsd.org> + * + * 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/socket.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/time.h> + +#include <netinet/in.h> +#include <arpa/inet.h> +#include <arpa/nameser.h> + +#include <err.h> +#include <event.h> +#include <netdb.h> +#include <resolv.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sysexits.h> +#include <unistd.h> + +#include "smtpd.h" + +struct mxrecord { + char hostname[MAXHOSTNAMELEN]; + u_int16_t priority; +}; + +static void mxsort(struct mxrecord *, size_t); +size_t getmxbyname(char *, char ***); + + +/* bubble sort MX records by priority */ +static void +mxsort(struct mxrecord *array, size_t len) +{ + u_int32_t i; + u_int32_t j; + struct mxrecord store; + + for (i = j = 0; i < len - 1; ++i) { + for (j = i + 1; j < len; ++j) { + if (array[i].priority > array[j].priority) { + store = array[i]; + array[i] = array[j]; + array[j] = store; + } + } + } +} + +size_t +getmxbyname(char *name, char ***result) +{ + union { + u_int8_t bytes[PACKETSZ]; + HEADER header; + } answer; + u_int32_t i, j; + int ret; + u_int8_t *sp; + u_int8_t *endp; + u_int8_t *ptr; + u_int16_t qdcount; + u_int8_t expbuf[PACKETSZ]; + u_int16_t type; + u_int16_t n; + u_int16_t priority, tprio; + size_t mxnb; + struct mxrecord mxarray[MXARRAYSIZE]; + size_t chunklen; + + ret = res_query(name, C_IN, T_MX, (u_int8_t *)&answer.bytes, + sizeof answer); + if (ret == -1) + return 0; + + /* sp stores start of dns packet, + * endp stores end of dns packet, + */ + sp = (u_int8_t *)&answer.bytes; + endp = sp + ret; + + /* skip header */ + ptr = sp + HFIXEDSZ; + + for (qdcount = ntohs(answer.header.qdcount); + qdcount--; + ptr += ret + QFIXEDSZ) { + ret = dn_skipname(ptr, endp); + if (ret == -1) + return 0; + } + + mxnb = 0; + for (; ptr < endp;) { + memset(expbuf, 0, sizeof expbuf); + ret = dn_expand(sp, endp, ptr, expbuf, sizeof expbuf); + if (ret == -1) + break; + ptr += ret; + + GETSHORT(type, ptr); + ptr += sizeof(u_int16_t) + sizeof(u_int32_t); + GETSHORT(n, ptr); + + if (type != T_MX) { + ptr += n; + continue; + } + + GETSHORT(priority, ptr); + ret = dn_expand(sp, endp, ptr, expbuf, sizeof expbuf); + if (ret == -1) + return 0; + ptr += ret; + + if (mxnb < sizeof(mxarray) / sizeof(struct mxrecord)) { + if (strlcpy(mxarray[mxnb].hostname, expbuf, + MAXHOSTNAMELEN) >= MAXHOSTNAMELEN) + return 0; + mxarray[mxnb].priority = priority; + if (tprio < priority) + tprio = priority; + } + else { + for (i = j = 0; + i < sizeof(mxarray) / sizeof(struct mxrecord); + ++i) { + if (tprio < mxarray[i].priority) { + tprio = mxarray[i].priority; + j = i; + } + } + + if (mxarray[j].priority > priority) { + if (strlcpy(mxarray[j].hostname, expbuf, + MAXHOSTNAMELEN) >= MAXHOSTNAMELEN) + return 0; + mxarray[j].priority = priority; + } + } + ++mxnb; + } + + if (mxnb == 0) + return 0; + + if (mxnb > sizeof(mxarray) / sizeof(struct mxrecord)) + mxnb = sizeof(mxarray) / sizeof(struct mxrecord); + + /* Rearrange MX records by priority */ + mxsort((struct mxrecord *)&mxarray, mxnb); + + chunklen = 0; + for (i = 0; i < mxnb; ++i) + chunklen += strlen(mxarray[i].hostname) + 1; + chunklen += ((mxnb + 1) * sizeof(char *)); + + *result = calloc(1, chunklen); + if (*result == NULL) { + err(1, "calloc"); + } + + ptr = (u_int8_t *)*result + (mxnb + 1) * sizeof(char *); + for (i = 0; i < mxnb; ++i) { + strlcpy(ptr, mxarray[i].hostname, MAXHOSTNAMELEN); + (*result)[i] = ptr; + ptr += strlen(mxarray[i].hostname) + 1; + } + (*result)[i] = NULL; + + return mxnb; +} diff --git a/usr.sbin/smtpd/forward.c b/usr.sbin/smtpd/forward.c new file mode 100644 index 00000000000..a1d510f9b4b --- /dev/null +++ b/usr.sbin/smtpd/forward.c @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2008 Gilles Chehade <gilles@openbsd.org> + * + * 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/stat.h> + +#include <ctype.h> +#include <db.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <paths.h> +#include <pwd.h> +#include <signal.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <util.h> + +#include "smtpd.h" + +int alias_parse(struct alias *, char *); +int forwards_get(struct aliaseslist *, char *); + +int +forwards_get(struct aliaseslist *aliases, char *username) +{ + FILE *fp; + struct alias alias; + struct alias *aliasp; + char pathname[MAXPATHLEN]; + struct passwd *pw; + char *buf, *lbuf; + size_t len; + struct stat sb; + + pw = getpwnam(username); + if (pw == NULL) + return 0; + + if (snprintf(pathname, MAXPATHLEN, "%s/.forward", pw->pw_dir) + >= MAXPATHLEN) + return 0; + + fp = fopen(pathname, "r"); + if (fp == NULL) + return 0; + + /* make sure ~/ is not writable by anyone but owner */ + if (stat(pw->pw_dir, &sb) == -1) + goto bad; + if (sb.st_uid != pw->pw_uid || sb.st_mode & (S_IWGRP|S_IWOTH)) + goto bad; + + /* make sure ~/.forward is not writable by anyone but owner */ + if (fstat(fileno(fp), &sb) == -1) + goto bad; + if (sb.st_uid != pw->pw_uid || sb.st_mode & (S_IWGRP|S_IWOTH)) + goto bad; + + lbuf = NULL; + while ((buf = fgetln(fp, &len))) { + if (buf[len - 1] == '\n') + buf[len - 1] = '\0'; + else { + /* EOF without EOL, copy and add the NUL */ + if ((lbuf = malloc(len + 1)) == NULL) + err(1, NULL); + memcpy(lbuf, buf, len); + lbuf[len] = '\0'; + buf = lbuf; + } + printf("%s\n", buf); + if (! alias_parse(&alias, buf)) { + log_debug("bad entry in ~/.forward"); + continue; + } + + if (alias.type == ALIAS_INCLUDE) { + log_debug("includes are forbidden in ~/.forward"); + continue; + } + aliasp = calloc(1, sizeof(struct alias)); + if (aliasp == NULL) + err(1, "calloc"); + *aliasp = alias; + TAILQ_INSERT_HEAD(aliases, aliasp, entry); + + } + free(lbuf); + fclose(fp); + return 1; + +bad: + if (fp != NULL) + fclose(fp); + return 0; +} diff --git a/usr.sbin/smtpd/imsg.c b/usr.sbin/smtpd/imsg.c new file mode 100644 index 00000000000..00665191dca --- /dev/null +++ b/usr.sbin/smtpd/imsg.c @@ -0,0 +1,317 @@ +/* $OpenBSD: imsg.c,v 1.1 2008/11/01 21:35:28 gilles Exp $ */ + +/* + * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org> + * + * 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/stat.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/param.h> +#include <sys/socket.h> +#include <sys/uio.h> + +#include <errno.h> +#include <event.h> +#include <pwd.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "smtpd.h" + +static u_int32_t imsgid = 1; + +void +imsg_init(struct imsgbuf *ibuf, int fd, void (*handler)(int, short, void *)) +{ + if (!ibuf->pid) { + msgbuf_init(&ibuf->w); + bzero(&ibuf->r, sizeof(ibuf->r)); + ibuf->pid = getpid(); + ibuf->handler = handler; + TAILQ_INIT(&ibuf->fds); + } + ibuf->fd = fd; + ibuf->w.fd = fd; +} + +ssize_t +imsg_read(struct imsgbuf *ibuf) +{ + struct msghdr msg; + struct cmsghdr *cmsg; + union { + struct cmsghdr hdr; + char buf[CMSG_SPACE(sizeof(int) * 16)]; + } cmsgbuf; + struct iovec iov; + ssize_t n; + int fd; + struct imsg_fd *ifd; + + bzero(&msg, sizeof(msg)); + + iov.iov_base = ibuf->r.buf + ibuf->r.wpos; + iov.iov_len = sizeof(ibuf->r.buf) - ibuf->r.wpos; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = &cmsgbuf.buf; + msg.msg_controllen = sizeof(cmsgbuf.buf); + + if ((n = recvmsg(ibuf->fd, &msg, 0)) == -1) { + if (errno != EINTR && errno != EAGAIN && errno != EMSGSIZE) { + log_warn("imsg_read: pipe read error"); + return (-1); + } + return (-2); + } + ibuf->r.wpos += n; + + for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL; + cmsg = CMSG_NXTHDR(&msg, cmsg)) { + if (cmsg->cmsg_level == SOL_SOCKET && + cmsg->cmsg_type == SCM_RIGHTS) { + fd = (*(int *)CMSG_DATA(cmsg)); + if ((ifd = calloc(1, sizeof(struct imsg_fd))) == NULL) + fatal("imsg_read calloc"); + ibuf->id = imsgid++; + ifd->fd = fd; + ifd->id = ibuf->id; + TAILQ_INSERT_TAIL(&ibuf->fds, ifd, entry); + } else + log_warn("imsg_read: got unexpected ctl data level %d " + "type %d", cmsg->cmsg_level, cmsg->cmsg_type); + } + + return (n); +} + +ssize_t +imsg_get(struct imsgbuf *ibuf, struct imsg *imsg) +{ + size_t av, left, datalen; + + av = ibuf->r.wpos; + + if (IMSG_HEADER_SIZE > av) + return (0); + + memcpy(&imsg->hdr, ibuf->r.buf, sizeof(imsg->hdr)); + if (imsg->hdr.len < IMSG_HEADER_SIZE || + imsg->hdr.len > MAX_IMSGSIZE) { + log_warnx("imsg_get: imsg hdr len %u out of bounds, type=%u", + imsg->hdr.len, imsg->hdr.type); + return (-1); + } + if (imsg->hdr.len > av) + return (0); + datalen = imsg->hdr.len - IMSG_HEADER_SIZE; + ibuf->r.rptr = ibuf->r.buf + IMSG_HEADER_SIZE; + if ((imsg->data = malloc(datalen)) == NULL) { + log_warn("imsg_get"); + return (-1); + } + + memcpy(imsg->data, ibuf->r.rptr, datalen); + imsg->id = 0; + if (ibuf->id != 0) { + imsg->id = ibuf->id; + ibuf->id = 0; + } + + if (imsg->hdr.len < av) { + left = av - imsg->hdr.len; + memmove(&ibuf->r.buf, ibuf->r.buf + imsg->hdr.len, left); + ibuf->r.wpos = left; + } else + ibuf->r.wpos = 0; + + return (datalen + IMSG_HEADER_SIZE); +} + +int +imsg_compose(struct imsgbuf *ibuf, enum imsg_type type, u_int32_t peerid, + pid_t pid, int fd, void *data, u_int16_t datalen) +{ + struct buf *wbuf; + int n; + + if ((wbuf = imsg_create(ibuf, type, peerid, pid, datalen)) == NULL) + return (-1); + + if (imsg_add(wbuf, data, datalen) == -1) + return (-1); + + wbuf->fd = fd; + + if ((n = imsg_close(ibuf, wbuf)) < 0) + return (-1); + + return (n); +} + +int +imsg_composev(struct imsgbuf *ibuf, enum imsg_type type, u_int32_t peerid, + pid_t pid, int fd, const struct iovec *iov, int iovcnt) +{ + struct buf *wbuf; + int n; + int i, datalen = 0; + + for (i = 0; i < iovcnt; i++) + datalen += iov[i].iov_len; + + if ((wbuf = imsg_create(ibuf, type, peerid, pid, datalen)) == NULL) + return (-1); + + for (i = 0; i < iovcnt; i++) + if (imsg_add(wbuf, iov[i].iov_base, iov[i].iov_len) == -1) + return (-1); + + wbuf->fd = fd; + + if ((n = imsg_close(ibuf, wbuf)) < 0) + return (-1); + + return (n); +} + +/* ARGSUSED */ +struct buf * +imsg_create(struct imsgbuf *ibuf, enum imsg_type type, u_int32_t peerid, + pid_t pid, u_int16_t datalen) +{ + struct buf *wbuf; + struct imsg_hdr hdr; + + datalen += IMSG_HEADER_SIZE; + if (datalen > MAX_IMSGSIZE) { + log_warnx("imsg_create: len %u > MAX_IMSGSIZE; " + "type %u peerid %lu", datalen + IMSG_HEADER_SIZE, + type, peerid); + return (NULL); + } + + hdr.type = type; + hdr.peerid = peerid; + if ((hdr.pid = pid) == 0) + hdr.pid = ibuf->pid; + if ((wbuf = buf_dynamic(datalen, MAX_IMSGSIZE)) == NULL) { + log_warn("imsg_create: buf_open"); + return (NULL); + } + if (imsg_add(wbuf, &hdr, sizeof(hdr)) == -1) + return (NULL); + + return (wbuf); +} + +int +imsg_add(struct buf *msg, void *data, u_int16_t datalen) +{ + if (datalen) + if (buf_add(msg, data, datalen) == -1) { + log_warnx("imsg_add: buf_add error"); + buf_free(msg); + return (-1); + } + return (datalen); +} + +int +imsg_append(struct imsgbuf *ibuf, struct buf *msg) +{ + int n; + struct imsg_hdr *hdr; + + hdr = (struct imsg_hdr *)msg->buf; + hdr->len = (u_int16_t)msg->wpos; + if ((n = buf_close(&ibuf->w, msg)) < 0) { + log_warnx("imsg_close: buf_close error"); + buf_free(msg); + return (-1); + } + + return (n); +} + +int +imsg_close(struct imsgbuf *ibuf, struct buf *msg) +{ + int n; + struct imsg_hdr *hdr; + + hdr = (struct imsg_hdr *)msg->buf; + hdr->len = (u_int16_t)msg->wpos; + if ((n = buf_close(&ibuf->w, msg)) < 0) { + log_warnx("imsg_close: buf_close error"); + buf_free(msg); + return (-1); + } + imsg_event_add(ibuf); + + return (n); +} + +void +imsg_free(struct imsg *imsg) +{ + free(imsg->data); +} + +int +imsg_get_fd(struct imsgbuf *ibuf, struct imsg *imsg) +{ + int fd; + struct imsg_fd *ifd; + + if ((ifd = TAILQ_FIRST(&ibuf->fds)) == NULL) + return (-1); + + TAILQ_FOREACH(ifd, &ibuf->fds, entry) { + if (ifd->id == imsg->id) + break; + } + + if (ifd == NULL) + return (-1); + + fd = ifd->fd; + + TAILQ_REMOVE(&ibuf->fds, ifd, entry); + free(ifd); + + return (fd); +} + +int +imsg_flush(struct imsgbuf *ibuf) +{ + while (ibuf->w.queued) + if (msgbuf_write(&ibuf->w) < 0) + return (-1); + return (0); +} + +void +imsg_clear(struct imsgbuf *ibuf) +{ + while (ibuf->w.queued) + msgbuf_clear(&ibuf->w); +} diff --git a/usr.sbin/smtpd/lka.c b/usr.sbin/smtpd/lka.c new file mode 100644 index 00000000000..f33b4cf5eb1 --- /dev/null +++ b/usr.sbin/smtpd/lka.c @@ -0,0 +1,1013 @@ +/* + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * Copyright (c) 2008 Gilles Chehade <gilles@openbsd.org> + * + * 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/time.h> + +#include <netinet/in.h> +#include <arpa/inet.h> + +#include <ctype.h> +#include <db.h> +#include <err.h> +#include <event.h> +#include <fcntl.h> +#include <pwd.h> +#include <netdb.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <util.h> + +#include "smtpd.h" + +__dead void lka_shutdown(void); +void lka_sig_handler(int, short, void *); +void lka_dispatch_parent(int, short, void *); +void lka_dispatch_mfa(int, short, void *); +void lka_dispatch_smtp(int, short, void *); +void lka_dispatch_queue(int, short, void *); +void lka_setup_events(struct smtpd *); +void lka_disable_events(struct smtpd *); +int lka_verify_mail(struct smtpd *, struct path *); +int lka_verify_rcpt(struct smtpd *, struct path *, struct sockaddr_storage *); +int lka_resolve_mail(struct smtpd *, struct rule *, struct path *); +int lka_resolve_rcpt(struct smtpd *, struct rule *, struct path *); +int lka_forward_file(struct passwd *); +size_t getmxbyname(char *, char ***); +int lka_expand(char *, size_t, struct path *); +int aliases_exist(struct smtpd *, char *); +int aliases_get(struct smtpd *, struct aliaseslist *, char *); +int lka_resolve_alias(struct smtpd *, struct imsgbuf *, struct message *, struct alias *); +int lka_parse_include(char *); +int forwards_get(struct aliaseslist *, char *); +int lka_check_source(struct smtpd *, struct map *, struct sockaddr_storage *); +int lka_match_mask(struct sockaddr_storage *, struct netaddr *); +int aliases_virtual_get(struct smtpd *, struct aliaseslist *, struct path *); +int aliases_virtual_exist(struct smtpd *, struct path *); + +void +lka_sig_handler(int sig, short event, void *p) +{ + switch (sig) { + case SIGINT: + case SIGTERM: + lka_shutdown(); + break; + default: + fatalx("lka_sig_handler: unexpected signal"); + } +} + +void +lka_dispatch_parent(int sig, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_PARENT]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("parent_dispatch_lka: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + default: + log_debug("parent_dispatch_lka: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + +void +lka_dispatch_mfa(int sig, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_MFA]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("lka_dispatch_mfa: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + case IMSG_LKA_LOOKUP_MAIL: { + struct submit_status *ss; + + ss = imsg.data; + ss->code = 530; + + if (ss->u.path.user[0] == '\0' && ss->u.path.domain[0] == '\0') + ss->code = 250; + else + if (lka_verify_mail(env, &ss->u.path)) + ss->code = 250; + + imsg_compose(ibuf, IMSG_MFA_LOOKUP_MAIL, 0, 0, -1, + ss, sizeof(*ss)); + + break; + } + case IMSG_LKA_LOOKUP_RCPT: { + struct submit_status *ss; + + ss = imsg.data; + ss->code = 530; + + if (lka_verify_rcpt(env, &ss->u.path, &ss->ss)) + ss->code = 250; + + imsg_compose(ibuf, IMSG_MFA_LOOKUP_RCPT, 0, 0, -1, + ss, sizeof(*ss)); + + break; + } + default: + log_debug("lka_dispatch_mfa: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + +void +lka_dispatch_smtp(int sig, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_SMTP]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("lka_dispatch_mfa: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + case IMSG_LKA_HOSTNAME_LOOKUP: { + + struct sockaddr *sa = NULL; + socklen_t salen; + char addr[NI_MAXHOST]; + struct addrinfo hints, *res; + int error; + struct session *s = imsg.data; + + if (s->s_ss.ss_family == PF_INET) { + struct sockaddr_in *ssin = (struct sockaddr_in *)&s->s_ss; + sa = (struct sockaddr *)ssin; + } + if (s->s_ss.ss_family == PF_INET6) { + struct sockaddr_in6 *ssin6 = (struct sockaddr_in6 *)&s->s_ss; + sa = (struct sockaddr *)ssin6; + } + + error = getnameinfo(sa, sa->sa_len, addr, sizeof(addr), + NULL, 0, NI_NAMEREQD); + if (error == 0) { + memset(&hints, 0, sizeof(hints)); + hints.ai_socktype = SOCK_DGRAM; + hints.ai_flags = AI_NUMERICHOST; + if (getaddrinfo(addr, "0", &hints, &res) == 0) { + freeaddrinfo(res); + strlcpy(s->s_hostname, "<bogus>", MAXHOSTNAMELEN); + imsg_compose(ibuf, IMSG_SMTP_HOSTNAME_ANSWER, 0, 0, -1, + s, sizeof(struct session)); + break; + } + } else { + error = getnameinfo(sa, salen, addr, sizeof(addr), + NULL, 0, NI_NUMERICHOST); + } + strlcpy(s->s_hostname, addr, MAXHOSTNAMELEN); + imsg_compose(ibuf, IMSG_SMTP_HOSTNAME_ANSWER, 0, 0, -1, + s, sizeof(struct session)); + break; + } + default: + log_debug("lka_dispatch_mfa: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + +void +lka_dispatch_queue(int sig, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_QUEUE]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("lka_dispatch_queue: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + + case IMSG_LKA_ALIAS_LOOKUP: { + struct message *messagep; + struct alias *alias; + struct alias *remalias; + struct path *path; + struct aliaseslist aliases; + u_int8_t done = 0; + size_t nbiterations = 5; + int ret; + + messagep = imsg.data; + path = &messagep->recipient; + + if (path->flags & F_EXPANDED) + break; + + TAILQ_INIT(&aliases); + + if (path->flags & F_ALIAS) { + ret = aliases_get(env, &aliases, path->user); + } + + if (path->flags & F_VIRTUAL) { + ret = aliases_virtual_get(env, &aliases, path); + } + + if (! ret) { + /* + * Aliases could not be retrieved, this happens + * if the aliases database is regenerated while + * the message is being processed. It is not an + * error necessarily so just ignore this and it + * will be handled by the queue process. + */ + imsg_compose(ibuf, IMSG_QUEUE_REMOVE_SUBMISSION, 0, 0, -1, messagep, + sizeof(struct message)); + break; + } + + /* First pass, figure out if some of the usernames that + * are in the list are actually aliases and expand them + * if they are. The resolution will be tried five times + * at most with an early exit if list did not change in + * a pass. + */ + while (!done && nbiterations--) { + done = 1; + remalias = NULL; + TAILQ_FOREACH(alias, &aliases, entry) { + if (remalias) { + TAILQ_REMOVE(&aliases, remalias, entry); + free(remalias); + remalias = NULL; + } + + if (alias->type == ALIAS_ADDRESS) { + if (aliases_virtual_get(env, &aliases, &alias->u.path)) { + done = 0; + remalias = alias; + } + } + + else if (alias->type == ALIAS_USERNAME) { + if (aliases_get(env, &aliases, alias->u.username)) { + done = 0; + remalias = alias; + } + } + } + if (remalias) { + TAILQ_REMOVE(&aliases, remalias, entry); + free(remalias); + remalias = NULL; + } + } + + /* Second pass, the list no longer contains aliases and + * the message can be sent back to queue process with a + * modified path. + */ + TAILQ_FOREACH(alias, &aliases, entry) { + struct message message = *messagep; + lka_resolve_alias(env, ibuf, &message, alias); + imsg_compose(ibuf, IMSG_LKA_ALIAS_RESULT, 0, 0, -1, + &message, sizeof(struct message)); + } + + imsg_compose(ibuf, IMSG_QUEUE_REMOVE_SUBMISSION, 0, 0, -1, + messagep, sizeof(struct message)); + + while ((alias = TAILQ_FIRST(&aliases))) { + TAILQ_REMOVE(&aliases, alias, entry); + free(alias); + } + break; + } + + case IMSG_LKA_FORWARD_LOOKUP: { + struct message *messagep; + struct aliaseslist aliases; + struct alias *alias; + + messagep = imsg.data; + + /* this is the tenth time the message has been forwarded + * internally, break out of the loop. + */ + if (messagep->recipient.forwardcnt == 10) { + imsg_compose(ibuf, IMSG_QUEUE_REMOVE_SUBMISSION, 0, 0, -1, messagep, + sizeof(struct message)); + break; + } + messagep->recipient.forwardcnt++; + + TAILQ_INIT(&aliases); + if (! forwards_get(&aliases, messagep->recipient.pw_name)) { + messagep->recipient.flags |= F_NOFORWARD; + imsg_compose(ibuf, IMSG_LKA_FORWARD_LOOKUP, 0, 0, -1, messagep, sizeof(struct message)); + imsg_compose(ibuf, IMSG_QUEUE_REMOVE_SUBMISSION, 0, 0, -1, messagep, + sizeof(struct message)); + break; + } + + TAILQ_FOREACH(alias, &aliases, entry) { + struct message message = *messagep; + lka_resolve_alias(env, ibuf, &message, alias); + if (strcmp(messagep->recipient.pw_name, alias->u.username) == 0) { + + message.recipient.flags |= F_FORWARDED; + } + imsg_compose(ibuf, IMSG_LKA_FORWARD_LOOKUP, 0, 0, -1, &message, sizeof(struct message)); + } + + imsg_compose(ibuf, IMSG_QUEUE_REMOVE_SUBMISSION, 0, 0, -1, messagep, sizeof(struct message)); + + while ((alias = TAILQ_FIRST(&aliases))) { + TAILQ_REMOVE(&aliases, alias, entry); + free(alias); + } + + break; + } + + case IMSG_LKA_MX_LOOKUP: { + struct batch *batchp; + struct addrinfo hints, *res, *resp; + char **mx = NULL; + char *lmx[1]; + size_t len, i, j; + int error; + u_int16_t port = 25; + + batchp = imsg.data; + + if (! IS_RELAY(batchp->rule.r_action)) + err(1, "lka_dispatch_queue: inconsistent internal state"); + + if (batchp->rule.r_action == A_RELAY) { + log_debug("attempting to resolve %s", batchp->hostname); + len = getmxbyname(batchp->hostname, &mx); + if (len == 0) { + lmx[0] = batchp->hostname; + mx = lmx; + len = 1; + } + } + else if (batchp->rule.r_action == A_RELAYVIA) { + lmx[0] = batchp->rule.r_value.host.hostname; + port = batchp->rule.r_value.host.port; + log_debug("attempting to resolve %s:%d (forced)", lmx[0], port); + mx = lmx; + len = 1; + } + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_protocol = IPPROTO_TCP; + for (i = j = 0; i < len; ++i) { + error = getaddrinfo(mx[i], NULL, &hints, &res); + if (error) + continue; + + log_debug("resolving MX: %s", mx[i]); + + for (resp = res; resp != NULL; resp = resp->ai_next) { + if (resp->ai_family == PF_INET) { + struct sockaddr_in *ssin; + + batchp->ss[j] = *(struct sockaddr_storage *)resp->ai_addr; + ssin = (struct sockaddr_in *)&batchp->ss[j]; + ssin->sin_port = htons(port); + ++j; + } + if (resp->ai_family == PF_INET6) { + struct sockaddr_in6 *ssin6; + batchp->ss[j] = *(struct sockaddr_storage *)resp->ai_addr; + ssin6 = (struct sockaddr_in6 *)&batchp->ss[j]; + ssin6->sin6_port = htons(port); + ++j; + } + } + + freeaddrinfo(res); + } + + batchp->ss_cnt = j; + batchp->h_errno = 0; + if (j == 0) + batchp->h_errno = error; + imsg_compose(ibuf, IMSG_LKA_MX_LOOKUP, 0, 0, -1, batchp, sizeof(*batchp)); + + if (mx != lmx) + free(mx); + + break; + } + default: + log_debug("lka_dispatch_queue: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + +void +lka_shutdown(void) +{ + log_info("lookup agent exiting"); + _exit(0); +} + +void +lka_setup_events(struct smtpd *env) +{ +} + +void +lka_disable_events(struct smtpd *env) +{ +} + +pid_t +lka(struct smtpd *env) +{ + pid_t pid; + struct passwd *pw; + + struct event ev_sigint; + struct event ev_sigterm; + + struct peer peers[] = { + { PROC_PARENT, lka_dispatch_parent }, + { PROC_MFA, lka_dispatch_mfa }, + { PROC_QUEUE, lka_dispatch_queue }, + { PROC_SMTP, lka_dispatch_smtp }, + }; + + switch (pid = fork()) { + case -1: + fatal("lka: cannot fork"); + case 0: + break; + default: + return (pid); + } + +// purge_config(env, PURGE_EVERYTHING); + + pw = env->sc_pw; + + setproctitle("lookup agent"); + smtpd_process = PROC_LKA; + +#ifndef DEBUG + 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("lka: cannot drop privileges"); +#endif + + event_init(); + + signal_set(&ev_sigint, SIGINT, lka_sig_handler, env); + signal_set(&ev_sigterm, SIGTERM, lka_sig_handler, env); + signal_add(&ev_sigint, NULL); + signal_add(&ev_sigterm, NULL); + signal(SIGPIPE, SIG_IGN); + signal(SIGHUP, SIG_IGN); + + config_peers(env, peers, 4); + + lka_setup_events(env); + event_dispatch(); + lka_shutdown(); + + return (0); +} + +int +lka_verify_mail(struct smtpd *env, struct path *path) +{ + struct rule *r; + struct cond *cond; + struct map *map; + struct mapel *me; + + TAILQ_FOREACH(r, env->sc_rules, r_entry) { + TAILQ_FOREACH(cond, &r->r_conditions, c_entry) { + if (cond->c_type == C_ALL) { + path->rule = *r; + if (r->r_action == A_MBOX || + r->r_action == A_MAILDIR) { + return lka_resolve_mail(env, r, path); + } + return 1; + } + + if (cond->c_type == C_DOM) { + cond->c_match = map_find(env, cond->c_map); + if (cond->c_match == NULL) + fatal("lka failed to lookup map."); + + map = cond->c_match; + TAILQ_FOREACH(me, &map->m_contents, me_entry) { + if (strcasecmp(me->me_key.med_string, + path->domain) == 0) { + path->rule = *r; + if (r->r_action == A_MBOX || + r->r_action == A_MAILDIR || + r->r_action == A_EXT) { + return lka_resolve_mail(env, r, path); + } + return 1; + } + } + } + } + } + path->rule.r_action = A_RELAY; + return 1; +} + +int +lka_verify_rcpt(struct smtpd *env, struct path *path, struct sockaddr_storage *ss) +{ + struct rule *r; + struct cond *cond; + struct map *map; + struct mapel *me; + + TAILQ_FOREACH(r, env->sc_rules, r_entry) { + + TAILQ_FOREACH(cond, &r->r_conditions, c_entry) { + + if (cond->c_type == C_ALL) { + path->rule = *r; + + if (! lka_check_source(env, r->r_sources, ss)) + return 0; + + if (r->r_action == A_MBOX || + r->r_action == A_MAILDIR) { + return lka_resolve_rcpt(env, r, path); + } + return 1; + } + + if (cond->c_type == C_DOM) { + + cond->c_match = map_find(env, cond->c_map); + if (cond->c_match == NULL) + fatal("lka failed to lookup map."); + + map = cond->c_match; + TAILQ_FOREACH(me, &map->m_contents, me_entry) { + if (strcasecmp(me->me_key.med_string, + path->domain) == 0) { + path->rule = *r; + if (! lka_check_source(env, r->r_sources, ss)) + return 0; + + if (IS_MAILBOX(r->r_action) || + IS_EXT(r->r_action)) { + return lka_resolve_rcpt(env, r, path); + } + return 1; + } + } + } + } + } + return 0; +} + +int +lka_resolve_mail(struct smtpd *env, struct rule *rule, struct path *path) +{ + char username[MAXLOGNAME]; + struct passwd *pw; + char *p; + + (void)strlcpy(username, path->user, MAXLOGNAME); + + for (p = &username[0]; *p != '\0' && *p != '+'; ++p) + *p = tolower((int)*p); + *p = '\0'; + + if (aliases_virtual_exist(env, path)) + path->flags |= F_VIRTUAL; + else if (aliases_exist(env, username)) + path->flags |= F_ALIAS; + else { + pw = getpwnam(username); + if (pw == NULL) + return 0; + (void)strlcpy(path->pw_name, pw->pw_name, MAXLOGNAME); + if (lka_expand(path->rule.r_value.path, MAXPATHLEN, path) >= + MAXPATHLEN) + return 0; + } + + return 1; +} + +int +lka_resolve_rcpt(struct smtpd *env, struct rule *rule, struct path *path) +{ + char username[MAXLOGNAME]; + struct passwd *pw; + char *p; + + (void)strlcpy(username, path->user, MAXLOGNAME); + + for (p = &username[0]; *p != '\0' && *p != '+'; ++p) + *p = tolower((int)*p); + *p = '\0'; + + if ((path->flags & F_EXPANDED) == 0 && aliases_virtual_exist(env, path)) + path->flags |= F_VIRTUAL; + else if ((path->flags & F_EXPANDED) == 0 && aliases_exist(env, username)) + path->flags |= F_ALIAS; + else { + pw = getpwnam(path->pw_name); + if (pw == NULL) + pw = getpwnam(username); + if (pw == NULL) + return 0; + (void)strlcpy(path->pw_name, pw->pw_name, MAXLOGNAME); + if (lka_expand(path->rule.r_value.path, MAXPATHLEN, path) >= + MAXPATHLEN) { + return 0; + } + } + + return 1; +} + +int +lka_expand(char *buf, size_t len, struct path *path) +{ + char *p, *pbuf; + struct rule r; + size_t ret; + struct passwd *pw; + + bzero(r.r_value.path, MAXPATHLEN); + pbuf = r.r_value.path; + + ret = 0; + for (p = path->rule.r_value.path; *p != '\0'; ++p) { + if (p == path->rule.r_value.path && *p == '~') { + if (*(p + 1) == '/' || *(p + 1) == '\0') { + pw = getpwnam(path->pw_name); + if (pw == NULL) + continue; + + ret += strlcat(pbuf, pw->pw_dir, len); + if (ret >= len) + return ret; + pbuf += strlen(pw->pw_dir); + ++p; + continue; + } + + if (*(p + 1) != '/') { + char username[MAXLOGNAME]; + char *delim; + + ret = strlcpy(username, p + 1, MAXLOGNAME); + delim = strchr(username, '/'); + if (delim == NULL && ret >= MAXLOGNAME) { + continue; + } + + if (delim != NULL) { + *delim = '\0'; + } + + pw = getpwnam(username); + if (pw == NULL) + continue; + + ret += strlcat(pbuf, pw->pw_dir, len); + if (ret >= len) + return ret; + pbuf += strlen(pw->pw_dir); + p += strlen(username); + continue; + } + } + if (strncmp(p, "%a", 2) == 0) { + ret += strlcat(pbuf, path->user, len); + if (ret >= len) + return ret; + pbuf += strlen(path->user); + ++p; + continue; + } + if (strncmp(p, "%u", 2) == 0) { + ret += strlcat(pbuf, path->pw_name, len); + if (ret >= len) + return ret; + pbuf += strlen(path->pw_name); + ++p; + continue; + } + if (strncmp(p, "%d", 2) == 0) { + ret += strlcat(pbuf, path->domain, len); + if (ret >= len) + return ret; + pbuf += strlen(path->domain); + ++p; + continue; + } + if (*p == '%' && isdigit((int)*(p+1)) && *(p+2) == 'a') { + size_t idx; + + idx = *(p+1) - '0'; + if (idx < strlen(path->user)) + *pbuf++ = path->user[idx]; + p+=2; + ++ret; + continue; + } + if (*p == '%' && isdigit((int)*(p+1)) && *(p+2) == 'u') { + size_t idx; + + idx = *(p+1) - '0'; + if (idx < strlen(path->pw_name)) + *pbuf++ = path->pw_name[idx]; + p+=2; + ++ret; + continue; + } + if (*p == '%' && isdigit((int)*(p+1)) && *(p+2) == 'd') { + size_t idx; + + idx = *(p+1) - '0'; + if (idx < strlen(path->domain)) + *pbuf++ = path->domain[idx]; + p+=2; + ++ret; + continue; + } + + *pbuf++ = *p; + ++ret; + } + + memcpy(path->rule.r_value.path, r.r_value.path, ret); + + return ret; +} + +int +lka_resolve_alias(struct smtpd *env, struct imsgbuf *ibuf, struct message *messagep, struct alias *alias) +{ + struct path *rpath = &messagep->recipient; + + rpath->flags &= ~F_ALIAS; + rpath->flags |= F_EXPANDED; + + switch (alias->type) { + case ALIAS_USERNAME: + if (strlcpy(rpath->pw_name, alias->u.username, + sizeof(rpath->pw_name)) >= sizeof(rpath->pw_name)) + return 0; + lka_verify_rcpt(env, rpath, NULL); + break; + + case ALIAS_FILENAME: + rpath->rule.r_action = A_FILENAME; + strlcpy(rpath->u.filename, alias->u.filename, MAXPATHLEN); + break; + + case ALIAS_FILTER: + rpath->rule.r_action = A_EXT; + strlcpy(rpath->rule.r_value.command, alias->u.filter + 2, MAXPATHLEN); + rpath->rule.r_value.command[strlen(rpath->rule.r_value.command) - 1] = '\0'; + break; + + case ALIAS_ADDRESS: + *rpath = alias->u.path; + lka_verify_rcpt(env, rpath, NULL); + if (IS_MAILBOX(rpath->rule.r_action) || + IS_EXT(rpath->rule.r_action)) + messagep->type = T_MDA_MESSAGE; + else + messagep->type = T_MTA_MESSAGE; + + break; + default: + /* ALIAS_INCLUDE cannot happen here, make gcc shut up */ + break; + } + return 1; +} + +int +lka_check_source(struct smtpd *env, struct map *map, struct sockaddr_storage *ss) +{ + struct mapel *me; + + if (ss == NULL) { + /* This happens when caller is part of an internal + * lookup (ie: alias resolved to a remote address) + */ + return 1; + } + + TAILQ_FOREACH(me, &map->m_contents, me_entry) { + + if (ss->ss_family != me->me_key.med_addr.ss.ss_family) + continue; + + if (ss->ss_len == me->me_key.med_addr.ss.ss_len) + continue; + + if (lka_match_mask(ss, &me->me_key.med_addr)) + return 1; + + } + return 0; +} + +int +lka_match_mask(struct sockaddr_storage *ss, struct netaddr *ssmask) +{ + if (ss->ss_family == AF_INET) { + struct sockaddr_in *ssin = (struct sockaddr_in *)ss; + struct sockaddr_in *ssinmask = (struct sockaddr_in *)&ssmask->ss; + + if ((ssin->sin_addr.s_addr & ssinmask->sin_addr.s_addr) == + ssinmask->sin_addr.s_addr) + return (1); + return (0); + } + + if (ss->ss_family == AF_INET6) { + struct in6_addr *in; + struct in6_addr *inmask; + struct in6_addr mask; + int i; + + bzero(&mask, sizeof(mask)); + for (i = 0; i < (128 - ssmask->masked) / 8; i++) + mask.s6_addr[i] = 0xff; + i = ssmask->masked % 8; + if (i) + mask.s6_addr[ssmask->masked / 8] = 0xff00 >> i; + + in = &((struct sockaddr_in6 *)ss)->sin6_addr; + inmask = &((struct sockaddr_in6 *)&ssmask->ss)->sin6_addr; + + for (i = 0; i < 16; i++) { + if ((in->s6_addr[i] & mask.s6_addr[i]) != + inmask->s6_addr[i]) + return (0); + } + return (1); + } + + return (0); +} diff --git a/usr.sbin/smtpd/log.c b/usr.sbin/smtpd/log.c new file mode 100644 index 00000000000..04b44c40b5c --- /dev/null +++ b/usr.sbin/smtpd/log.c @@ -0,0 +1,161 @@ +/* $OpenBSD: log.c,v 1.1 2008/11/01 21:35:28 gilles Exp $ */ + +/* + * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org> + * + * 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 MIND, 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/time.h> + +#include <errno.h> +#include <event.h> +#include <pwd.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <syslog.h> + +#include "smtpd.h" + +int debug; + +void vlog(int, const char *, va_list); +void logit(int, const char *, ...); + +void +log_init(int n_debug) +{ + extern char *__progname; + + debug = n_debug; + + if (!debug) + openlog(__progname, LOG_PID | LOG_NDELAY, LOG_MAIL); + + tzset(); +} + +void +logit(int pri, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vlog(pri, fmt, ap); + va_end(ap); +} + +void +vlog(int pri, const char *fmt, va_list ap) +{ + char *nfmt; + + if (debug) { + /* best effort in out of mem situations */ + if (asprintf(&nfmt, "%s\n", fmt) == -1) { + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n"); + } else { + vfprintf(stderr, nfmt, ap); + free(nfmt); + } + fflush(stderr); + } else + vsyslog(pri, fmt, ap); +} + + +void +log_warn(const char *emsg, ...) +{ + char *nfmt; + va_list ap; + + /* best effort to even work in out of memory situations */ + if (emsg == NULL) + logit(LOG_CRIT, "%s", strerror(errno)); + else { + va_start(ap, emsg); + + if (asprintf(&nfmt, "%s: %s", emsg, strerror(errno)) == -1) { + /* we tried it... */ + vlog(LOG_CRIT, emsg, ap); + logit(LOG_CRIT, "%s", strerror(errno)); + } else { + vlog(LOG_CRIT, nfmt, ap); + free(nfmt); + } + va_end(ap); + } +} + +void +log_warnx(const char *emsg, ...) +{ + va_list ap; + + va_start(ap, emsg); + vlog(LOG_CRIT, emsg, ap); + va_end(ap); +} + +void +log_info(const char *emsg, ...) +{ + va_list ap; + + va_start(ap, emsg); + vlog(LOG_INFO, emsg, ap); + va_end(ap); +} + +void +log_debug(const char *emsg, ...) +{ + va_list ap; + + if (debug > 1) { + va_start(ap, emsg); + vlog(LOG_DEBUG, emsg, ap); + va_end(ap); + } +} + +void +fatal(const char *emsg) +{ + if (emsg == NULL) + logit(LOG_CRIT, "fatal: %s", strerror(errno)); + else + if (errno) + logit(LOG_CRIT, "fatal: %s: %s", + emsg, strerror(errno)); + else + logit(LOG_CRIT, "fatal: %s", emsg); + + exit(1); +} + +void +fatalx(const char *emsg) +{ + errno = 0; + fatal(emsg); +} diff --git a/usr.sbin/smtpd/map.c b/usr.sbin/smtpd/map.c new file mode 100644 index 00000000000..2f72a4a434a --- /dev/null +++ b/usr.sbin/smtpd/map.c @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2008 Gilles Chehade <gilles@openbsd.org> + * + * 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 <err.h> +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <paths.h> +#include <pwd.h> +#include <signal.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sysexits.h> +#include <unistd.h> + +#include "smtpd.h" + +struct map * +map_findbyname(struct smtpd *env, const char *name) +{ + struct map *m; + + TAILQ_FOREACH(m, env->sc_maps, m_entry) { + if (strcmp(m->m_name, name) == 0) + break; + } + return (m); +} + +struct map * +map_find(struct smtpd *env, objid_t id) +{ + struct map *m; + + TAILQ_FOREACH(m, env->sc_maps, m_entry) { + if (m->m_id == id) + break; + } + return (m); +} diff --git a/usr.sbin/smtpd/mda.c b/usr.sbin/smtpd/mda.c new file mode 100644 index 00000000000..5eaaf781c88 --- /dev/null +++ b/usr.sbin/smtpd/mda.c @@ -0,0 +1,390 @@ +/* + * Copyright (c) 2008 Gilles Chehade <gilles@openbsd.org> + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * + * 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/stat.h> + +#include <err.h> +#include <event.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "smtpd.h" + +__dead void mda_shutdown(void); +void mda_sig_handler(int, short, void *); +void mda_dispatch_parent(int, short, void *); +void mda_dispatch_queue(int, short, void *); +void mda_setup_events(struct smtpd *); +void mda_disable_events(struct smtpd *); +void mda_timeout(int, short, void *); +void mda_remove_message(struct smtpd *, struct batch *, struct message *x); + +void +mda_sig_handler(int sig, short event, void *p) +{ + switch (sig) { + case SIGINT: + case SIGTERM: + mda_shutdown(); + break; + default: + fatalx("mda_sig_handler: unexpected signal"); + } +} + +void +mda_dispatch_parent(int sig, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_PARENT]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("parent_dispatch_mda: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + case IMSG_MDA_MAILBOX_FILE: { + struct batch *batchp; + struct message *messagep; + enum message_status status; + + batchp = (struct batch *)imsg.data; + messagep = &batchp->message; + status = messagep->status; + + batchp = batch_by_id(env, batchp->id); + if (batchp == NULL) + errx(1, "%s: internal inconsistency.", __func__); + + messagep = message_by_id(env, batchp, messagep->id); + if (messagep == NULL) + errx(1, "%s: internal inconsistency.", __func__); + messagep->status = status; + + messagep->mboxfd = imsg_get_fd(ibuf, &imsg); + if (messagep->mboxfd == -1) { + mda_remove_message(env, batchp, messagep); + break; + } + + batchp->message = *messagep; + imsg_compose(env->sc_ibufs[PROC_PARENT], + IMSG_PARENT_MESSAGE_OPEN, 0, 0, -1, batchp, + sizeof(struct batch)); + break; + } + + case IMSG_MDA_MESSAGE_FILE: { + struct batch *batchp; + struct message *messagep; + enum message_status status; + int (*store)(struct batch *, struct message *) = store_write_message; + + batchp = (struct batch *)imsg.data; + messagep = &batchp->message; + status = messagep->status; + + batchp = batch_by_id(env, batchp->id); + if (batchp == NULL) + errx(1, "%s: internal inconsistency.", __func__); + + messagep = message_by_id(env, batchp, messagep->id); + if (messagep == NULL) + errx(1, "%s: internal inconsistency.", __func__); + messagep->status = status; + + messagep->messagefd = imsg_get_fd(ibuf, &imsg); + if (messagep->messagefd == -1) { + if (messagep->mboxfd != -1) + close(messagep->mboxfd); + mda_remove_message(env, batchp, messagep); + break; + } + + /* If batch is a daemon message, override the default store function */ + if (batchp->type & T_DAEMON_BATCH) { + store = store_write_daemon; + } + + if (store_message(batchp, messagep, store)) { + if (batchp->message.recipient.rule.r_action == A_MAILDIR) + imsg_compose(env->sc_ibufs[PROC_PARENT], + IMSG_PARENT_MAILBOX_RENAME, 0, 0, -1, batchp, + sizeof(struct batch)); + } + + if (messagep->mboxfd != -1) + close(messagep->mboxfd); + if (messagep->messagefd != -1) + close(messagep->messagefd); + + mda_remove_message(env, batchp, messagep); + break; + } + default: + log_debug("parent_dispatch_mda: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + +void +mda_dispatch_queue(int sig, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_QUEUE]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("parent_dispatch_queue: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + case IMSG_CREATE_BATCH: { + struct batch *batchp; + + batchp = calloc(1, sizeof (struct batch)); + if (batchp == NULL) + fatal("calloc"); + *batchp = *(struct batch *)imsg.data; + batchp->env = env; + batchp->flags = 0; + + TAILQ_INIT(&batchp->messages); + SPLAY_INSERT(batchtree, &env->batch_queue, batchp); + break; + } + + case IMSG_BATCH_APPEND: { + struct batch *batchp; + struct message *messagep; + + messagep = calloc(1, sizeof (struct message)); + if (messagep == NULL) + fatal("calloc"); + + *messagep = *(struct message *)imsg.data; + batchp = batch_by_id(env, messagep->batch_id); + if (batchp == NULL) + errx(1, "%s: internal inconsistency.", __func__); + + TAILQ_INSERT_TAIL(&batchp->messages, messagep, entry); + break; + } + + case IMSG_BATCH_CLOSE: { + struct batch lookup; + struct batch *batchp; + struct message *messagep; + + lookup = *(struct batch *)imsg.data; + batchp = batch_by_id(env, lookup.id); + if (batchp == NULL) + errx(1, "%s: internal inconsistency.", __func__); + + lookup = *batchp; + TAILQ_FOREACH(messagep, &batchp->messages, entry) { + lookup.message = *messagep; + imsg_compose(env->sc_ibufs[PROC_PARENT], + IMSG_PARENT_MAILBOX_OPEN, 0, 0, -1, &lookup, + sizeof(struct batch)); + } + + break; + } + + default: + log_debug("parent_dispatch_queue: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + + +void +mda_shutdown(void) +{ + log_info("mail delivery agent exiting"); + _exit(0); +} + +void +mda_setup_events(struct smtpd *env) +{ + struct timeval tv; + + evtimer_set(&env->sc_ev, mda_timeout, env); + tv.tv_sec = 3; + tv.tv_usec = 0; + evtimer_add(&env->sc_ev, &tv); +} + +void +mda_disable_events(struct smtpd *env) +{ + evtimer_del(&env->sc_ev); +} + +void +mda_timeout(int fd, short event, void *p) +{ + struct smtpd *env = p; + struct timeval tv; + + tv.tv_sec = 3; + tv.tv_usec = 0; + evtimer_add(&env->sc_ev, &tv); +} + +pid_t +mda(struct smtpd *env) +{ + pid_t pid; + struct passwd *pw; + + struct event ev_sigint; + struct event ev_sigterm; + + struct peer peers[] = { + { PROC_PARENT, mda_dispatch_parent }, + { PROC_QUEUE, mda_dispatch_queue } + }; + + switch (pid = fork()) { + case -1: + fatal("mda: cannot fork"); + case 0: + break; + default: + return (pid); + } + + purge_config(env, PURGE_EVERYTHING); + + pw = env->sc_pw; + +#ifndef DEBUG + if (chroot(pw->pw_dir) == -1) + fatal("mda: chroot"); + if (chdir("/") == -1) + fatal("mda: chdir(\"/\")"); +#else +#warning disabling privilege revocation and chroot in DEBUG MODE +#endif + + setproctitle("mail delivery agent"); + smtpd_process = PROC_MDA; + +#ifndef DEBUG + 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("mda: cannot drop privileges"); +#endif + + SPLAY_INIT(&env->batch_queue); + + event_init(); + + signal_set(&ev_sigint, SIGINT, mda_sig_handler, env); + signal_set(&ev_sigterm, SIGTERM, mda_sig_handler, env); + signal_add(&ev_sigint, NULL); + signal_add(&ev_sigterm, NULL); + signal(SIGPIPE, SIG_IGN); + signal(SIGHUP, SIG_IGN); + + config_peers(env, peers, 2); + + mda_setup_events(env); + event_dispatch(); + mda_shutdown(); + + return (0); +} + +void +mda_remove_message(struct smtpd *env, struct batch *batchp, struct message *messagep) +{ + imsg_compose(env->sc_ibufs[PROC_QUEUE], IMSG_QUEUE_MESSAGE_UPDATE, 0, 0, + -1, messagep, sizeof (struct message)); + + queue_remove_batch_message(env, batchp, messagep); +} diff --git a/usr.sbin/smtpd/mfa.c b/usr.sbin/smtpd/mfa.c new file mode 100644 index 00000000000..5bfed1f68fe --- /dev/null +++ b/usr.sbin/smtpd/mfa.c @@ -0,0 +1,356 @@ +/* + * Copyright (c) 2008 Gilles Chehade <gilles@openbsd.org> + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * + * 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/time.h> + +#include <event.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "smtpd.h" + +__dead void mfa_shutdown(void); +void mfa_sig_handler(int, short, void *); +void mfa_dispatch_parent(int, short, void *); +void mfa_dispatch_smtp(int, short, void *); +void mfa_dispatch_lka(int, short, void *); +void mfa_setup_events(struct smtpd *); +void mfa_disable_events(struct smtpd *); +void mfa_timeout(int, short, void *); + +void +mfa_sig_handler(int sig, short event, void *p) +{ + switch (sig) { + case SIGINT: + case SIGTERM: + mfa_shutdown(); + break; + default: + fatalx("mfa_sig_handler: unexpected signal"); + } +} + +void +mfa_dispatch_parent(int sig, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_PARENT]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("parent_dispatch_mfa: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + default: + log_debug("parent_dispatch_mfa: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + +void +mfa_dispatch_smtp(int sig, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_SMTP]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("mfa_dispatch_smtp: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + case IMSG_MFA_RPATH_SUBMIT: { + struct message *m; + struct submit_status ss; + + m = imsg.data; + log_debug("mfa_dispatch_smtp: testing return path"); + ss.id = m->id; + ss.code = 250; + ss.u.path = m->sender; + + imsg_compose(env->sc_ibufs[PROC_LKA], IMSG_LKA_LOOKUP_MAIL, 0, 0, -1, + &ss, sizeof(ss)); + break; + } + case IMSG_MFA_RCPT_SUBMIT: { + struct message_recipient *mr; + struct submit_status ss; + + mr = imsg.data; + log_debug("mfa_dispatch_smtp: testing forward path"); + ss.id = mr->id; + ss.code = 250; + ss.u.path = mr->path; + ss.ss = mr->ss; + + imsg_compose(env->sc_ibufs[PROC_LKA], IMSG_LKA_LOOKUP_RCPT, 0, 0, -1, + &ss, sizeof(ss)); + break; + } + case IMSG_MFA_DATA_SUBMIT: + break; + default: + log_debug("mfa_dispatch_smtp: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + +void +mfa_dispatch_lka(int sig, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_LKA]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("mfa_dispatch_lka: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + case IMSG_MFA_LOOKUP_MAIL: { + struct submit_status *ss; + + ss = imsg.data; + imsg_compose(env->sc_ibufs[PROC_SMTP], IMSG_MFA_RPATH_SUBMIT, 0, 0, -1, + ss, sizeof(*ss)); + break; + } + case IMSG_MFA_LOOKUP_RCPT: { + struct submit_status *ss; + + ss = imsg.data; + imsg_compose(env->sc_ibufs[PROC_SMTP], IMSG_MFA_RCPT_SUBMIT, 0, 0, -1, + ss, sizeof(*ss)); + break; + } + default: + log_debug("mfa_dispatch_lka: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + +void +mfa_shutdown(void) +{ + log_info("mail filter exiting"); + _exit(0); +} + +void +mfa_setup_events(struct smtpd *env) +{ + struct timeval tv; + + evtimer_set(&env->sc_ev, mfa_timeout, env); + tv.tv_sec = 3; + tv.tv_usec = 0; + evtimer_add(&env->sc_ev, &tv); +} + +void +mfa_disable_events(struct smtpd *env) +{ + evtimer_del(&env->sc_ev); +} + +void +mfa_timeout(int fd, short event, void *p) +{ + struct smtpd *env = p; + struct timeval tv; + + tv.tv_sec = 3; + tv.tv_usec = 0; + evtimer_add(&env->sc_ev, &tv); +} + +pid_t +mfa(struct smtpd *env) +{ + pid_t pid; + struct passwd *pw; + + struct event ev_sigint; + struct event ev_sigterm; + + struct peer peers[] = { + { PROC_PARENT, mfa_dispatch_parent }, + { PROC_SMTP, mfa_dispatch_smtp }, + { PROC_LKA, mfa_dispatch_lka }, + }; + + switch (pid = fork()) { + case -1: + fatal("mfa: cannot fork"); + case 0: + break; + default: + return (pid); + } + +// purge_config(env, PURGE_EVERYTHING); + + pw = env->sc_pw; + +#ifndef DEBUG + if (chroot(pw->pw_dir) == -1) + fatal("mfa: chroot"); + if (chdir("/") == -1) + fatal("mfa: chdir(\"/\")"); +#else +#warning disabling privilege revocation and chroot in DEBUG MODE +#endif + + setproctitle("mail filter agent"); + smtpd_process = PROC_MFA; + +#ifndef DEBUG + 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("mfa: cannot drop privileges"); +#endif + + event_init(); + + signal_set(&ev_sigint, SIGINT, mfa_sig_handler, env); + signal_set(&ev_sigterm, SIGTERM, mfa_sig_handler, env); + signal_add(&ev_sigint, NULL); + signal_add(&ev_sigterm, NULL); + signal(SIGPIPE, SIG_IGN); + signal(SIGHUP, SIG_IGN); + + config_peers(env, peers, 3); + + mfa_setup_events(env); + event_dispatch(); + mfa_shutdown(); + + return (0); +} + +int +msg_cmp(struct message *m1, struct message *m2) +{ + /* + * do not return u_int64_t's + */ + if (m1->id - m2->id > 0) + return (1); + else if (m1->id - m2->id < 0) + return (-1); + else + return (0); +} + +SPLAY_GENERATE(msgtree, message, nodes, msg_cmp); diff --git a/usr.sbin/smtpd/mta.c b/usr.sbin/smtpd/mta.c new file mode 100644 index 00000000000..03bb668bea6 --- /dev/null +++ b/usr.sbin/smtpd/mta.c @@ -0,0 +1,786 @@ +/* + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * Copyright (c) 2008 Gilles Chehade <gilles@openbsd.org> + * + * 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 <netinet/in.h> +#include <arpa/inet.h> + +#include <err.h> +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "smtpd.h" + +__dead void mta_shutdown(void); +void mta_sig_handler(int, short, void *); +void mta_dispatch_parent(int, short, void *); +void mta_dispatch_queue(int, short, void *); +void mta_setup_events(struct smtpd *); +void mta_disable_events(struct smtpd *); +void mta_timeout(int, short, void *); +void mta_write(int, short, void *); +int mta_connect(struct batch *); +void mta_read_handler(struct bufferevent *, void *); +void mta_write_handler(struct bufferevent *, void *); +void mta_error_handler(struct bufferevent *, short, void *); +int mta_reply_handler(struct bufferevent *, void *); +void mta_batch_update_queue(struct batch *); + +void +mta_sig_handler(int sig, short event, void *p) +{ + switch (sig) { + case SIGINT: + case SIGTERM: + mta_shutdown(); + break; + default: + fatalx("mta_sig_handler: unexpected signal"); + } +} + +void +mta_dispatch_parent(int sig, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_PARENT]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("parent_dispatch_mta: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + default: + log_debug("parent_dispatch_mta: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + +void +mta_dispatch_queue(int sig, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_QUEUE]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("parent_dispatch_mta: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + case IMSG_CREATE_BATCH: { + struct batch *batchp; + + batchp = calloc(1, sizeof (struct batch)); + if (batchp == NULL) + err(1, "calloc"); + + *batchp = *(struct batch *)imsg.data; + batchp->ss_off = 0; + batchp->env = env; + batchp->flags = 0; + + TAILQ_INIT(&batchp->messages); + SPLAY_INSERT(batchtree, &env->batch_queue, batchp); + + break; + } + case IMSG_BATCH_APPEND: { + struct batch *batchp; + struct message *messagep; + + messagep = calloc(1, sizeof (struct message)); + if (messagep == NULL) + fatal("calloc"); + + *messagep = *(struct message *)imsg.data; + + batchp = batch_by_id(env, messagep->batch_id); + if (batchp == NULL) + errx(1, "%s: internal inconsistency.", __func__); + + TAILQ_INSERT_TAIL(&batchp->messages, messagep, entry); + + break; + } + case IMSG_BATCH_CLOSE: { + struct batch *batchp; + + batchp = (struct batch *)imsg.data; + batchp = batch_by_id(env, batchp->id); + if (batchp == NULL) + errx(1, "%s: internal inconsistency.", __func__); + + batchp->flags |= F_BATCH_COMPLETE; + + while (! mta_connect(batchp)) { + if (batchp->ss_off == batchp->ss_cnt) { + break; + } + } + break; + } + case IMSG_QUEUE_MESSAGE_FD: { + struct batch *batchp; + int fd; + + if ((fd = imsg_get_fd(ibuf, &imsg)) == -1) { + /* NEEDS_FIX - unsure yet how it must be handled */ + errx(1, "imsg_get_fd"); + } + + batchp = (struct batch *)imsg.data; + batchp = batch_by_id(env, batchp->id); + + if ((batchp->messagefp = fdopen(fd, "r")) == NULL) + err(1, "fdopen"); + + evbuffer_add_printf(batchp->bev->output, "DATA\r\n"); + + bufferevent_enable(batchp->bev, EV_WRITE|EV_READ); + break; + } + default: + log_debug("parent_dispatch_mta: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + +void +mta_shutdown(void) +{ + log_info("mail transfer agent exiting"); + _exit(0); +} + +void +mta_setup_events(struct smtpd *env) +{ + struct timeval tv; + + evtimer_set(&env->sc_ev, mta_timeout, env); + tv.tv_sec = 3; + tv.tv_usec = 0; + evtimer_add(&env->sc_ev, &tv); +} + +void +mta_disable_events(struct smtpd *env) +{ + evtimer_del(&env->sc_ev); +} + +void +mta_timeout(int fd, short event, void *p) +{ + struct smtpd *env = p; + struct timeval tv; + + tv.tv_sec = 3; + tv.tv_usec = 0; + evtimer_add(&env->sc_ev, &tv); +} + +pid_t +mta(struct smtpd *env) +{ + pid_t pid; + + struct passwd *pw; + struct event ev_sigint; + struct event ev_sigterm; + + struct peer peers[] = { + { PROC_QUEUE, mta_dispatch_queue } + }; + + switch (pid = fork()) { + case -1: + fatal("mta: cannot fork"); + case 0: + break; + default: + return (pid); + } + + purge_config(env, PURGE_EVERYTHING); + + pw = env->sc_pw; +#ifndef DEBUG + if (chroot(pw->pw_dir) == -1) + fatal("mta: chroot"); + if (chdir("/") == -1) + fatal("mta: chdir(\"/\")"); +#else +#warning disabling privilege revocation and chroot in DEBUG MODE +#endif + + setproctitle("mail transfer agent"); + smtpd_process = PROC_MTA; + +#ifndef DEBUG + 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("mta: cannot drop privileges"); +#endif + + event_init(); + + signal_set(&ev_sigint, SIGINT, mta_sig_handler, env); + signal_set(&ev_sigterm, SIGTERM, mta_sig_handler, env); + signal_add(&ev_sigint, NULL); + signal_add(&ev_sigterm, NULL); + signal(SIGPIPE, SIG_IGN); + signal(SIGHUP, SIG_IGN); + + config_peers(env, peers, 1); + + SPLAY_INIT(&env->batch_queue); + + mta_setup_events(env); + event_dispatch(); + mta_shutdown(); + + return (0); +} + +/* shamelessly ripped usr.sbin/relayd/check_tcp.c ;) */ +int +mta_connect(struct batch *batchp) +{ + int s; + int type; + struct linger lng; + struct sockaddr_in ssin; + struct sockaddr_in6 ssin6; + + if ((s = socket(batchp->ss[batchp->ss_off].ss_family, SOCK_STREAM, 0)) == -1) { + goto bad; + } + + bzero(&lng, sizeof(lng)); + if (setsockopt(s, SOL_SOCKET, SO_LINGER, &lng, sizeof (lng)) == -1) { + goto bad; + } + + type = 1; + if (setsockopt(s, SOL_SOCKET, SO_REUSEPORT, &type, sizeof (type)) == -1) { + goto bad; + } + + if (fcntl(s, F_SETFL, O_NONBLOCK) == -1) { + goto bad; + } + + if (batchp->ss[batchp->ss_off].ss_family == PF_INET) { + ssin = *(struct sockaddr_in *)&batchp->ss[batchp->ss_off]; + if (connect(s, (struct sockaddr *)&ssin, sizeof(struct sockaddr_in)) == -1) { + if (errno != EINPROGRESS) { + goto bad; + } + } + } + + if (batchp->ss[batchp->ss_off].ss_family == PF_INET6) { + ssin6 = *(struct sockaddr_in6 *)&batchp->ss[batchp->ss_off]; + if (connect(s, (struct sockaddr *)&ssin6, sizeof(struct sockaddr_in6)) == -1) { + if (errno != EINPROGRESS) { + goto bad; + } + } + } + + batchp->tv.tv_sec = SMTPD_CONNECT_TIMEOUT; + batchp->tv.tv_usec = 0; + batchp->peerfd = s; + event_set(&batchp->ev, s, EV_TIMEOUT|EV_WRITE, mta_write, batchp); + event_add(&batchp->ev, &batchp->tv); + + return 1; + +bad: + batchp->ss_off++; + close(s); + return 0; +} + +void +mta_write(int s, short event, void *arg) +{ + struct batch *batchp = arg; + int ret; + + if (event == EV_TIMEOUT) { + batchp->ss_off++; + close(s); + if (batchp->bev) { + bufferevent_free(batchp->bev); + batchp->bev = NULL; + } + strlcpy(batchp->errorline, "connection timed-out.", STRLEN); + + ret = 0; + while (batchp->ss_off < batchp->ss_cnt && + (ret = mta_connect(batchp)) == 0) { + continue; + } + if (ret) + return; + + mta_batch_update_queue(batchp); + return; + } + + batchp->bev = bufferevent_new(s, mta_read_handler, mta_write_handler, + mta_error_handler, batchp); + + if (batchp->bev == NULL) { + mta_batch_update_queue(batchp); + close(s); + return; + } + + bufferevent_enable(batchp->bev, EV_READ|EV_WRITE); +} + +void +mta_read_handler(struct bufferevent *bev, void *arg) +{ + while (mta_reply_handler(bev, arg)) + ; +} + +int +mta_reply_handler(struct bufferevent *bev, void *arg) +{ + struct batch *batchp = arg; + struct smtpd *env = batchp->env; + struct message *messagep = NULL; + char *line; + int i; + int code; +#define F_ISINFO 0x1 +#define F_ISPROTOERROR 0x2 + char codebuf[4]; + char *errstr; + int flags = 0; + + line = evbuffer_readline(bev->input); + if (line == NULL) { + bufferevent_enable(bev, EV_READ|EV_WRITE); + return 0; + } + + line[strcspn(line, "\r")] = '\0'; + + bufferevent_enable(bev, EV_READ|EV_WRITE); + + log_debug("remote server sent: [%s]", line); + + strlcpy(codebuf, line, sizeof codebuf); + code = strtonum(codebuf, 0, UINT16_MAX, (const char **)&errstr); + if (errstr || code < 100) { + /* Server sent invalid line, protocol error */ + batchp->status |= S_BATCH_PERMFAILURE; + strlcpy(batchp->errorline, line, STRLEN); + mta_batch_update_queue(batchp); + return 0; + } + + if (line[3] == '-') { + return 1; + } + + switch (code) { + case 250: + if (batchp->state == S_DONE) { + mta_batch_update_queue(batchp); + return 0; + } + break; + + case 220: + evbuffer_add_printf(batchp->bev->output, "EHLO %s\r\n", env->sc_hostname); + batchp->state = S_GREETED; + return 1; + + case 421: + case 450: + case 451: + batchp->status |= S_BATCH_TEMPFAILURE; + strlcpy(batchp->errorline, line, STRLEN); + mta_batch_update_queue(batchp); + return 0; + + /* The following codes are state dependant and will cause + * a batch rejection if returned at the wrong state. + */ + case 530: + case 550: + if (batchp->state == S_RCPT) { + batchp->messagep->status = (S_MESSAGE_REJECTED|S_MESSAGE_PERMFAILURE); + strlcpy(batchp->messagep->session_errorline, line, STRLEN); + break; + } + case 354: + if (batchp->state == S_RCPT && batchp->messagep == NULL) { + batchp->state = S_DATA; + break; + } + + case 221: + if (batchp->state == S_DONE) { + mta_batch_update_queue(batchp); + return 0; + } + + case 552: + case 553: + flags |= F_ISPROTOERROR; + default: + /* Server sent code we know nothing about, error */ + if (!(flags & F_ISPROTOERROR)) + log_debug("Ouch, SMTP session returned unhandled %d status.", code); + + batchp->status |= S_BATCH_PERMFAILURE; + strlcpy(batchp->errorline, line, STRLEN); + mta_batch_update_queue(batchp); + return 0; + } + + + switch (batchp->state) { + case S_GREETED: { + char *user; + char *domain; + + if (batchp->type & T_DAEMON_BATCH) { + user = "MAILER-DAEMON"; + domain = env->sc_hostname; + } + else { + messagep = TAILQ_FIRST(&batchp->messages); + user = messagep->sender.user; + domain = messagep->sender.domain; + } + + if (user[0] == '\0' && domain[0] == '\0') + evbuffer_add_printf(batchp->bev->output, "MAIL FROM:<>\r\n"); + else + evbuffer_add_printf(batchp->bev->output, "MAIL FROM:<%s@%s>\r\n", user, domain); + batchp->state = S_MAIL; + + break; + } + + case S_MAIL: + batchp->state = S_RCPT; + + case S_RCPT: { + char *user; + char *domain; + + /* Is this the first RCPT ? */ + if (batchp->messagep == NULL) + messagep = TAILQ_FIRST(&batchp->messages); + else { + /* We already had a RCPT, mark is as accepted and + * fetch next one from queue if we aren't dealing + * with a daemon batch. + */ + if (batchp->type & T_DAEMON_BATCH) + messagep = NULL; + else { + messagep = batchp->messagep; + if ((messagep->status & S_MESSAGE_REJECTED) == 0) + messagep->status = S_MESSAGE_ACCEPTED; + messagep = TAILQ_NEXT(batchp->messagep, entry); + } + } + batchp->messagep = messagep; + + if (messagep) { + if (batchp->type & T_DAEMON_BATCH) { + user = messagep->sender.user; + domain = messagep->sender.domain; + } + else { + user = messagep->recipient.user; + domain = messagep->recipient.domain; + } + evbuffer_add_printf(batchp->bev->output, "RCPT TO:<%s@%s>\r\n", user, domain); + } + else { + /* Do we have at least one accepted recipient ? */ + if ((batchp->type & T_DAEMON_BATCH) == 0) { + TAILQ_FOREACH(messagep, &batchp->messages, entry) { + if (messagep->status & S_MESSAGE_ACCEPTED) + break; + } + if (messagep == NULL) { + batchp->status |= S_BATCH_PERMFAILURE; + mta_batch_update_queue(batchp); + return 0; + } + } + + bufferevent_disable(batchp->bev, EV_WRITE|EV_READ); + imsg_compose(env->sc_ibufs[PROC_QUEUE], IMSG_QUEUE_MESSAGE_FD, + 0, 0, -1, batchp, sizeof(*batchp)); + } + break; + } + + case S_DATA: { + bufferevent_enable(batchp->bev, EV_READ|EV_WRITE); + + evbuffer_add_printf(batchp->bev->output, + "Received: from %s (%s [%s])\r\n" + "\tby %s with ESMTP id %s\r\n", + "localhost", "localhost", "127.0.0.1", + "", batchp->message_id); + + evbuffer_add_printf(batchp->bev->output, "X-OpenSMTPD: experiment\r\n"); + + if (batchp->type & T_DAEMON_BATCH) { + evbuffer_add_printf(batchp->bev->output, + "Hi !\r\n\r\n" + "This is the MAILER-DAEMON, please DO NOT REPLY to this e-mail it is\r\n" + "just a notification to let you know that an error has occured.\r\n\r\n"); + + if (batchp->status & S_BATCH_PERMFAILURE) { + evbuffer_add_printf(batchp->bev->output, + "You ran into a PERMANENT FAILURE, which means that the e-mail can't\r\n" + "be delivered to the remote host no matter how much I'll try.\r\n\r\n" + "Diagnostic:\r\n" + "%s\r\n\r\n", batchp->errorline); + } + + if (batchp->status & S_BATCH_TEMPFAILURE) { + evbuffer_add_printf(batchp->bev->output, + "You ran into a TEMPORARY FAILURE, which means that the e-mail can't\r\n" + "be delivered right now, but could be deliberable at a later time. I\r\n" + "will attempt until it succeeds for the next four days, then let you\r\n" + "know if it didn't work out.\r\n" + "Diagnostic:\r\n" + "%s\r\n\r\n", batchp->errorline); + } + + i = 0; + TAILQ_FOREACH(messagep, &batchp->messages, entry) { + if (messagep->status & S_MESSAGE_TEMPFAILURE) { + if (i == 0) { + evbuffer_add_printf(batchp->bev->output, + "The following recipients caused a temporary failure:\r\n"); + ++i; + } + + evbuffer_add_printf(batchp->bev->output, + "\t<%s@%s>:\r\n%s\r\n\r\n", + messagep->recipient.user, messagep->recipient.domain, messagep->session_errorline); + } + } + + i = 0; + TAILQ_FOREACH(messagep, &batchp->messages, entry) { + if (messagep->status & S_MESSAGE_PERMFAILURE) { + if (i == 0) { + evbuffer_add_printf(batchp->bev->output, + "The following recipients caused a permanent failure:\r\n"); + ++i; + } + + evbuffer_add_printf(batchp->bev->output, + "\t<%s@%s>:\r\n%s\r\n\r\n", + messagep->recipient.user, messagep->recipient.domain, messagep->session_errorline); + } + } + + evbuffer_add_printf(batchp->bev->output, + "Below is a copy of the original message:\r\n\r\n"); + } + + break; + } + case S_DONE: + evbuffer_add_printf(batchp->bev->output, "QUIT\r\n"); + batchp->state = S_QUIT; + break; + + default: + log_info("unknown command: %d", batchp->state); + } + + return 1; +} + +void +mta_write_handler(struct bufferevent *bev, void *arg) +{ + struct batch *batchp = arg; + char *buf, *lbuf; + size_t len; + + if (batchp->state == S_QUIT) { + bufferevent_disable(bev, EV_READ|EV_WRITE); + close(batchp->peerfd); + return; + } + + /* Progressively fill the output buffer with data */ + if (batchp->state == S_DATA) { + + lbuf = NULL; + if ((buf = fgetln(batchp->messagefp, &len))) { + if (buf[len - 1] == '\n') + buf[len - 1] = '\0'; + else { + if ((lbuf = malloc(len + 1)) == NULL) + err(1, "malloc"); + memcpy(lbuf, buf, len); + lbuf[len] = '\0'; + buf = lbuf; + } + evbuffer_add_printf(batchp->bev->output, "%s\r\n", buf); + free(lbuf); + } + else { + evbuffer_add_printf(batchp->bev->output, ".\r\n"); + batchp->state = S_DONE; + fclose(batchp->messagefp); + batchp->messagefp = NULL; + } + } + bufferevent_enable(batchp->bev, EV_READ|EV_WRITE); +} + +void +mta_error_handler(struct bufferevent *bev, short error, void *arg) +{ + struct batch *batchp = arg; + if (error & (EVBUFFER_TIMEOUT|EVBUFFER_EOF)) { + bufferevent_disable(bev, EV_READ|EV_WRITE); + close(batchp->peerfd); + return; + } +} + +void +mta_batch_update_queue(struct batch *batchp) +{ + struct smtpd *env = batchp->env; + struct message *messagep; + + while ((messagep = TAILQ_FIRST(&batchp->messages)) != NULL) { + + if (batchp->status & S_BATCH_PERMFAILURE) { + messagep->status |= S_MESSAGE_PERMFAILURE; + } + + if (batchp->status & S_BATCH_TEMPFAILURE) { + if (messagep->status != S_MESSAGE_PERMFAILURE) + messagep->status |= S_MESSAGE_TEMPFAILURE; + } + + imsg_compose(env->sc_ibufs[PROC_QUEUE], + IMSG_QUEUE_MESSAGE_UPDATE, 0, 0, -1, messagep, + sizeof(struct message)); + TAILQ_REMOVE(&batchp->messages, messagep, entry); + free(messagep); + } + + SPLAY_REMOVE(batchtree, &env->batch_queue, batchp); + + if (batchp->messagefp) + fclose(batchp->messagefp); + + if (batchp->bev) + bufferevent_free(batchp->bev); + + free(batchp); +} diff --git a/usr.sbin/smtpd/parse.y b/usr.sbin/smtpd/parse.y new file mode 100644 index 00000000000..29c4b340deb --- /dev/null +++ b/usr.sbin/smtpd/parse.y @@ -0,0 +1,1377 @@ +/* + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * + * 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/time.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/param.h> +#include <sys/socket.h> +#include <sys/stat.h> + +#include <netinet/in.h> +#include <arpa/inet.h> + +#include <ctype.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <limits.h> +#include <pwd.h> +#include <netdb.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "smtpd.h" + +TAILQ_HEAD(files, file) files = TAILQ_HEAD_INITIALIZER(files); +static struct file { + TAILQ_ENTRY(file) entry; + FILE *stream; + char *name; + int lineno; + int errors; +} *file, *topfile; +struct file *pushfile(const char *, int); +int popfile(void); +int check_file_secrecy(int, const char *); +int yyparse(void); +int yylex(void); +int yyerror(const char *, ...); +int kw_cmp(const void *, const void *); +int lookup(char *); +int lgetc(int); +int lungetc(int); +int findeol(void); + +TAILQ_HEAD(symhead, sym) symhead = TAILQ_HEAD_INITIALIZER(symhead); +struct sym { + TAILQ_ENTRY(sym) entry; + int used; + int persist; + char *nam; + char *val; +}; +int symset(const char *, const char *, int); +char *symget(const char *); + +struct smtpd *conf = NULL; +static int errors = 0; + +objid_t last_map_id = 0; +struct map *map = NULL; +struct rule *rule = NULL; +struct mapel_list *contents = NULL; + +struct listener *host_v4(const char *, in_port_t); +struct listener *host_v6(const char *, in_port_t); +int host_dns(const char *, struct listenerlist *, + int, in_port_t, u_int8_t); +int host(const char *, struct listenerlist *, + int, in_port_t, u_int8_t); + +typedef struct { + union { + int64_t number; + objid_t object; + struct timeval tv; + struct cond *cond; + char *string; + struct host *host; + } v; + int lineno; +} YYSTYPE; + +%} + +%token QUEUE INTERVAL LISTEN ON ALL PORT USE +%token MAP TYPE HASH LIST SINGLE SSL SSMTP CERTIFICATE +%token DNS DB TFILE EXTERNAL DOMAIN CONFIG SOURCE +%token RELAY VIA DELIVER TO MAILDIR MBOX HOSTNAME +%token ACCEPT REJECT INCLUDE NETWORK ERROR MDA FROM FOR +%token <v.string> STRING +%token <v.number> NUMBER +%type <v.map> map +%type <v.number> quantifier decision port ssmtp from +%type <v.cond> condition +%type <v.tv> interval +%type <v.object> mapref +%type <v.string> certname + +%% + +grammar : /* empty */ + | grammar '\n' + | grammar include '\n' + | grammar varset '\n' + | grammar main '\n' + | grammar map '\n' + | grammar rule '\n' + | grammar error '\n' { file->errors++; } + ; + +include : INCLUDE STRING { + struct file *nfile; + + if ((nfile = pushfile($2, 0)) == NULL) { + yyerror("failed to include file %s", $2); + free($2); + YYERROR; + } + free($2); + + file = nfile; + lungetc('\n'); + } + ; + +varset : STRING '=' STRING { + if (symset($1, $3, 0) == -1) + fatal("cannot store variable"); + free($1); + free($3); + } + ; + +comma : ',' + | nl + | /* empty */ + ; + +optnl : '\n' optnl + | + ; + +nl : '\n' optnl + ; + +quantifier : /* empty */ { $$ = 1; } + | 'm' { $$ = 60; } + | 'h' { $$ = 3600; } + | 'd' { $$ = 86400; } + ; + +interval : NUMBER quantifier { + if ($1 < 0) { + yyerror("invalid interval: %d\n", $1); + YYERROR; + } + $$.tv_usec = 0; + $$.tv_sec = $1 * $2; + } + + +port : PORT STRING { + struct servent *servent; + + servent = getservbyname($2, "tcp"); + if (servent == NULL) { + yyerror("port %s is invalid", $2); + free($2); + YYERROR; + } + $$ = servent->s_port; + free($2); + } + | PORT NUMBER { + if ($2 <= 0 || $2 >= (int)USHRT_MAX) { + yyerror("invalid port: %d", $2); + YYERROR; + } + $$ = htons($2); + } + | /* empty */ { + $$ = 0; + } + ; + +certname : USE CERTIFICATE STRING { + if (($$ = strdup($3)) == NULL) + fatal(NULL); + free($3); + } + | /* empty */ { $$ = NULL; } + ; + +ssmtp : SSMTP { $$ = 1; } + | /* empty */ { $$ = 0; } + ; + +main : QUEUE INTERVAL interval { + conf->sc_qintval = $3; + } + | ssmtp LISTEN ON STRING port certname { + char *cert; + u_int8_t flags; + + if ($5 == 0) { + if ($1) + $5 = htons(487); + else + $5 = htons(25); + } + cert = ($6 != NULL) ? $6 : $4; + + flags = 0; + if (ssl_load_certfile(conf, cert) < 0) { + log_warnx("could not load cert: %s", cert); + if ($1 || $6 != NULL) { + yyerror("cannot load certificate: %s", + cert); + free($6); + free($4); + YYERROR; + } + } + else { + if ($1) + flags = F_SSMTP; + else + flags = F_STARTTLS; + } + + if (host($4, &conf->sc_listeners, + MAX_LISTEN, $5, flags) <= 0) { + yyerror("invalid virtual ip: %s", $4); + free($6); + free($4); + YYERROR; + } + free($6); + free($4); + } + | HOSTNAME STRING { + if (strlcpy(conf->sc_hostname, $2, + sizeof(conf->sc_hostname)) >= + sizeof(conf->sc_hostname)) { + yyerror("hostname truncated"); + free($2); + YYERROR; + } + free($2); + } + ; + +maptype : SINGLE { map->m_type = T_SINGLE; } + | LIST { map->m_type = T_LIST; } + | HASH { map->m_type = T_HASH; } + ; + +mapsource : DNS { map->m_src = S_DNS; } + | TFILE { map->m_src = S_FILE; } + | DB STRING { + map->m_src = S_DB; + if (strlcpy(map->m_config, $2, MAXPATHLEN) + >= MAXPATHLEN) + err(1, "pathname too long"); + } + | EXTERNAL { map->m_src = S_EXT; } + ; + +mapopt : TYPE maptype + | SOURCE mapsource + | CONFIG STRING { + } + ; + +mapopts_l : mapopts_l mapopt nl + | mapopt optnl + ; + +map : MAP STRING { + struct map *m; + + TAILQ_FOREACH(m, conf->sc_maps, m_entry) + if (strcmp(m->m_name, $2) == 0) + break; + + if (m != NULL) { + yyerror("map %s defined twice", $2); + free($2); + YYERROR; + } + if ((m = calloc(1, sizeof(*m))) == NULL) + fatal("out of memory"); + if (strlcpy(m->m_name, $2, sizeof(m->m_name)) >= + sizeof(m->m_name)) { + yyerror("map name truncated"); + free(m); + free($2); + YYERROR; + } + + m->m_id = last_map_id++; + m->m_type = T_SINGLE; + + if (m->m_id == INT_MAX) { + yyerror("too many maps defined"); + free($2); + free(m); + YYERROR; + } + map = m; + } '{' optnl mapopts_l '}' { + if (map->m_src == S_NONE) { + yyerror("map %s has no source defined"); + free(map); + map = NULL; + YYERROR; + } + TAILQ_INSERT_TAIL(conf->sc_maps, map, m_entry); + map = NULL; + } + ; + +keyval : STRING '=' '>' STRING { + struct mapel *me; + + if ((me = calloc(1, sizeof(*me))) == NULL) + fatal("out of memory"); + + if (strlcpy(me->me_key.med_string, $1, + sizeof(me->me_key.med_string)) >= + sizeof(me->me_key.med_string) || + strlcpy(me->me_val.med_string, $4, + sizeof(me->me_val.med_string)) >= + sizeof(me->me_val.med_string)) { + yyerror("map elements too long: %s, %s", + $1, $4); + free(me); + free($1); + free($4); + YYERROR; + } + free($1); + free($4); + + TAILQ_INSERT_TAIL(contents, me, me_entry); + } + +keyval_list : keyval + | keyval comma keyval_list + ; + +stringel : STRING { + struct mapel *me; + int bits; + struct sockaddr_in ssin; + struct sockaddr_in6 ssin6; + + if ((me = calloc(1, sizeof(*me))) == NULL) + fatal("out of memory"); + + /* Attempt detection of $1 format */ + if (strchr($1, '/') != NULL) { + /* Dealing with a netmask */ + bzero(&ssin, sizeof(struct sockaddr_in)); + bits = inet_net_pton(AF_INET, $1, &ssin.sin_addr, sizeof(struct in_addr)); + if (bits != -1) { + ssin.sin_family = AF_INET; + me->me_key.med_addr.masked = bits; + me->me_key.med_addr.ss = *(struct sockaddr_storage *)&ssin; + } + else { + bzero(&ssin6, sizeof(struct sockaddr_in6)); + bits = inet_net_pton(AF_INET6, $1, &ssin6.sin6_addr, sizeof(struct in6_addr)); + if (bits == -1) + err(1, "inet_net_pton"); + ssin6.sin6_family = AF_INET6; + me->me_key.med_addr.masked = bits; + me->me_key.med_addr.ss = *(struct sockaddr_storage *)&ssin6; + } + } + else { + /* IP address ? */ + if (inet_pton(AF_INET, $1, &ssin.sin_addr) == 1) { + ssin.sin_family = AF_INET; + me->me_key.med_addr.masked = 0; + me->me_key.med_addr.ss = *(struct sockaddr_storage *)&ssin; + } + else if (inet_pton(AF_INET6, $1, &ssin6.sin6_addr) == 1) { + ssin6.sin6_family = AF_INET6; + me->me_key.med_addr.masked = 0; + me->me_key.med_addr.ss = *(struct sockaddr_storage *)&ssin6; + } + else { + /* either a hostname or a value unrelated to network */ + if (strlcpy(me->me_key.med_string, $1, + sizeof(me->me_key.med_string)) >= + sizeof(me->me_key.med_string)) { + yyerror("map element too long: %s", $1); + free(me); + free($1); + YYERROR; + } + } + } + free($1); + TAILQ_INSERT_TAIL(contents, me, me_entry); + } + ; + +string_list : stringel + | stringel comma string_list + ; + +mapref : STRING { + struct map *m; + struct mapel *me; + int bits; + struct sockaddr_in ssin; + struct sockaddr_in6 ssin6; + + if ((m = calloc(1, sizeof(*m))) == NULL) + fatal("out of memory"); + m->m_id = last_map_id++; + if (m->m_id == INT_MAX) { + yyerror("too many maps defined"); + free(m); + YYERROR; + } + snprintf(m->m_name, STRLEN, "<dynamic(%u)>", m->m_id); + m->m_flags |= F_DYNAMIC|F_USED; + m->m_type = T_SINGLE; + + TAILQ_INIT(&m->m_contents); + + if ((me = calloc(1, sizeof(*me))) == NULL) + fatal("out of memory"); + + /* Attempt detection of $1 format */ + if (strchr($1, '/') != NULL) { + /* Dealing with a netmask */ + bzero(&ssin, sizeof(struct sockaddr_in)); + bits = inet_net_pton(AF_INET, $1, &ssin.sin_addr, sizeof(struct in_addr)); + if (bits != -1) { + ssin.sin_family = AF_INET; + me->me_key.med_addr.masked = bits; + me->me_key.med_addr.ss = *(struct sockaddr_storage *)&ssin; + } + else { + bzero(&ssin6, sizeof(struct sockaddr_in6)); + bits = inet_net_pton(AF_INET6, $1, &ssin6.sin6_addr, sizeof(struct in6_addr)); + if (bits == -1) + err(1, "inet_net_pton"); + ssin6.sin6_family = AF_INET6; + me->me_key.med_addr.masked = bits; + me->me_key.med_addr.ss = *(struct sockaddr_storage *)&ssin6; + } + } + else { + /* IP address ? */ + if (inet_pton(AF_INET, $1, &ssin.sin_addr) == 1) { + ssin.sin_family = AF_INET; + me->me_key.med_addr.masked = 0; + me->me_key.med_addr.ss = *(struct sockaddr_storage *)&ssin; + } + else if (inet_pton(AF_INET6, $1, &ssin6.sin6_addr) == 1) { + ssin6.sin6_family = AF_INET6; + me->me_key.med_addr.masked = 0; + me->me_key.med_addr.ss = *(struct sockaddr_storage *)&ssin6; + } + else { + /* either a hostname or a value unrelated to network */ + if (strlcpy(me->me_key.med_string, $1, + sizeof(me->me_key.med_string)) >= + sizeof(me->me_key.med_string)) { + yyerror("map element too long: %s", $1); + free(me); + free(m); + free($1); + YYERROR; + } + } + } + free($1); + + TAILQ_INSERT_TAIL(&m->m_contents, me, me_entry); + TAILQ_INSERT_TAIL(conf->sc_maps, m, m_entry); + $$ = m->m_id; + } + | '(' { + struct map *m; + + if ((m = calloc(1, sizeof(*m))) == NULL) + fatal("out of memory"); + + m->m_id = last_map_id++; + if (m->m_id == INT_MAX) { + yyerror("too many maps defined"); + free(m); + YYERROR; + } + snprintf(m->m_name, STRLEN, "<dynamic(%u)>", m->m_id); + m->m_flags |= F_DYNAMIC|F_USED; + m->m_type = T_LIST; + + TAILQ_INIT(&m->m_contents); + contents = &m->m_contents; + map = m; + + } string_list ')' { + TAILQ_INSERT_TAIL(conf->sc_maps, map, m_entry); + $$ = map->m_id; + } + | '{' { + struct map *m; + + if ((m = calloc(1, sizeof(*m))) == NULL) + fatal("out of memory"); + + m->m_id = last_map_id++; + if (m->m_id == INT_MAX) { + yyerror("too many maps defined"); + free(m); + YYERROR; + } + snprintf(m->m_name, STRLEN, "<dynamic(%u)>", m->m_id); + m->m_flags |= F_DYNAMIC|F_USED; + m->m_type = T_HASH; + + TAILQ_INIT(&m->m_contents); + contents = &m->m_contents; + map = m; + + } keyval_list '}' { + TAILQ_INSERT_TAIL(conf->sc_maps, map, m_entry); + $$ = map->m_id; + } + | MAP STRING { + struct map *m; + + if ((m = map_findbyname(conf, $2)) == NULL) { + yyerror("no such map: %s"); + free($2); + YYERROR; + } + free($2); + m->m_flags |= F_USED; + $$ = m->m_id; + } + ; + +decision : ACCEPT { $$ = 1; } + | REJECT { $$ = 0; } + ; + +condition : NETWORK mapref { + struct cond *c; + + if ((c = calloc(1, sizeof *c)) == NULL) + fatal("out of memory"); + c->c_type = C_NET; + c->c_map = $2; + $$ = c; + } + | DOMAIN mapref { + struct cond *c; + + if ((c = calloc(1, sizeof *c)) == NULL) + fatal("out of memory"); + c->c_type = C_DOM; + c->c_map = $2; + $$ = c; + } + | ALL { + struct cond *c; + + if ((c = calloc(1, sizeof *c)) == NULL) + fatal("out of memory"); + c->c_type = C_ALL; + $$ = c; + } + ; + +condition_list : condition comma condition_list { + TAILQ_INSERT_TAIL(&rule->r_conditions, $1, c_entry); + } + ; + +conditions : condition { + TAILQ_INSERT_TAIL(&rule->r_conditions, $1, c_entry); + } + | '{' condition_list '}' + ; + +action : DELIVER TO MAILDIR STRING { + rule->r_action = A_MAILDIR; + if (strlcpy(rule->r_value.path, $4, MAXPATHLEN) + >= MAXPATHLEN) + fatal("pathname too long"); + free($4); + } + | DELIVER TO MBOX STRING { + rule->r_action = A_MBOX; + if (strlcpy(rule->r_value.path, $4, MAXPATHLEN) + >= MAXPATHLEN) + fatal("pathname too long"); + free($4); + } + | DELIVER TO MDA STRING { + rule->r_action = A_EXT; + if (strlcpy(rule->r_value.command, $4, MAXPATHLEN) + >= MAXPATHLEN) + fatal("command too long"); + free($4); + } + | RELAY { + rule->r_action = A_RELAY; + } + | RELAY VIA STRING PORT NUMBER { + rule->r_action = A_RELAYVIA; + if (strlcpy(rule->r_value.host.hostname, $3, MAXHOSTNAMELEN) + >= MAXHOSTNAMELEN) + fatal("hostname too long"); + if ($5 <= 0 || $5 >= (int)USHRT_MAX) { + yyerror("invalid port: %d", $5); + YYERROR; + } + rule->r_value.host.port = $5; + free($3); + } + ; + +from : FROM mapref { + $$ = $2; + } + | /* empty */ { + struct map *m; + struct mapel *me; + struct sockaddr_in *ssin; + struct sockaddr_in6 *ssin6; + + if ((m = calloc(1, sizeof(*m))) == NULL) + fatal("out of memory"); + m->m_id = last_map_id++; + if (m->m_id == INT_MAX) { + yyerror("too many maps defined"); + free(m); + YYERROR; + } + snprintf(m->m_name, STRLEN, "<dynamic(%u)>", m->m_id); + m->m_flags |= F_DYNAMIC|F_USED; + m->m_type = T_SINGLE; + + TAILQ_INIT(&m->m_contents); + + if ((me = calloc(1, sizeof(*me))) == NULL) + fatal("out of memory"); + me->me_key.med_addr.masked = 0; + ssin = (struct sockaddr_in *)&me->me_key.med_addr.ss; + ssin->sin_family = AF_INET; + if (inet_pton(AF_INET, "127.0.0.1", &ssin->sin_addr) != 1) { + free(me); + free(m); + YYERROR; + } + TAILQ_INSERT_TAIL(&m->m_contents, me, me_entry); + + if ((me = calloc(1, sizeof(*me))) == NULL) + fatal("out of memory"); + me->me_key.med_addr.masked = 0; + ssin6 = (struct sockaddr_in6 *)&me->me_key.med_addr.ss; + ssin6->sin6_family = AF_INET6; + if (inet_pton(AF_INET6, "::1", &ssin6->sin6_addr) != 1) { + free(me); + free(m); + YYERROR; + } + TAILQ_INSERT_TAIL(&m->m_contents, me, me_entry); + + TAILQ_INSERT_TAIL(conf->sc_maps, m, m_entry); + $$ = m->m_id; + } + ; + +rule : decision from { + struct rule *r; + + if ((r = calloc(1, sizeof(*r))) == NULL) + fatal("out of memory"); + rule = r; + rule->r_sources = map_find(conf, $2); + TAILQ_INIT(&rule->r_conditions); + TAILQ_INIT(&rule->r_options); + + } FOR conditions action { + TAILQ_INSERT_TAIL(conf->sc_rules, rule, r_entry); + } + ; +%% + +struct keywords { + const char *k_name; + int k_val; +}; + +int +yyerror(const char *fmt, ...) +{ + va_list ap; + + file->errors++; + va_start(ap, fmt); + fprintf(stderr, "%s:%d: ", file->name, yylval.lineno); + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n"); + va_end(ap); + return (0); +} + +int +kw_cmp(const void *k, const void *e) +{ + return (strcmp(k, ((const struct keywords *)e)->k_name)); +} + +int +lookup(char *s) +{ + /* this has to be sorted always */ + static const struct keywords keywords[] = { + { "accept", ACCEPT }, + { "all", ALL }, + { "certificate", CERTIFICATE }, + { "config", CONFIG }, + { "db", DB }, + { "deliver", DELIVER }, + { "dns", DNS }, + { "domain", DOMAIN }, + { "external", EXTERNAL }, + { "file", TFILE }, + { "for", FOR }, + { "from", FROM }, + { "hash", HASH }, + { "hostname", HOSTNAME }, + { "include", INCLUDE }, + { "interval", INTERVAL }, + { "list", LIST }, + { "listen", LISTEN }, + { "maildir", MAILDIR }, + { "map", MAP }, + { "mbox", MBOX }, + { "mda", MDA }, + { "network", NETWORK }, + { "on", ON }, + { "port", PORT }, + { "queue", QUEUE }, + { "reject", REJECT }, + { "relay", RELAY }, + { "single", SINGLE }, + { "source", SOURCE }, + { "ssl", SSL }, + { "ssmtp", SSMTP }, + { "to", TO }, + { "type", TYPE }, + { "use", USE }, + { "via", VIA }, + }; + const struct keywords *p; + + p = bsearch(s, keywords, sizeof(keywords)/sizeof(keywords[0]), + sizeof(keywords[0]), kw_cmp); + + if (p) + return (p->k_val); + else + return (STRING); +} + +#define MAXPUSHBACK 128 + +char *parsebuf; +int parseindex; +char pushback_buffer[MAXPUSHBACK]; +int pushback_index = 0; + +int +lgetc(int quotec) +{ + int c, next; + + if (parsebuf) { + /* Read character from the parsebuffer instead of input. */ + if (parseindex >= 0) { + c = parsebuf[parseindex++]; + if (c != '\0') + return (c); + parsebuf = NULL; + } else + parseindex++; + } + + if (pushback_index) + return (pushback_buffer[--pushback_index]); + + if (quotec) { + if ((c = getc(file->stream)) == EOF) { + yyerror("reached end of file while parsing " + "quoted string"); + if (file == topfile || popfile() == EOF) + return (EOF); + return (quotec); + } + return (c); + } + + while ((c = getc(file->stream)) == '\\') { + next = getc(file->stream); + if (next != '\n') { + c = next; + break; + } + yylval.lineno = file->lineno; + file->lineno++; + } + + while (c == EOF) { + if (file == topfile || popfile() == EOF) + return (EOF); + c = getc(file->stream); + } + return (c); +} + +int +lungetc(int c) +{ + if (c == EOF) + return (EOF); + if (parsebuf) { + parseindex--; + if (parseindex >= 0) + return (c); + } + if (pushback_index < MAXPUSHBACK-1) + return (pushback_buffer[pushback_index++] = c); + else + return (EOF); +} + +int +findeol(void) +{ + int c; + + parsebuf = NULL; + pushback_index = 0; + + /* skip to either EOF or the first real EOL */ + while (1) { + c = lgetc(0); + if (c == '\n') { + file->lineno++; + break; + } + if (c == EOF) + break; + } + return (ERROR); +} + +int +yylex(void) +{ + char buf[8096]; + char *p, *val; + int quotec, next, c; + int token; + +top: + p = buf; + while ((c = lgetc(0)) == ' ' || c == '\t') + ; /* nothing */ + + yylval.lineno = file->lineno; + if (c == '#') + while ((c = lgetc(0)) != '\n' && c != EOF) + ; /* nothing */ + if (c == '$' && parsebuf == NULL) { + while (1) { + if ((c = lgetc(0)) == EOF) + return (0); + + if (p + 1 >= buf + sizeof(buf) - 1) { + yyerror("string too long"); + return (findeol()); + } + if (isalnum(c) || c == '_') { + *p++ = (char)c; + continue; + } + *p = '\0'; + lungetc(c); + break; + } + val = symget(buf); + if (val == NULL) { + yyerror("macro '%s' not defined", buf); + return (findeol()); + } + parsebuf = val; + parseindex = 0; + goto top; + } + + switch (c) { + case '\'': + case '"': + quotec = c; + while (1) { + if ((c = lgetc(quotec)) == EOF) + return (0); + if (c == '\n') { + file->lineno++; + continue; + } else if (c == '\\') { + if ((next = lgetc(quotec)) == EOF) + return (0); + if (next == quotec || c == ' ' || c == '\t') + c = next; + else if (next == '\n') + continue; + else + lungetc(next); + } else if (c == quotec) { + *p = '\0'; + break; + } + if (p + 1 >= buf + sizeof(buf) - 1) { + yyerror("string too long"); + return (findeol()); + } + *p++ = (char)c; + } + yylval.v.string = strdup(buf); + if (yylval.v.string == NULL) + err(1, "yylex: strdup"); + return (STRING); + } + +#define allowed_to_end_number(x) \ + (isspace(x) || x == ')' || x ==',' || x == '/' || x == '}' || x == '=') + + if (c == '-' || isdigit(c)) { + do { + *p++ = c; + if ((unsigned)(p-buf) >= sizeof(buf)) { + yyerror("string too long"); + return (findeol()); + } + } while ((c = lgetc(0)) != EOF && isdigit(c)); + lungetc(c); + if (p == buf + 1 && buf[0] == '-') + goto nodigits; + if (c == EOF || allowed_to_end_number(c)) { + const char *errstr = NULL; + + *p = '\0'; + yylval.v.number = strtonum(buf, LLONG_MIN, + LLONG_MAX, &errstr); + if (errstr) { + yyerror("\"%s\" invalid number: %s", + buf, errstr); + return (findeol()); + } + return (NUMBER); + } else { +nodigits: + while (p > buf + 1) + lungetc(*--p); + c = *--p; + if (c == '-') + return (c); + } + } + +#define allowed_in_string(x) \ + (isalnum(x) || (ispunct(x) && x != '(' && x != ')' && \ + x != '{' && x != '}' && x != '<' && x != '>' && \ + x != '!' && x != '=' && x != '#' && \ + x != ',')) + + if (isalnum(c) || c == ':' || c == '_') { + do { + *p++ = c; + if ((unsigned)(p-buf) >= sizeof(buf)) { + yyerror("string too long"); + return (findeol()); + } + } while ((c = lgetc(0)) != EOF && (allowed_in_string(c))); + lungetc(c); + *p = '\0'; + if ((token = lookup(buf)) == STRING) + if ((yylval.v.string = strdup(buf)) == NULL) + err(1, "yylex: strdup"); + return (token); + } + if (c == '\n') { + yylval.lineno = file->lineno; + file->lineno++; + } + if (c == EOF) + return (0); + return (c); +} + +int +check_file_secrecy(int fd, const char *fname) +{ + struct stat st; + + if (fstat(fd, &st)) { + log_warn("cannot stat %s", fname); + return (-1); + } + if (st.st_uid != 0 && st.st_uid != getuid()) { + log_warnx("%s: owner not root or current user", fname); + return (-1); + } + if (st.st_mode & (S_IRWXG | S_IRWXO)) { + log_warnx("%s: group/world readable/writeable", fname); + return (-1); + } + return (0); +} + +struct file * +pushfile(const char *name, int secret) +{ + struct file *nfile; + + if ((nfile = calloc(1, sizeof(struct file))) == NULL || + (nfile->name = strdup(name)) == NULL) { + log_warn("malloc"); + return (NULL); + } + if ((nfile->stream = fopen(nfile->name, "r")) == NULL) { + log_warn("%s", nfile->name); + free(nfile->name); + free(nfile); + return (NULL); + } else if (secret && + check_file_secrecy(fileno(nfile->stream), nfile->name)) { + fclose(nfile->stream); + free(nfile->name); + free(nfile); + return (NULL); + } + nfile->lineno = 1; + TAILQ_INSERT_TAIL(&files, nfile, entry); + return (nfile); +} + +int +popfile(void) +{ + struct file *prev; + + if ((prev = TAILQ_PREV(file, files, entry)) != NULL) + prev->errors += file->errors; + + TAILQ_REMOVE(&files, file, entry); + fclose(file->stream); + free(file->name); + free(file); + file = prev; + return (file ? 0 : EOF); +} + +int +parse_config(struct smtpd *x_conf, const char *filename, int opts) +{ + struct sym *sym, *next; + + conf = x_conf; + bzero(conf, sizeof(*conf)); + if ((conf->sc_maps = calloc(1, sizeof(*conf->sc_maps))) == NULL || + (conf->sc_rules = calloc(1, sizeof(*conf->sc_rules))) == NULL) { + log_warn("cannot allocate memory"); + return 0; + } + + errors = 0; + last_map_id = 0; + + map = NULL; + rule = NULL; + + TAILQ_INIT(&conf->sc_listeners); + TAILQ_INIT(conf->sc_maps); + TAILQ_INIT(conf->sc_rules); + SPLAY_INIT(&conf->sc_sessions); + SPLAY_INIT(&conf->sc_ssl); + + conf->sc_qintval.tv_sec = SMTPD_QUEUE_INTERVAL; + conf->sc_qintval.tv_usec = 0; + conf->sc_opts = opts; + + if ((file = pushfile(filename, 0)) == NULL) { + purge_config(conf, PURGE_EVERYTHING); + return (-1); + } + topfile = file; + + /* + * parse configuration + */ + setservent(1); + yyparse(); + errors = file->errors; + popfile(); + endservent(); + + /* Free macros and check which have not been used. */ + for (sym = TAILQ_FIRST(&symhead); sym != NULL; sym = next) { + next = TAILQ_NEXT(sym, entry); + if ((conf->sc_opts & SMTPD_OPT_VERBOSE) && !sym->used) + fprintf(stderr, "warning: macro '%s' not " + "used\n", sym->nam); + if (!sym->persist) { + free(sym->nam); + free(sym->val); + TAILQ_REMOVE(&symhead, sym, entry); + free(sym); + } + } + + if (TAILQ_EMPTY(conf->sc_rules)) { + log_warnx("no rules, nothing to do"); + errors++; + } + + if (strlen(conf->sc_hostname) == 0) + if (gethostname(conf->sc_hostname, + sizeof(conf->sc_hostname)) == -1) { + log_warn("could not determine host name"); + bzero(conf->sc_hostname, sizeof(conf->sc_hostname)); + errors++; + } + + if (errors) { + purge_config(conf, PURGE_EVERYTHING); + return (-1); + } + + return (0); +} + +int +symset(const char *nam, const char *val, int persist) +{ + struct sym *sym; + + for (sym = TAILQ_FIRST(&symhead); sym && strcmp(nam, sym->nam); + sym = TAILQ_NEXT(sym, entry)) + ; /* nothing */ + + if (sym != NULL) { + if (sym->persist == 1) + return (0); + else { + free(sym->nam); + free(sym->val); + TAILQ_REMOVE(&symhead, sym, entry); + free(sym); + } + } + if ((sym = calloc(1, sizeof(*sym))) == NULL) + return (-1); + + sym->nam = strdup(nam); + if (sym->nam == NULL) { + free(sym); + return (-1); + } + sym->val = strdup(val); + if (sym->val == NULL) { + free(sym->nam); + free(sym); + return (-1); + } + sym->used = 0; + sym->persist = persist; + TAILQ_INSERT_TAIL(&symhead, sym, entry); + return (0); +} + +int +cmdline_symset(char *s) +{ + char *sym, *val; + int ret; + size_t len; + + if ((val = strrchr(s, '=')) == NULL) + return (-1); + + len = strlen(s) - strlen(val) + 1; + if ((sym = malloc(len)) == NULL) + errx(1, "cmdline_symset: malloc"); + + (void)strlcpy(sym, s, len); + + ret = symset(sym, val + 1, 1); + free(sym); + + return (ret); +} + +char * +symget(const char *nam) +{ + struct sym *sym; + + TAILQ_FOREACH(sym, &symhead, entry) + if (strcmp(nam, sym->nam) == 0) { + sym->used = 1; + return (sym->val); + } + return (NULL); +} + +struct listener * +host_v4(const char *s, in_port_t port) +{ + struct in_addr ina; + struct sockaddr_in *sain; + struct listener *h; + + bzero(&ina, sizeof(ina)); + if (inet_pton(AF_INET, s, &ina) != 1) + return (NULL); + + if ((h = calloc(1, sizeof(*h))) == NULL) + fatal(NULL); + sain = (struct sockaddr_in *)&h->ss; + sain->sin_len = sizeof(struct sockaddr_in); + sain->sin_family = AF_INET; + sain->sin_addr.s_addr = ina.s_addr; + sain->sin_port = port; + + return (h); +} + +struct listener * +host_v6(const char *s, in_port_t port) +{ + struct in6_addr ina6; + struct sockaddr_in6 *sin6; + struct listener *h; + + bzero(&ina6, sizeof(ina6)); + if (inet_pton(AF_INET6, s, &ina6) != 1) + return (NULL); + + if ((h = calloc(1, sizeof(*h))) == NULL) + fatal(NULL); + sin6 = (struct sockaddr_in6 *)&h->ss; + sin6->sin6_len = sizeof(struct sockaddr_in6); + sin6->sin6_family = AF_INET6; + sin6->sin6_port = port; + memcpy(&sin6->sin6_addr, &ina6, sizeof(ina6)); + + return (h); +} + +int +host_dns(const char *s, struct listenerlist *al, int max, in_port_t port, + u_int8_t flags) +{ + struct addrinfo hints, *res0, *res; + int error, cnt = 0; + struct sockaddr_in *sain; + struct sockaddr_in6 *sin6; + struct listener *h; + + bzero(&hints, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; /* DUMMY */ + error = getaddrinfo(s, NULL, &hints, &res0); + if (error == EAI_AGAIN || error == EAI_NODATA || error == EAI_NONAME) + return (0); + if (error) { + log_warnx("host_dns: could not parse \"%s\": %s", s, + gai_strerror(error)); + return (-1); + } + + for (res = res0; res && cnt < max; res = res->ai_next) { + if (res->ai_family != AF_INET && + res->ai_family != AF_INET6) + continue; + if ((h = calloc(1, sizeof(*h))) == NULL) + fatal(NULL); + + h->port = port; + h->flags = flags; + h->ss.ss_family = res->ai_family; + h->ssl = NULL; + (void)strlcpy(h->ssl_cert_name, s, sizeof(h->ssl_cert_name)); + + if (res->ai_family == AF_INET) { + sain = (struct sockaddr_in *)&h->ss; + sain->sin_len = sizeof(struct sockaddr_in); + sain->sin_addr.s_addr = ((struct sockaddr_in *) + res->ai_addr)->sin_addr.s_addr; + sain->sin_port = port; + } else { + sin6 = (struct sockaddr_in6 *)&h->ss; + sin6->sin6_len = sizeof(struct sockaddr_in6); + memcpy(&sin6->sin6_addr, &((struct sockaddr_in6 *) + res->ai_addr)->sin6_addr, sizeof(struct in6_addr)); + sin6->sin6_port = port; + } + + TAILQ_INSERT_HEAD(al, h, entry); + cnt++; + } + if (cnt == max && res) { + log_warnx("host_dns: %s resolves to more than %d hosts", + s, max); + } + freeaddrinfo(res0); + return (cnt); +} + +int +host(const char *s, struct listenerlist *al, int max, in_port_t port, + u_int8_t flags) +{ + struct listener *h; + + h = host_v4(s, port); + + /* IPv6 address? */ + if (h == NULL) + h = host_v6(s, port); + + if (h != NULL) { + h->port = port; + h->flags = flags; + h->ssl = NULL; + (void)strlcpy(h->ssl_cert_name, s, sizeof(h->ssl_cert_name)); + + TAILQ_INSERT_HEAD(al, h, entry); + return (1); + } + + return (host_dns(s, al, max, port, flags)); +} diff --git a/usr.sbin/smtpd/queue.c b/usr.sbin/smtpd/queue.c new file mode 100644 index 00000000000..3938c01bfaf --- /dev/null +++ b/usr.sbin/smtpd/queue.c @@ -0,0 +1,1315 @@ +/* + * Copyright (c) 2008 Gilles Chehade <gilles@openbsd.org> + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * + * 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/stat.h> +#include <sys/time.h> + +#include <netinet/in.h> +#include <arpa/inet.h> + +#include <dirent.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <netdb.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#include "smtpd.h" + +__dead void queue_shutdown(void); +void queue_sig_handler(int, short, void *); +void queue_dispatch_control(int, short, void *); +void queue_dispatch_smtp(int, short, void *); +void queue_dispatch_mda(int, short, void *); +void queue_dispatch_mta(int, short, void *); +void queue_dispatch_lka(int, short, void *); +void queue_setup_events(struct smtpd *); +void queue_disable_events(struct smtpd *); +void queue_timeout(int, short, void *); +int queue_create_message_file(char *); +void queue_delete_message_file(char *); +int queue_record_submission(struct message *); +int queue_remove_submission(struct message *); +struct batch *batch_lookup(struct smtpd *, struct message *); +int batch_schedule(struct batch *, time_t); +void batch_unschedule(struct batch *); +void batch_send(struct smtpd *, struct batch *, time_t); +int queue_update_database(struct message *); +int queue_open_message_file(struct batch *); +int queue_batch_resolved(struct smtpd *, struct batch *); +struct batch *queue_record_batch(struct smtpd *, struct message *); +struct batch *batch_by_id(struct smtpd *, u_int64_t); +struct message *message_by_id(struct smtpd *, struct batch *, u_int64_t); +void queue_mailer_daemon(struct smtpd *, struct batch *, enum batch_status); +void debug_display_batch(struct batch *); +void debug_display_message(struct message *); +int queue_record_daemon(struct message *); +struct batch *queue_register_daemon_batch(struct smtpd *, struct batch *); +void queue_register_daemon_message(struct smtpd *, struct batch *, struct message *); +void queue_load_submissions(struct smtpd *, time_t); +int queue_message_schedule(struct message *, time_t); +int queue_message_from_id(char *, struct message *); + +void +queue_sig_handler(int sig, short event, void *p) +{ + switch (sig) { + case SIGINT: + case SIGTERM: + queue_shutdown(); + break; + default: + fatalx("queue_sig_handler: unexpected signal"); + } +} + +void +queue_dispatch_control(int sig, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_CONTROL]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("queue_dispatch_control: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + default: + log_debug("queue_dispatch_control: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + +void +queue_dispatch_smtp(int sig, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_SMTP]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("queue_dispatch_smtp: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + case IMSG_QUEUE_CREATE_MESSAGE_FILE: { + struct message *messagep; + struct submit_status ss; + int fd; + + log_debug("mfa_dispatch_smtp: creating message file"); + messagep = imsg.data; + ss.id = messagep->session_id; + ss.code = 250; + fd = queue_create_message_file(ss.u.msgid); + imsg_compose(ibuf, IMSG_SMTP_MESSAGE_FILE, 0, 0, fd, + &ss, sizeof(ss)); + break; + } + case IMSG_QUEUE_DELETE_MESSAGE_FILE: { + struct message *messagep; + + messagep = imsg.data; + queue_delete_message_file(messagep->message_id); + break; + } + case IMSG_QUEUE_MESSAGE_SUBMIT: { + struct message *messagep; + struct submit_status ss; + + messagep = imsg.data; + messagep->id = queue_generate_id(); + ss.id = messagep->session_id; + ss.code = 250; + ss.u.path = messagep->recipient; + + if (IS_MAILBOX(messagep->recipient.rule.r_action) || + IS_EXT(messagep->recipient.rule.r_action)) + messagep->type = T_MDA_MESSAGE; + else + messagep->type = T_MTA_MESSAGE; + + /* Write to disk */ + queue_record_submission(messagep); + imsg_compose(ibuf, IMSG_SMTP_SUBMIT_ACK, 0, 0, -1, + &ss, sizeof(ss)); + + if (messagep->type & T_MTA_MESSAGE) { + messagep->flags |= F_MESSAGE_READY; + queue_update_database(messagep); + break; + } + + if ((messagep->recipient.flags & (F_ALIAS|F_VIRTUAL)) == 0) { + /* not an alias, perform ~/.forward resolution */ + imsg_compose(env->sc_ibufs[PROC_LKA], IMSG_LKA_FORWARD_LOOKUP, 0, 0, -1, + messagep, sizeof(struct message)); + break; + } + + /* Recipient is an alias, proceed to resolving it. + * ~/.forward will be handled by the IMSG_LKA_ALIAS_RESULT + * dispatch case. + */ + imsg_compose(env->sc_ibufs[PROC_LKA], IMSG_LKA_ALIAS_LOOKUP, 0, 0, -1, + messagep, sizeof (struct message)); + + break; + } + default: + log_debug("queue_dispatch_smtp: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + +void +queue_dispatch_mda(int sig, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_MDA]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("queue_dispatch_mda: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + + case IMSG_QUEUE_MESSAGE_UPDATE: { + struct message *messagep; + + messagep = (struct message *)imsg.data; + messagep->batch_id = 0; + messagep->retry++; + + if (messagep->status & S_MESSAGE_TEMPFAILURE) { + messagep->status &= ~S_MESSAGE_TEMPFAILURE; + messagep->flags &= ~F_MESSAGE_PROCESSING; + queue_update_database(messagep); + break; + } + + if (messagep->status & S_MESSAGE_PERMFAILURE) { + struct message msave; + + messagep->status &= ~S_MESSAGE_PERMFAILURE; + if ((messagep->type & T_DAEMON_MESSAGE) == 0) { + msave = *messagep; + messagep->id = queue_generate_id(); + messagep->batch_id = 0; + messagep->type |= T_DAEMON_MESSAGE; + messagep->flags |= F_MESSAGE_READY; + messagep->lasttry = 0; + messagep->retry = 0; + queue_record_submission(messagep); + *messagep = msave; + } + queue_remove_submission(messagep); + break; + } + + /* no error, remove submission */ + queue_remove_submission(messagep); + break; + } + + default: + log_debug("queue_dispatch_mda: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + +void +queue_dispatch_mta(int sig, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_MTA]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("queue_dispatch_mda: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + + case IMSG_QUEUE_MESSAGE_FD: { + int fd; + struct batch *batchp; + + batchp = imsg.data; + fd = queue_open_message_file(batchp); + imsg_compose(ibuf, IMSG_QUEUE_MESSAGE_FD, 0, 0, fd, batchp, + sizeof(*batchp)); + break; + } + + case IMSG_QUEUE_MESSAGE_UPDATE: { + struct message *messagep; + + messagep = (struct message *)imsg.data; + messagep->batch_id = 0; + messagep->retry++; + + if (messagep->status & S_MESSAGE_TEMPFAILURE) { + messagep->status &= ~S_MESSAGE_TEMPFAILURE; + messagep->flags &= ~F_MESSAGE_PROCESSING; + queue_update_database(messagep); + break; + } + + if (messagep->status & S_MESSAGE_PERMFAILURE) { + struct message msave; + + messagep->status &= ~S_MESSAGE_PERMFAILURE; + if ((messagep->type & T_DAEMON_MESSAGE) == 0) { + msave = *messagep; + messagep->id = queue_generate_id(); + messagep->batch_id = 0; + messagep->type |= T_DAEMON_MESSAGE; + messagep->flags |= F_MESSAGE_READY; + messagep->lasttry = 0; + messagep->retry = 0; + queue_record_submission(messagep); + *messagep = msave; + } + queue_remove_submission(messagep); + break; + } + + /* no error, remove submission */ + queue_remove_submission(messagep); + break; + } + + default: + log_debug("queue_dispatch_mda: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + +void +queue_dispatch_lka(int sig, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_LKA]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("queue_dispatch_lka: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + + case IMSG_LKA_ALIAS_RESULT: { + struct message *messagep; + + messagep = imsg.data; + messagep->id = queue_generate_id(); + messagep->batch_id = 0; + queue_record_submission(messagep); + + if (messagep->type & T_MTA_MESSAGE) { + messagep->flags |= F_MESSAGE_READY; + queue_update_database(messagep); + } + + if (messagep->type & T_MDA_MESSAGE) { + imsg_compose(ibuf, IMSG_LKA_FORWARD_LOOKUP, 0, 0, -1, + messagep, sizeof(struct message)); + } + break; + } + + case IMSG_LKA_FORWARD_LOOKUP: { + struct message *messagep; + + messagep = (struct message *)imsg.data; + messagep->id = queue_generate_id(); + messagep->batch_id = 0; + messagep->flags |= F_MESSAGE_READY; + queue_record_submission(messagep); + break; + } + + case IMSG_QUEUE_REMOVE_SUBMISSION: { + struct message *messagep; + + messagep = (struct message *)imsg.data; + queue_remove_submission(messagep); + break; + } + + case IMSG_LKA_MX_LOOKUP: { + queue_batch_resolved(env, imsg.data); + break; + } + default: + log_debug("queue_dispatch_lka: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + +void +queue_shutdown(void) +{ + log_info("queue handler"); + _exit(0); +} + +void +queue_setup_events(struct smtpd *env) +{ + struct timeval tv; + + evtimer_set(&env->sc_ev, queue_timeout, env); + tv.tv_sec = 1; + tv.tv_usec = 0; + evtimer_add(&env->sc_ev, &tv); +} + +void +queue_disable_events(struct smtpd *env) +{ + evtimer_del(&env->sc_ev); +} + +void +queue_timeout(int fd, short event, void *p) +{ + struct smtpd *env = p; + struct batch *batchp, *nxt; + struct timeval tv; + time_t curtime; + + curtime = time(NULL); + queue_load_submissions(env, curtime); + + for (batchp = SPLAY_MIN(batchtree, &env->batch_queue); + batchp != NULL; + batchp = nxt) { + nxt = SPLAY_NEXT(batchtree, &env->batch_queue, batchp); + + if ((batchp->type & T_MTA_BATCH) && + (batchp->flags & F_BATCH_RESOLVED) == 0) + continue; + + batch_send(env, batchp, curtime); + + SPLAY_REMOVE(batchtree, &env->batch_queue, batchp); + bzero(batchp, sizeof(struct batch)); + free(batchp); + + } + + tv.tv_sec = 5; + tv.tv_usec = 0; + evtimer_add(&env->sc_ev, &tv); +} + +void +queue_load_submissions(struct smtpd *env, time_t tm) +{ + DIR *dirp; + struct dirent *dp; + struct batch *batchp; + struct message *messagep; + struct message message; + + dirp = opendir(PATH_ENVELOPES); + if (dirp == NULL) + err(1, "opendir"); + + while ((dp = readdir(dirp)) != NULL) { + + if (dp->d_name[0] == '.') + continue; + + if (! queue_message_from_id(dp->d_name, &message)) + errx(1, "failed to load message"); + + if (! queue_message_schedule(&message, tm)) { + if (message.flags & F_MESSAGE_EXPIRED) { + log_debug("message expired, create mdaemon"); + queue_remove_submission(&message); + } + continue; + } + message.lasttry = tm; + message.flags |= F_MESSAGE_PROCESSING; + queue_update_database(&message); + + messagep = calloc(1, sizeof (struct message)); + if (messagep == NULL) + err(1, "calloc"); + *messagep = message; + + batchp = batch_lookup(env, messagep); + if (batchp != NULL) + messagep->batch_id = batchp->id; + + batchp = queue_record_batch(env, messagep); + if (messagep->batch_id == 0) + messagep->batch_id = batchp->id; + + } + + closedir(dirp); +} + +int +queue_message_from_id(char *message_id, struct message *message) +{ + char pathname[MAXPATHLEN]; + int fd; + int ret; + + if (snprintf(pathname, MAXPATHLEN, "%s/%s", PATH_ENVELOPES, message_id) + >= MAXPATHLEN) { + warnx("queue_load_submissions: filename too long."); + return 0; + } + + fd = open(pathname, O_RDONLY); + if (fd == -1) { + warnx("queue_load_submissions: open: %s", message_id); + goto bad; + } + + ret = atomic_read(fd, message, sizeof(struct message)); + if (ret != sizeof(struct message)) { + warnx("queue_load_submissions: atomic_read: %s", message_id); + goto bad; + } + + close(fd); + return 1; +bad: + if (fd != -1) + close(fd); + return 0; +} + +pid_t +queue(struct smtpd *env) +{ + pid_t pid; + struct passwd *pw; + + struct event ev_sigint; + struct event ev_sigterm; + + struct peer peers[] = { + { PROC_CONTROL, queue_dispatch_control }, + { PROC_SMTP, queue_dispatch_smtp }, + { PROC_MDA, queue_dispatch_mda }, + { PROC_MTA, queue_dispatch_mta }, + { PROC_LKA, queue_dispatch_lka } + }; + + switch (pid = fork()) { + case -1: + fatal("queue: cannot fork"); + case 0: + break; + default: + return (pid); + } + + purge_config(env, PURGE_EVERYTHING); + + pw = env->sc_pw; + +#ifndef DEBUG + if (chroot(PATH_SPOOL) == -1) + fatal("queue: chroot"); + if (chdir("/") == -1) + fatal("queue: chdir(\"/\")"); +#else +#warning disabling privilege revocation and chroot in DEBUG MODE +#endif + + setproctitle("queue handler"); + smtpd_process = PROC_QUEUE; + +#ifndef DEBUG + 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("queue: cannot drop privileges"); +#endif + + event_init(); + + signal_set(&ev_sigint, SIGINT, queue_sig_handler, env); + signal_set(&ev_sigterm, SIGTERM, queue_sig_handler, env); + signal_add(&ev_sigint, NULL); + signal_add(&ev_sigterm, NULL); + signal(SIGPIPE, SIG_IGN); + signal(SIGHUP, SIG_IGN); + + config_peers(env, peers, 5); + + SPLAY_INIT(&env->batch_queue); + + queue_setup_events(env); + event_dispatch(); + queue_shutdown(); + + return (0); +} + +u_int64_t +queue_generate_id(void) +{ + u_int64_t id; + struct timeval tp; + + if (gettimeofday(&tp, NULL) == -1) + fatal("queue_generate_id: time"); + + id = (u_int32_t)tp.tv_sec; + id <<= 32; + id |= (u_int32_t)tp.tv_usec; + usleep(1); + + return (id); +} + +int +queue_create_message_file(char *message_id) +{ + int fd; + char pathname[MAXPATHLEN]; + + if (snprintf(pathname, MAXPATHLEN, "%s/%d.XXXXXXXXXXXXXXXX", + PATH_MESSAGES, time(NULL)) >= MAXPATHLEN) + return -1; + + fd = mkstemp(pathname); + if (fd == -1) + fatal("queue_create_message_file: mkstemp"); + + /* XXX - this won't fail if message_id is MAXPATHLEN bytes */ + if (strlcpy(message_id, pathname + sizeof(PATH_MESSAGES), MAXPATHLEN) + >= MAXPATHLEN) + fatal("queue_create_message_file: message id too long"); + + return fd; +} + +void +queue_delete_message_file(char *message_id) +{ + char pathname[MAXPATHLEN]; + + if (snprintf(pathname, MAXPATHLEN, "%s/%s", PATH_MESSAGES, message_id) + >= MAXPATHLEN) + fatal("queue_delete_message_file: message id too long"); + + if (unlink(pathname) == -1) + fatal("queue_delete_message_file: unlink"); +} + +int +queue_record_submission(struct message *message) +{ + char pathname[MAXPATHLEN]; + char linkname[MAXPATHLEN]; + char dbname[MAXPATHLEN]; + char message_uid[MAXPATHLEN]; + char *spool; + size_t spoolsz; + int fd; + int mode = O_CREAT|O_TRUNC|O_WRONLY|O_EXCL|O_SYNC|O_EXLOCK; + + if (message->type & T_DAEMON_MESSAGE) { + spool = PATH_DAEMON; + } + else { + switch (message->recipient.rule.r_action) { + case A_MBOX: + case A_MAILDIR: + case A_EXT: + spool = PATH_LOCAL; + break; + default: + spool = PATH_RELAY; + } + } + spoolsz = strlen(spool); + + if (snprintf(pathname, MAXPATHLEN, "%s/%s", PATH_MESSAGES, + message->message_id) >= MAXPATHLEN) + fatal("queue_record_submission: message id too long"); + + for (;;) { + if (snprintf(linkname, MAXPATHLEN, "%s/%s.%qu", spool, + message->message_id, (u_int64_t)arc4random()) + >= MAXPATHLEN) + fatal("queue_record_submission: message uid too long"); + + (void)strlcpy(message_uid, linkname + spoolsz + 1, MAXPATHLEN); + + if (link(pathname, linkname) == -1) { + if (errno == EEXIST) + continue; + err(1, "link: %s , %s", pathname, linkname); + } + + if (snprintf(dbname, MAXPATHLEN, "%s/%s", PATH_ENVELOPES, + message_uid) >= MAXPATHLEN) + fatal("queue_record_submission: database uid too long"); + + fd = open(dbname, mode, 0600); + if (fd == -1) + if (unlink(linkname) == -1) + fatal("queue_record_submission: unlink"); + + if (strlcpy(message->message_uid, message_uid, MAXPATHLEN) + >= MAXPATHLEN) + fatal("queue_record_submission: message uid too long"); + + message->creation = time(NULL); + + if (atomic_write(fd, message, sizeof(struct message)) + != sizeof(struct message)) { + close(fd); + return 0; + } + close(fd); + break; + } + return 1; +} + +struct batch * +queue_record_batch(struct smtpd *env, struct message *messagep) +{ + struct batch *batchp; + struct path *path; + + batchp = NULL; + if (messagep->batch_id != 0) { + batchp = batch_by_id(env, messagep->batch_id); + if (batchp == NULL) + errx(1, "%s: internal inconsistency.", __func__); + } + + if (batchp == NULL) { + batchp = calloc(1, sizeof(struct batch)); + if (batchp == NULL) + err(1, "%s: calloc", __func__); + + batchp->id = queue_generate_id(); + batchp->creation = messagep->creation; + + (void)strlcpy(batchp->message_id, messagep->message_id, + sizeof(batchp->message_id)); + + TAILQ_INIT(&batchp->messages); + SPLAY_INSERT(batchtree, &env->batch_queue, batchp); + + if (messagep->type & T_DAEMON_MESSAGE) { + batchp->type = T_DAEMON_BATCH; + path = &messagep->sender; + } + else { + path = &messagep->recipient; + } + + batchp->rule = path->rule; + + (void)strlcpy(batchp->hostname, path->domain, + sizeof(batchp->hostname)); + + if (IS_MAILBOX(path->rule.r_action) || + IS_EXT(path->rule.r_action)) { + batchp->type |= T_MDA_BATCH; + } + else { + batchp->type |= T_MTA_BATCH; + imsg_compose(env->sc_ibufs[PROC_LKA], IMSG_LKA_MX_LOOKUP, 0, 0, -1, + batchp, sizeof(struct batch)); + } + } + + TAILQ_INSERT_TAIL(&batchp->messages, messagep, entry); + + return batchp; +} + +int +queue_remove_submission(struct message *message) +{ + char pathname[MAXPATHLEN]; + char linkname[MAXPATHLEN]; + char dbname[MAXPATHLEN]; + char *spool; + struct stat sb; + + if (message->type & T_DAEMON_MESSAGE) { + spool = PATH_DAEMON; + } + else { + switch (message->recipient.rule.r_action) { + case A_MBOX: + case A_MAILDIR: + case A_EXT: + spool = PATH_LOCAL; + break; + default: + spool = PATH_RELAY; + } + } + + if (snprintf(dbname, MAXPATHLEN, "%s/%s", PATH_ENVELOPES, + message->message_uid) >= MAXPATHLEN) + fatal("queue_remove_submission: database uid too long"); + + if (snprintf(linkname, MAXPATHLEN, "%s/%s", spool, + message->message_uid) >= MAXPATHLEN) + fatal("queue_remove_submission: message uid too long"); + + if (snprintf(pathname, MAXPATHLEN, "%s/%s", PATH_MESSAGES, + message->message_id) >= MAXPATHLEN) + fatal("queue_remove_submission: message id too long"); + + if (unlink(dbname) == -1) { + warnx("dbname: %s", dbname); + fatal("queue_remove_submission: unlink"); + } + + if (unlink(linkname) == -1) { + warnx("linkname: %s", linkname); + fatal("queue_remove_submission: unlink"); + } + + if (stat(pathname, &sb) == -1) { + warnx("pathname: %s", pathname); + fatal("queue_remove_submission: stat"); + } + + if (sb.st_nlink == 1) { + if (unlink(pathname) == -1) { + warnx("pathname: %s", pathname); + fatal("queue_remove_submission: unlink"); + } + } + + return 1; +} + +int +queue_remove_batch_message(struct smtpd *env, struct batch *batchp, struct message *messagep) +{ + TAILQ_REMOVE(&batchp->messages, messagep, entry); + bzero(messagep, sizeof(struct message)); + free(messagep); + + if (TAILQ_FIRST(&batchp->messages) == NULL) { + SPLAY_REMOVE(batchtree, &env->batch_queue, batchp); + bzero(batchp, sizeof(struct batch)); + free(batchp); + return 1; + } + + return 0; +} + +int +queue_batch_resolved(struct smtpd *env, struct batch *lookup) +{ + u_int32_t i; + struct batch *batchp; + + batchp = batch_by_id(env, lookup->id); + batchp->h_errno = lookup->h_errno; + batchp->ss_cnt = lookup->ss_cnt; + +/* + EAI_NODATA no address associated with hostname + EAI_NONAME hostname or servname not provided, or not known + EAI_PROTOCOL resolved protocol is unknown + EAI_SERVICE servname not supported for ai_socktype + EAI_SOCKTYPE ai_socktype not supported + EAI_SYSTEM system error returned in errno + + + */ + + switch (batchp->h_errno) { + case EAI_ADDRFAMILY: + case EAI_BADFLAGS: + case EAI_BADHINTS: + case EAI_FAIL: + case EAI_FAMILY: + case EAI_NODATA: + case EAI_NONAME: + case EAI_SERVICE: + case EAI_SOCKTYPE: + case EAI_SYSTEM: + /* XXX */ + /* + * In the case of a DNS permanent error, do not generate a + * daemon message if the error originates from one already + * as this would cause a loop. Remove the initial batch as + * it will never succeed. + * + */ + return 0; + + case EAI_AGAIN: + case EAI_MEMORY: + /* XXX */ + /* + * Do not generate a daemon message if this error happened + * while processing a daemon message. Do NOT remove batch, + * it may succeed later. + */ + return 0; + + default: + batchp->flags |= F_BATCH_RESOLVED; + for (i = 0; i < batchp->ss_cnt; ++i) + batchp->ss[i] = lookup->ss[i]; + } + return 1; +} + +int +queue_open_message_file(struct batch *batch) +{ + int fd; + char pathname[MAXPATHLEN]; + + if (snprintf(pathname, MAXPATHLEN, "%s/%s", PATH_MESSAGES, + batch->message_id) >= MAXPATHLEN) + fatal("queue_open_message_file: message id too long"); + + fd = open(pathname, O_RDONLY); + if (fd == -1) + fatal("queue_open_message_file: open"); + + return fd; +} + +int +queue_update_database(struct message *message) +{ + int fd; + char *spool; + char pathname[MAXPATHLEN]; + + if (message->type & T_DAEMON_MESSAGE) { + spool = PATH_DAEMON; + } + else { + switch (message->recipient.rule.r_action) { + case A_MBOX: + case A_MAILDIR: + case A_EXT: + spool = PATH_LOCAL; + break; + default: + spool = PATH_RELAY; + } + } + + if (snprintf(pathname, MAXPATHLEN, "%s/%s", PATH_ENVELOPES, + message->message_uid) >= MAXPATHLEN) + fatal("queue_update_database: pathname too long"); + + if ((fd = open(pathname, O_RDWR|O_EXLOCK)) == -1) + fatal("queue_update_database: cannot open database"); + + if (atomic_write(fd, message, sizeof (struct message)) == -1) + fatal("queue_update_database: cannot open database"); + + close(fd); + + return 1; +} + + +int +queue_record_daemon(struct message *message) +{ + char pathname[MAXPATHLEN]; + char linkname[MAXPATHLEN]; + char dbname[MAXPATHLEN]; + char message_uid[MAXPATHLEN]; + size_t spoolsz; + int fd; + int mode = O_CREAT|O_TRUNC|O_WRONLY|O_EXCL|O_SYNC|O_EXLOCK; + + (void)snprintf(pathname, MAXPATHLEN, "%s/%s", + PATH_MESSAGES, message->message_id); + + spoolsz = strlen(PATH_DAEMON); + + for (;;) { + (void)snprintf(linkname, MAXPATHLEN, "%s/%s.%qu", + PATH_DAEMON, message->message_id, (u_int64_t)arc4random()); + (void)strlcpy(message_uid, linkname + spoolsz + 1, MAXPATHLEN); + + if (link(pathname, linkname) == -1) { + if (errno == EEXIST) + continue; + err(1, "link"); + } + + (void)snprintf(dbname, MAXPATHLEN, "%s/%s", + PATH_ENVELOPES, message_uid); + + fd = open(dbname, mode, 0600); + if (fd == -1) + if (unlink(linkname) == -1) + err(1, "unlink"); + + (void)strlcpy(message->message_uid, message_uid, MAXPATHLEN); + + message->creation = time(NULL); + + atomic_write(fd, message, sizeof(*message)); + close(fd); + break; + } + + return 1; +} + + +struct batch * +batch_lookup(struct smtpd *env, struct message *message) +{ + struct batch *batchp; + struct batch lookup; + + /* If message->batch_id != 0, we can retrieve batch by id */ + if (message->batch_id != 0) { + lookup.id = message->batch_id; + return SPLAY_FIND(batchtree, &env->batch_queue, &lookup); + } + + /* We do not know the batch_id yet, maybe it was created but we could not + * be notified, or it just does not exist. Let's scan to see if we can do + * a match based on our message_id and flags. + */ + SPLAY_FOREACH(batchp, batchtree, &env->batch_queue) { + + if (batchp->type != message->type) + continue; + + if (strcasecmp(batchp->message_id, message->message_id) != 0) + continue; + + if (batchp->type & T_MTA_BATCH) + if (strcasecmp(batchp->hostname, message->recipient.domain) != 0) + continue; + + break; + } + + return batchp; +} + +int +batch_cmp(struct batch *s1, struct batch *s2) +{ + /* + * do not return u_int64_t's + */ + if (s1->id < s2->id) + return (-1); + + if (s1->id > s2->id) + return (1); + + return (0); +} + +int +queue_message_schedule(struct message *messagep, time_t tm) +{ + time_t delay; + + /* Batch has been in the queue for too long and expired */ + if (tm - messagep->creation >= SMTPD_QUEUE_EXPIRY) { + messagep->flags |= F_MESSAGE_EXPIRED; + return 0; + } + + if (messagep->retry == 255) { + messagep->flags |= F_MESSAGE_EXPIRED; + return 0; + } + + if ((messagep->flags & F_MESSAGE_READY) == 0) + return 0; + + if ((messagep->flags & F_MESSAGE_PROCESSING) != 0) + return 0; + + if (messagep->lasttry == 0) + return 1; + + delay = SMTPD_QUEUE_MAXINTERVAL; + + if (messagep->type & T_MDA_MESSAGE) { + if (messagep->retry < 5) + return 1; + + if (messagep->retry < 15) + delay = (messagep->retry * 60) + arc4random() % 60; + } + + if (messagep->type & T_MTA_MESSAGE) { + if (messagep->retry < 3) + delay = SMTPD_QUEUE_INTERVAL; + else if (messagep->retry <= 7) { + delay = SMTPD_QUEUE_INTERVAL * (1 << (messagep->retry - 3)); + if (delay > SMTPD_QUEUE_MAXINTERVAL) + delay = SMTPD_QUEUE_MAXINTERVAL; + } + } + + if (tm >= messagep->lasttry + delay) + return 1; + + return 0; +} + +void +batch_unschedule(struct batch *batchp) +{ + batchp->flags &= ~(F_BATCH_SCHEDULED); +} + +void +batch_send(struct smtpd *env, struct batch *batchp, time_t curtime) +{ + u_int8_t proctype; + struct message *messagep; + + if ((batchp->type & (T_MDA_BATCH|T_MTA_BATCH)) == 0) + fatal("batch_send: unknown batch type"); + + if (batchp->type & T_MDA_BATCH) + proctype = PROC_MDA; + else if (batchp->type & T_MTA_BATCH) + proctype = PROC_MTA; + + imsg_compose(env->sc_ibufs[proctype], IMSG_CREATE_BATCH, 0, 0, -1, + batchp, sizeof (struct batch)); + + while ((messagep = TAILQ_FIRST(&batchp->messages))) { + imsg_compose(env->sc_ibufs[proctype], IMSG_BATCH_APPEND, 0, 0, + -1, messagep, sizeof (struct message)); + TAILQ_REMOVE(&batchp->messages, messagep, entry); + bzero(messagep, sizeof(struct message)); + free(messagep); + } + + imsg_compose(env->sc_ibufs[proctype], IMSG_BATCH_CLOSE, 0, 0, -1, + batchp, sizeof(struct batch)); +} + +struct batch * +batch_by_id(struct smtpd *env, u_int64_t id) +{ + struct batch lookup; + + lookup.id = id; + return SPLAY_FIND(batchtree, &env->batch_queue, &lookup); +} + +struct message * +message_by_id(struct smtpd *env, struct batch *batchp, u_int64_t id) +{ + struct message *messagep; + + if (batchp != NULL) { + TAILQ_FOREACH(messagep, &batchp->messages, entry) { + if (messagep->id == id) + break; + } + return messagep; + } + + SPLAY_FOREACH(batchp, batchtree, &env->batch_queue) { + TAILQ_FOREACH(messagep, &batchp->messages, entry) { + if (messagep->id == id) + return messagep; + } + } + return NULL; +} + +SPLAY_GENERATE(batchtree, batch, b_nodes, batch_cmp); diff --git a/usr.sbin/smtpd/smtp.c b/usr.sbin/smtpd/smtp.c new file mode 100644 index 00000000000..01f06953648 --- /dev/null +++ b/usr.sbin/smtpd/smtp.c @@ -0,0 +1,586 @@ +/* + * Copyright (c) 2008 Gilles Chehade <gilles@openbsd.org> + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * + * 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 <ctype.h> +#include <event.h> +#include <fcntl.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "smtpd.h" + +__dead void smtp_shutdown(void); +void smtp_sig_handler(int, short, void *); +void smtp_dispatch_parent(int, short, void *); +void smtp_dispatch_mfa(int, short, void *); +void smtp_dispatch_lka(int, short, void *); +void smtp_dispatch_queue(int, short, void *); +void smtp_setup_events(struct smtpd *); +void smtp_disable_events(struct smtpd *); +void smtp_accept(int, short, void *); +void session_timeout(int, short, void *); + +void +smtp_sig_handler(int sig, short event, void *p) +{ + switch (sig) { + case SIGINT: + case SIGTERM: + smtp_shutdown(); + break; + default: + fatalx("smtp_sig_handler: unexpected signal"); + } +} + +void +smtp_dispatch_parent(int sig, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_PARENT]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("parent_dispatch_smtp: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + case IMSG_CONF_START: + if (env->sc_flags & SMTPD_CONFIGURING) + break; + env->sc_flags |= SMTPD_CONFIGURING; + smtp_disable_events(env); + break; + case IMSG_CONF_SSL: { + struct ssl *s; + struct ssl *x_ssl; + + if (!(env->sc_flags & SMTPD_CONFIGURING)) + break; + + if ((s = calloc(1, sizeof(*s))) == NULL) + fatal(NULL); + x_ssl = imsg.data; + (void)strlcpy(s->ssl_name, x_ssl->ssl_name, + sizeof(s->ssl_name)); + s->ssl_cert_len = x_ssl->ssl_cert_len; + if ((s->ssl_cert = malloc(s->ssl_cert_len + 1)) == NULL) + fatal(NULL); + (void)strlcpy(s->ssl_cert, + (char *)imsg.data + sizeof(*s), + s->ssl_cert_len); + + s->ssl_key_len = x_ssl->ssl_key_len; + if ((s->ssl_key = malloc(s->ssl_key_len + 1)) == NULL) + fatal(NULL); + (void)strlcpy(s->ssl_key, + (char *)imsg.data + (sizeof(*s) + s->ssl_cert_len), + s->ssl_key_len); + + SPLAY_INSERT(ssltree, &env->sc_ssl, s); + break; + } + case IMSG_CONF_LISTENER: { + struct listener *l; + struct ssl key; + + if (!(env->sc_flags & SMTPD_CONFIGURING)) + break; + + if ((l = calloc(1, sizeof(*l))) == NULL) + fatal(NULL); + memcpy(l, imsg.data, sizeof(*l)); + if ((l->fd = imsg_get_fd(ibuf, &imsg)) == -1) + fatal("cannot get fd"); + + log_debug("smtp_dispatch_parent: " + "got fd %d for listener: %p", l->fd, l); + + (void)strlcpy(key.ssl_name, l->ssl_cert_name, + sizeof(key.ssl_name)); + + if (l->flags & F_SSL) + if ((l->ssl = SPLAY_FIND(ssltree, + &env->sc_ssl, &key)) == NULL) + fatal("parent and smtp desynchronized"); + + TAILQ_INSERT_TAIL(&env->sc_listeners, l, entry); + break; + } + case IMSG_CONF_END: + if (!(env->sc_flags & SMTPD_CONFIGURING)) + break; + smtp_setup_events(env); + env->sc_flags &= ~SMTPD_CONFIGURING; + break; + case IMSG_PARENT_AUTHENTICATE: { + struct session *s; + struct session key; + struct session_auth_reply *reply; + + log_debug("smtp_dispatch_parent: parent handled authentication"); + reply = imsg.data; + key.s_id = reply->session_id; + key.s_msg.id = reply->session_id; + + s = SPLAY_FIND(sessiontree, &env->sc_sessions, &key); + if (s == NULL) { + /* Session was removed while we were waiting for the message */ + break; + } + + if (reply->value) + s->s_flags |= F_AUTHENTICATED; + + session_pickup(s, NULL); + + break; + } + default: + log_debug("parent_dispatch_smtp: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + +void +smtp_dispatch_mfa(int sig, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_MFA]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("smtp_dispatch_mfa: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + case IMSG_MFA_RCPT_SUBMIT: + case IMSG_MFA_RPATH_SUBMIT: { + struct submit_status *ss; + struct session *s; + struct session key; + + log_debug("smtp_dispatch_mfa: mfa handled return path"); + ss = imsg.data; + key.s_id = ss->id; + key.s_msg.id = ss->id; + + s = SPLAY_FIND(sessiontree, &env->sc_sessions, &key); + if (s == NULL) { + /* Session was removed while we were waiting for the message */ + break; + } + + session_pickup(s, ss); + break; + } + default: + log_debug("smtp_dispatch_mfa: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + +void +smtp_dispatch_lka(int sig, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_LKA]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("smtp_dispatch_lka: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + case IMSG_SMTP_HOSTNAME_ANSWER: { + struct session key; + struct session *s; + struct session *ss; + + s = imsg.data; + key.s_id = s->s_id; + + ss = SPLAY_FIND(sessiontree, &env->sc_sessions, &key); + if (ss == NULL) { + /* Session was removed while we were waiting for the message */ + break; + } + + strlcpy(ss->s_hostname, s->s_hostname, MAXHOSTNAMELEN); + + break; + } + default: + log_debug("smtp_dispatch_lka: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + +void +smtp_dispatch_queue(int sig, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_QUEUE]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("smtp_dispatch_queue: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + case IMSG_SMTP_MESSAGE_FILE: { + struct submit_status *ss; + struct session *s; + struct session key; + int fd; + + log_debug("smtp_dispatch_queue: queue handled message creation"); + ss = imsg.data; + + key.s_id = ss->id; + + s = SPLAY_FIND(sessiontree, &env->sc_sessions, &key); + if (s == NULL) { + /* Session was removed while we were waiting for the message */ + break; + } + + (void)strlcpy(s->s_msg.message_id, ss->u.msgid, + sizeof(s->s_msg.message_id)); + + fd = imsg_get_fd(ibuf, &imsg); + if (fd != -1) { + s->s_msg.datafp = fdopen(fd, "w"); + if (s->s_msg.datafp == NULL) { + /* no need to handle error, it will be + * caught in session_pickup() + */ + close(fd); + } + } + session_pickup(s, ss); + + break; + } + case IMSG_SMTP_SUBMIT_ACK: { + struct submit_status *ss; + struct session *s; + struct session key; + + log_debug("smtp_dispatch_queue: queue acknowledged message submission"); + ss = imsg.data; + key.s_id = ss->id; + key.s_msg.id = ss->id; + + s = SPLAY_FIND(sessiontree, &env->sc_sessions, &key); + if (s == NULL) { + /* Session was removed while we were waiting for the message */ + break; + } + + (void)strlcpy(s->s_msg.message_id, ss->u.msgid, + sizeof(s->s_msg.message_id)); + + session_pickup(s, ss); + + break; + } + default: + log_debug("smtp_dispatch_queue: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + +void +smtp_shutdown(void) +{ + log_info("smtp server exiting"); + _exit(0); +} + +pid_t +smtp(struct smtpd *env) +{ + pid_t pid; + struct passwd *pw; + + struct event ev_sigint; + struct event ev_sigterm; + + struct peer peers[] = { + { PROC_PARENT, smtp_dispatch_parent }, + { PROC_MFA, smtp_dispatch_mfa }, + { PROC_QUEUE, smtp_dispatch_queue }, + { PROC_LKA, smtp_dispatch_lka } + }; + + switch (pid = fork()) { + case -1: + fatal("smtp: cannot fork"); + case 0: + break; + default: + return (pid); + } + + ssl_init(); + purge_config(env, PURGE_EVERYTHING); + + pw = env->sc_pw; + +#ifndef DEBUG + if (chroot(pw->pw_dir) == -1) + fatal("smtp: chroot"); + if (chdir("/") == -1) + fatal("smtp: chdir(\"/\")"); +#else +#warning disabling privilege revocation and chroot in DEBUG MODE +#endif + + setproctitle("smtp server"); + smtpd_process = PROC_SMTP; + +#ifndef DEBUG + 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("smtp: cannot drop privileges"); +#endif + + event_init(); + + signal_set(&ev_sigint, SIGINT, smtp_sig_handler, env); + signal_set(&ev_sigterm, SIGTERM, smtp_sig_handler, env); + signal_add(&ev_sigint, NULL); + signal_add(&ev_sigterm, NULL); + signal(SIGPIPE, SIG_IGN); + signal(SIGHUP, SIG_IGN); + + config_peers(env, peers, 4); + + smtp_setup_events(env); + event_dispatch(); + smtp_shutdown(); + + return (0); +} + +void +smtp_setup_events(struct smtpd *env) +{ + struct listener *l; + struct timeval tv; + + TAILQ_FOREACH(l, &env->sc_listeners, entry) { + log_debug("smtp_setup_events: configuring listener: %p%s.", + l, (l->flags & F_SSL)?" (with ssl)":""); + + if (fcntl(l->fd, F_SETFL, O_NONBLOCK) == -1) + fatal("fcntl"); + if (listen(l->fd, l->backlog) == -1) + fatal("listen"); + l->env = env; + event_set(&l->ev, l->fd, EV_READ, smtp_accept, l); + event_add(&l->ev, NULL); + ssl_setup(env, l); + } + + evtimer_set(&env->sc_ev, session_timeout, env); + tv.tv_sec = 1; + tv.tv_usec = 0; + evtimer_add(&env->sc_ev, &tv); +} + +void +smtp_disable_events(struct smtpd *env) +{ + struct listener *l; + + log_debug("smtp_disable_events: closing listening sockets"); + while ((l = TAILQ_FIRST(&env->sc_listeners)) != NULL) { + TAILQ_REMOVE(&env->sc_listeners, l, entry); + event_del(&l->ev); + close(l->fd); + free(l); + } + TAILQ_INIT(&env->sc_listeners); +} + +void +smtp_accept(int fd, short event, void *p) +{ + int s_fd; + struct sockaddr_storage ss; + struct listener *l = p; + struct session *s; + socklen_t len; + + log_debug("smtp_accept: incoming client on listener: %p", l); + len = sizeof(struct sockaddr_storage); + if ((s_fd = accept(l->fd, (struct sockaddr *)&ss, &len)) == -1) { + event_add(&l->ev, NULL); + return; + } + + if ((s = calloc(1, sizeof(*s))) == NULL) + fatal(NULL); + len = sizeof(s->s_ss); + + s->s_fd = s_fd; + s->s_tm = time(NULL); + (void)memcpy(&s->s_ss, &ss, sizeof(s->s_ss)); + + session_init(l, s); + event_add(&l->ev, NULL); +} + +void +smtp_listener_setup(struct smtpd *env, struct listener *l) +{ + int opt; + + if ((l->fd = socket(l->ss.ss_family, SOCK_STREAM, 0)) == -1) + fatal("socket"); + + opt = 1; + setsockopt(l->fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + if (bind(l->fd, (struct sockaddr *)&l->ss, l->ss.ss_len) == -1) + fatal("bind"); +} diff --git a/usr.sbin/smtpd/smtp_session.c b/usr.sbin/smtpd/smtp_session.c new file mode 100644 index 00000000000..cfca2f0a99e --- /dev/null +++ b/usr.sbin/smtpd/smtp_session.c @@ -0,0 +1,955 @@ +/* + * Copyright (c) 2008 Gilles Chehade <gilles@openbsd.org> + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * + * 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 <netinet/in.h> +#include <arpa/inet.h> + +#include <ctype.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "smtpd.h" + +int session_rfc5321_helo_handler(struct session *, char *); +int session_rfc5321_ehlo_handler(struct session *, char *); +int session_rfc5321_rset_handler(struct session *, char *); +int session_rfc5321_noop_handler(struct session *, char *); +int session_rfc5321_data_handler(struct session *, char *); +int session_rfc5321_mail_handler(struct session *, char *); +int session_rfc5321_rcpt_handler(struct session *, char *); +int session_rfc5321_vrfy_handler(struct session *, char *); +int session_rfc5321_expn_handler(struct session *, char *); +int session_rfc5321_turn_handler(struct session *, char *); +int session_rfc5321_help_handler(struct session *, char *); +int session_rfc5321_quit_handler(struct session *, char *); +int session_rfc5321_none_handler(struct session *, char *); + +int session_rfc1652_mail_handler(struct session *, char *); + +int session_rfc3207_stls_handler(struct session *, char *); + +int session_rfc4954_auth_handler(struct session *, char *); + +void session_command(struct session *, char *, char *); +int session_set_path(struct path *, char *); +void session_timeout(int, short, void *); + +struct session_timeout { + enum session_state state; + time_t timeout; +}; + +struct session_timeout rfc5321_timeouttab[] = { + { S_INIT, 300 }, + { S_GREETED, 300 }, + { S_HELO, 300 }, + { S_MAIL, 300 }, + { S_RCPT, 300 }, + { S_DATA, 120 }, + { S_DATACONTENT, 180 }, + { S_DONE, 600 } +}; + +struct session_cmd { + char *name; + int (*func)(struct session *, char *); +}; + +struct session_cmd rfc5321_cmdtab[] = { + { "helo", session_rfc5321_helo_handler }, + { "ehlo", session_rfc5321_ehlo_handler }, + { "rset", session_rfc5321_rset_handler }, + { "noop", session_rfc5321_noop_handler }, + { "data", session_rfc5321_data_handler }, + { "mail from", session_rfc5321_mail_handler }, + { "rcpt to", session_rfc5321_rcpt_handler }, + { "vrfy", session_rfc5321_vrfy_handler }, + { "expn", session_rfc5321_expn_handler }, + { "turn", session_rfc5321_turn_handler }, + { "help", session_rfc5321_help_handler }, + { "quit", session_rfc5321_quit_handler } +}; + +struct session_cmd rfc1652_cmdtab[] = { + { "mail from", session_rfc1652_mail_handler }, +}; + +struct session_cmd rfc3207_cmdtab[] = { + { "starttls", session_rfc3207_stls_handler } +}; + +struct session_cmd rfc4954_cmdtab[] = { + { "auth", session_rfc4954_auth_handler } +}; + +int +session_rfc3207_stls_handler(struct session *s, char *args) +{ + if (s->s_state == S_GREETED) { + evbuffer_add_printf(s->s_bev->output, + "503 Polite people say HELO first\r\n"); + return 1; + } + + if (args != NULL) { + evbuffer_add_printf(s->s_bev->output, + "501 Syntax error (no parameters allowed)\r\n"); + return 1; + } + + evbuffer_add_printf(s->s_bev->output, + "220 Ready to start TLS\r\n"); + + s->s_state = S_TLS; + + return 1; +} + +int +session_rfc4954_auth_handler(struct session *s, char *args) +{ + char *method; + char *eom; + struct session_auth_req req; + + if (s->s_state == S_GREETED) { + evbuffer_add_printf(s->s_bev->output, + "503 Polite people say HELO first\r\n"); + return 1; + } + + if (args == NULL) { + evbuffer_add_printf(s->s_bev->output, + "501 Syntax error (no parameters given)\r\n"); + return 1; + } + + method = args; + eom = strchr(args, ' '); + if (eom == NULL) + eom = strchr(args, '\t'); + if (eom != NULL) + *eom++ = '\0'; + + if (eom == NULL) { + /* NEEDS_FIX - unsupported yet */ + evbuffer_add_printf(s->s_bev->output, + "501 Syntax error\r\n"); + return 1; + } + + req.session_id = s->s_id; + if (strlcpy(req.buffer, eom, sizeof(req.buffer)) >= + sizeof(req.buffer)) { + evbuffer_add_printf(s->s_bev->output, + "501 Syntax error\r\n"); + return 1; + } + + s->s_state = S_AUTH; + + imsg_compose(s->s_env->sc_ibufs[PROC_PARENT], IMSG_PARENT_AUTHENTICATE, + 0, 0, -1, &req, sizeof(req)); + + return 1; +} + +int +session_rfc1652_mail_handler(struct session *s, char *args) +{ + char *body; + + if (s->s_state == S_GREETED) { + evbuffer_add_printf(s->s_bev->output, + "503 Polite people say HELO first\r\n"); + return 1; + } + + body = strrchr(args, ' '); + if (body != NULL) { + *body++ = '\0'; + + if (strcasecmp("body=7bit", body) == 0) { + s->s_flags &= ~F_8BITMIME; + } + + else if (strcasecmp("body=8bitmime", body) != 0) { + evbuffer_add_printf(s->s_bev->output, + "503 Invalid BODY\r\n"); + return 1; + } + + return session_rfc5321_mail_handler(s, args); + } + + return 0; +} + +int +session_rfc5321_helo_handler(struct session *s, char *args) +{ + void *p; + char addrbuf[INET6_ADDRSTRLEN]; + + if (args == NULL) { + evbuffer_add_printf(s->s_bev->output, + "501 HELO requires domain address.\r\n"); + return 1; + } + + if (strlcpy(s->s_msg.session_helo, args, sizeof(s->s_msg.session_helo)) + >= sizeof(s->s_msg.session_helo)) { + evbuffer_add_printf(s->s_bev->output, + "501 Invalid domain name\r\n"); + return 1; + } + + s->s_state = S_HELO; + + if (s->s_ss.ss_family == PF_INET) { + struct sockaddr_in *ssin = (struct sockaddr_in *)&s->s_ss; + p = &ssin->sin_addr.s_addr; + } + if (s->s_ss.ss_family == PF_INET6) { + struct sockaddr_in6 *ssin6 = (struct sockaddr_in6 *)&s->s_ss; + p = &ssin6->sin6_addr.s6_addr; + } + + bzero(addrbuf, sizeof (addrbuf)); + inet_ntop(s->s_ss.ss_family, p, addrbuf, sizeof (addrbuf)); + + evbuffer_add_printf(s->s_bev->output, + "250 %s Hello %s [%s%s], pleased to meet you\r\n", + s->s_env->sc_hostname, args, + s->s_ss.ss_family == PF_INET ? "" : "IPv6:", addrbuf); + + return 1; +} + +int +session_rfc5321_ehlo_handler(struct session *s, char *args) +{ + void *p; + char addrbuf[INET6_ADDRSTRLEN]; + + if (args == NULL) { + evbuffer_add_printf(s->s_bev->output, + "501 HELO requires domain address.\r\n"); + return 1; + } + + if (strlcpy(s->s_msg.session_helo, args, sizeof(s->s_msg.session_helo)) + >= sizeof(s->s_msg.session_helo)) { + evbuffer_add_printf(s->s_bev->output, + "501 Invalid domain name\r\n"); + return 1; + } + + s->s_state = S_HELO; + s->s_flags |= F_8BITMIME; + + if (s->s_ss.ss_family == PF_INET) { + struct sockaddr_in *ssin = (struct sockaddr_in *)&s->s_ss; + p = &ssin->sin_addr.s_addr; + } + if (s->s_ss.ss_family == PF_INET6) { + struct sockaddr_in6 *ssin6 = (struct sockaddr_in6 *)&s->s_ss; + p = &ssin6->sin6_addr.s6_addr; + } + + bzero(addrbuf, sizeof (addrbuf)); + inet_ntop(s->s_ss.ss_family, p, addrbuf, sizeof (addrbuf)); + evbuffer_add_printf(s->s_bev->output, + "250-%s Hello %s [%s%s], pleased to meet you\r\n", + s->s_env->sc_hostname, args, + s->s_ss.ss_family == PF_INET ? "" : "IPv6:", addrbuf); + + evbuffer_add_printf(s->s_bev->output, "250-8BITMIME\r\n"); + + /* only advertise starttls if listener can support it */ + if (s->s_l->flags & F_STARTTLS) + evbuffer_add_printf(s->s_bev->output, "250-STARTTLS\r\n"); + + /* only advertise auth if session is secure */ + /* + if (s->s_flags & F_SECURE) + evbuffer_add_printf(s->s_bev->output, "250-AUTH %s\r\n", "PLAIN"); + */ + evbuffer_add_printf(s->s_bev->output, "250 HELP\r\n"); + + return 1; +} + +int +session_rfc5321_rset_handler(struct session *s, char *args) +{ + struct path *pathp; + + while ((pathp = TAILQ_FIRST(&s->s_msg.recipients)) != NULL) { + TAILQ_REMOVE(&s->s_msg.recipients, pathp, entry); + free(pathp); + } + + s->s_msg.rcptcount = 0; + s->s_state = S_HELO; + evbuffer_add_printf(s->s_bev->output, "250 Reset state.\r\n"); + + return 1; +} + +int +session_rfc5321_noop_handler(struct session *s, char *args) +{ + evbuffer_add_printf(s->s_bev->output, "250 OK.\r\n"); + + return 1; +} + +int +session_rfc5321_mail_handler(struct session *s, char *args) +{ + struct path *pathp; + char buffer[MAX_PATH_SIZE]; + + if (s->s_state == S_GREETED) { + evbuffer_add_printf(s->s_bev->output, + "503 Polite people say HELO first\r\n"); + return 1; + } + + if (strlcpy(buffer, args, sizeof(buffer)) >= sizeof(buffer)) { + evbuffer_add_printf(s->s_bev->output, + "%d %s\r\n", 553, "Syntax error for sender address"); + return 1; + } + + if (! session_set_path(&s->s_msg.sender, buffer)) { + /* No need to even transmit to MFA, path is invalid */ + evbuffer_add_printf(s->s_bev->output, + "%d %s\r\n", 553, "Syntax error for sender address"); + return 1; + } + + while ((pathp = TAILQ_FIRST(&s->s_msg.recipients)) != NULL) { + TAILQ_REMOVE(&s->s_msg.recipients, pathp, entry); + free(pathp); + } + s->s_msg.rcptcount = 0; + + s->s_state = S_MAIL; + s->s_flags |= F_IMSG_SENT; + s->s_msg.id = s->s_id; + + log_debug("session_mail_handler: sending notification to mfa"); + + imsg_compose(s->s_env->sc_ibufs[PROC_MFA], IMSG_MFA_RPATH_SUBMIT, + 0, 0, -1, &s->s_msg, sizeof(s->s_msg)); + + return 1; +} + +int +session_rfc5321_rcpt_handler(struct session *s, char *args) +{ + char buffer[MAX_PATH_SIZE]; + struct message_recipient mr; + + if (s->s_state == S_GREETED) { + evbuffer_add_printf(s->s_bev->output, + "503 Polite people say HELO first\r\n"); + return 1; + } + + if (s->s_state == S_HELO) { + evbuffer_add_printf(s->s_bev->output, + "503 Need MAIL before RCPT\r\n"); + return 1; + } + + bzero(&mr, sizeof(mr)); + + if (strlcpy(buffer, args, sizeof(buffer)) >= sizeof(buffer)) { + evbuffer_add_printf(s->s_bev->output, + "%d %s\r\n", 553, "Syntax error for recipient address"); + return 1; + } + + if (! session_set_path(&mr.path, buffer)) { + /* No need to even transmit to MFA, path is invalid */ + evbuffer_add_printf(s->s_bev->output, + "%d %s\r\n", 553, "Syntax error for recipient address"); + return 1; + } + + mr.id = s->s_msg.id; + + s->s_state = S_RCPT; + s->s_flags |= F_IMSG_SENT; + + mr.ss = s->s_ss; + + imsg_compose(s->s_env->sc_ibufs[PROC_MFA], IMSG_MFA_RCPT_SUBMIT, + 0, 0, -1, &mr, sizeof(mr)); + + return 1; +} + +int +session_rfc5321_quit_handler(struct session *s, char *args) +{ + evbuffer_add_printf(s->s_bev->output, "221 %s Closing connection.\r\n", + s->s_env->sc_hostname); + + s->s_flags |= F_QUIT; + + return 1; +} + +int +session_rfc5321_data_handler(struct session *s, char *args) +{ + if (s->s_state == S_GREETED) { + evbuffer_add_printf(s->s_bev->output, + "503 Polite people say HELO first\r\n"); + return 1; + } + + if (s->s_state == S_HELO) { + evbuffer_add_printf(s->s_bev->output, + "503 Need MAIL before DATA\r\n"); + return 1; + } + + if (s->s_state == S_MAIL) { + evbuffer_add_printf(s->s_bev->output, + "503 Need RCPT before DATA\r\n"); + return 1; + } + + s->s_state = S_DATA; + s->s_flags |= F_IMSG_SENT; + s->s_msg.session_id = s->s_id; + s->s_msg.session_ss = s->s_ss; + + imsg_compose(s->s_env->sc_ibufs[PROC_QUEUE], + IMSG_QUEUE_CREATE_MESSAGE_FILE, 0, 0, -1, &s->s_msg, + sizeof(s->s_msg)); + + return 1; +} + +int +session_rfc5321_vrfy_handler(struct session *s, char *args) +{ + evbuffer_add_printf(s->s_bev->output, + "252 Cannot VRFY user; try RCPT to attempt delivery.\r\n"); + + return 1; +} + +int +session_rfc5321_expn_handler(struct session *s, char *args) +{ + evbuffer_add_printf(s->s_bev->output, + "502 Sorry, we do not allow this operation.\r\n"); + + return 1; +} + +int +session_rfc5321_turn_handler(struct session *s, char *args) +{ + evbuffer_add_printf(s->s_bev->output, + "502 Sorry, we do not allow this operation.\r\n"); + + return 1; +} + +int +session_rfc5321_help_handler(struct session *s, char *args) +{ + evbuffer_add_printf(s->s_bev->output, + "214- This is OpenSMTPD\r\n" + "214- To report bugs in the implementation, please contact\r\n" + "214- bugs@poolp.org\r\n"); + evbuffer_add_printf(s->s_bev->output, + "214 End of HELP info\r\n"); + + return 1; +} + +void +session_command(struct session *s, char *cmd, char *args) +{ + int i; + + /* RFC 1652 - 8BITMIME */ + for (i = 0; i < (int)(sizeof(rfc1652_cmdtab) / sizeof(struct session_cmd)); ++i) + if (strcasecmp(rfc1652_cmdtab[i].name, cmd) == 0) + break; + if (i < (int)(sizeof(rfc1652_cmdtab) / sizeof(struct session_cmd))) { + if (rfc1652_cmdtab[i].func(s, args)) + return; + } + + /* RFC 3207 - STARTTLS */ + for (i = 0; i < (int)(sizeof(rfc3207_cmdtab) / sizeof(struct session_cmd)); ++i) + if (strcasecmp(rfc3207_cmdtab[i].name, cmd) == 0) + break; + if (i < (int)(sizeof(rfc3207_cmdtab) / sizeof(struct session_cmd))) { + if (rfc3207_cmdtab[i].func(s, args)) + return; + } + + /* RFC 4954 - AUTH */ + /* + for (i = 0; i < (int)(sizeof(rfc4954_cmdtab) / sizeof(struct session_cmd)); ++i) + if (strcasecmp(rfc4954_cmdtab[i].name, cmd) == 0) + break; + if (i < (int)(sizeof(rfc4954_cmdtab) / sizeof(struct session_cmd))) { + if (rfc4954_cmdtab[i].func(s, args)) + return; + } + */ + + /* RFC 5321 - SMTP */ + for (i = 0; i < (int)(sizeof(rfc5321_cmdtab) / sizeof(struct session_cmd)); ++i) + if (strcasecmp(rfc5321_cmdtab[i].name, cmd) == 0) + break; + if (i < (int)(sizeof(rfc5321_cmdtab) / sizeof(struct session_cmd))) { + if (rfc5321_cmdtab[i].func(s, args)) + return; + } + + evbuffer_add_printf(s->s_bev->output, + "500 Command unrecognized.\r\n"); +} + +void +session_pickup(struct session *s, struct submit_status *ss) +{ + struct path *path; + + if (s == NULL) + fatal("session_pickup: desynchronized"); + + bufferevent_disable(s->s_bev, EV_READ); + bufferevent_enable(s->s_bev, EV_WRITE); + + switch (s->s_state) { + case S_INIT: + s->s_state = S_GREETED; + log_debug("session_pickup: greeting client"); + evbuffer_add_printf(s->s_bev->output, + SMTPD_BANNER, s->s_env->sc_hostname); + break; + + case S_GREETED: + case S_HELO: + break; + + case S_TLS: + s->s_state = S_GREETED; + ssl_session_init(s); + bufferevent_disable(s->s_bev, EV_READ|EV_WRITE); + break; + + case S_AUTH: + if (s->s_flags & F_AUTHENTICATED) { + evbuffer_add_printf(s->s_bev->output, + "235 Authentication Succeeded\r\n"); + } + else { + evbuffer_add_printf(s->s_bev->output, + "535 Authentication Credentials Invalid\r\n"); + } + break; + + case S_MAIL: + /* sender was not accepted, downgrade state */ + if (ss->code != 250) { + s->s_state = S_HELO; + evbuffer_add_printf(s->s_bev->output, + "%d Sender rejected\r\n", ss->code); + return; + } + + s->s_msg.sender = ss->u.path; + evbuffer_add_printf(s->s_bev->output, + "%d Sender ok\r\n", ss->code); + break; + + case S_RCPT: + /* recipient was not accepted */ + if (ss->code != 250) { + /* We do not have a valid recipient, downgrade state */ + if (s->s_msg.rcptcount == 0) + s->s_state = S_MAIL; + evbuffer_add_printf(s->s_bev->output, + "%d %s\r\n", ss->code, "Recipient rejected"); + return; + } + + path = calloc(1, sizeof(struct path)); + if (path == NULL) + err(1, "calloc"); + *path = ss->u.path; + TAILQ_INSERT_TAIL(&s->s_msg.recipients, path, entry); + s->s_msg.rcptcount++; + evbuffer_add_printf(s->s_bev->output, + "%d Recipient ok\r\n", ss->code); + break; + + case S_DATA: + if (s->s_msg.datafp == NULL) { + /* Remove message file */ + imsg_compose(s->s_env->sc_ibufs[PROC_QUEUE], IMSG_QUEUE_DELETE_MESSAGE_FILE, + 0, 0, -1, &s->s_msg, sizeof(s->s_msg)); + evbuffer_add_printf(s->s_bev->output, + "%d %s\r\n", 421, + "Service temporarily unavailable"); + return; + } + s->s_state = S_DATACONTENT; + evbuffer_add_printf(s->s_bev->output, + "%d %s\r\n", 354, + "Enter mail, end with \".\" on a line by itself"); + break; + + case S_DATACONTENT: + break; + + case S_DONE: + s->s_msg.rcptcount--; + if (s->s_msg.rcptcount) + return; + + s->s_state = S_HELO; + s->s_msg.datafp = NULL; + evbuffer_add_printf(s->s_bev->output, + "250 %s Message accepted for delivery\r\n", + s->s_msg.message_id); + + break; + + default: + log_debug("session_pickup: state value: %d", s->s_state); + fatal("session_pickup: unknown state"); + break; + } +} + +void +session_init(struct listener *l, struct session *s) +{ + s->s_state = S_INIT; + s->s_env = l->env; + s->s_l = l; + s->s_id = queue_generate_id(); + + strlcpy(s->s_hostname, "<unknown>", MAXHOSTNAMELEN); + + TAILQ_INIT(&s->s_msg.recipients); + + SPLAY_INSERT(sessiontree, &s->s_env->sc_sessions, s); + + imsg_compose(s->s_env->sc_ibufs[PROC_LKA], IMSG_LKA_HOSTNAME_LOOKUP, + 0, 0, -1, s, sizeof(struct session)); + + if ((s->s_bev = bufferevent_new(s->s_fd, session_read, session_write, + session_error, s)) == NULL) + fatal(NULL); + + if (l->flags & F_SSMTP) { + log_debug("session_init: initializing ssl"); + ssl_session_init(s); + return; + } + + session_pickup(s, NULL); +} + +void +session_read(struct bufferevent *bev, void *p) +{ + struct session *s = p; + char *line; + char *ep; + char *args; + +read: + s->s_tm = time(NULL); + s->s_flags &= ~F_IMSG_SENT; + line = evbuffer_readline(bev->input); + if (line == NULL) { + bufferevent_disable(s->s_bev, EV_READ); + bufferevent_enable(s->s_bev, EV_WRITE); + return; + } + + if (s->s_state == S_DATACONTENT) { + line[strcspn(line, "\r")] = '\0'; + if (strcmp(line, ".") == 0) { + s->s_state = S_DONE; + fclose(s->s_msg.datafp); + + if (s->s_msg.status & S_MESSAGE_PERMFAILURE) { + evbuffer_add_printf(s->s_bev->output, + "554 Transaction failed\r\n"); + + s->s_msg.datafp = NULL; + /* Remove message file */ + imsg_compose(s->s_env->sc_ibufs[PROC_QUEUE], IMSG_QUEUE_DELETE_MESSAGE_FILE, + 0, 0, -1, &s->s_msg, sizeof(s->s_msg)); + bufferevent_enable(s->s_bev, EV_WRITE); + free(line); + return; + } + bufferevent_disable(s->s_bev, EV_READ); + session_msg_submit(s); + free(line); + return; + } else { + size_t i; + size_t len; + + len = strlen(line); + fwrite(line, len, 1, s->s_msg.datafp); + fwrite("\n", 1, 1, s->s_msg.datafp); + fflush(s->s_msg.datafp); + + if (! (s->s_flags & F_8BITMIME)) { + for (i = 0; i < len; ++i) { + if (line[i] & 0x80) { + s->s_msg.status |= S_MESSAGE_PERMFAILURE; + strlcpy(s->s_msg.session_errorline, "8BIT data transfered over 7BIT limited channel", + sizeof s->s_msg.session_errorline); + } + } + } + } + goto read; + } + bufferevent_disable(s->s_bev, EV_READ); + bufferevent_enable(s->s_bev, EV_WRITE); + + line[strcspn(line, "\r")] = '\0'; + if ((ep = strchr(line, ':')) == NULL) + ep = strchr(line, ' '); + if (ep != NULL) { + *ep = '\0'; + args = ++ep; + while (isspace((int)*args)) + args++; + } else + args = NULL; + log_debug("command: %s\targs: %s", line, args); + session_command(s, line, args); + free(line); + return; +} + +void +session_write(struct bufferevent *bev, void *p) +{ + struct session *s = p; + + if (!(s->s_flags & F_QUIT)) { + if (! EVBUFFER_LENGTH(EVBUFFER_OUTPUT(bev))) { + bufferevent_disable(s->s_bev, EV_WRITE); + bufferevent_enable(s->s_bev, EV_READ); + } + + if (s->s_state == S_TLS) + session_pickup(s, NULL); + + return; + } + + if (! EVBUFFER_LENGTH(EVBUFFER_OUTPUT(bev))) { + session_destroy(s); + } +} + +void +session_destroy(struct session *s) +{ + struct path *pathp; + + /* + * cleanup + */ + log_debug("session_destroy: killing client"); + close(s->s_fd); + + if (s->s_msg.datafp != NULL) { + fclose(s->s_msg.datafp); + s->s_msg.datafp = NULL; + /* Remove message file */ + imsg_compose(s->s_env->sc_ibufs[PROC_QUEUE], IMSG_QUEUE_DELETE_MESSAGE_FILE, + 0, 0, -1, &s->s_msg, sizeof(s->s_msg)); + } + + if (s->s_bev != NULL) { + bufferevent_disable(s->s_bev, EV_READ|EV_WRITE); + bufferevent_free(s->s_bev); + } + ssl_session_destroy(s); + + while ((pathp = TAILQ_FIRST(&s->s_msg.recipients)) != NULL) { + TAILQ_REMOVE(&s->s_msg.recipients, pathp, entry); + free(pathp); + } + + SPLAY_REMOVE(sessiontree, &s->s_env->sc_sessions, s); + bzero(s, sizeof(*s)); + free(s); +} + +void +session_error(struct bufferevent *bev, short event, void *p) +{ + struct session *s = p; + + session_destroy(s); +} + +void +session_msg_submit(struct session *s) +{ + struct path *rpath; + + strlcpy(s->s_msg.session_hostname, s->s_hostname, MAXHOSTNAMELEN); + TAILQ_FOREACH(rpath, &s->s_msg.recipients, entry) { + s->s_msg.recipient = *rpath; + imsg_compose(s->s_env->sc_ibufs[PROC_QUEUE], + IMSG_QUEUE_MESSAGE_SUBMIT, 0, 0, -1, &s->s_msg, + sizeof(s->s_msg)); + } +} + +int +session_cmp(struct session *s1, struct session *s2) +{ + /* + * do not return u_int64_t's + */ + if (s1->s_id < s2->s_id) + return (-1); + + if (s1->s_id > s2->s_id) + return (1); + + return (0); +} + +int +session_set_path(struct path *path, char *line) +{ + size_t len; + char *username; + char *hostname; + + len = strlen(line); + if (*line != '<' || line[len - 1] != '>') + return 0; + line[len - 1] = '\0'; + + username = line + 1; + hostname = strchr(username, '@'); + + if (username[0] == '\0') { + *path->user = '\0'; + *path->domain = '\0'; + return 1; + } + + if (hostname == NULL) { + if (strcasecmp(username, "postmaster") != 0) + return 0; + hostname = "localhost"; + } else { + *hostname++ = '\0'; + } + + if (strlcpy(path->user, username, sizeof(path->user)) + >= MAX_LOCALPART_SIZE) + return 0; + + if (strlcpy(path->domain, hostname, sizeof(path->domain)) + >= MAX_DOMAINPART_SIZE) + return 0; + + return 1; +} + +void +session_timeout(int fd, short event, void *p) +{ + struct smtpd *env = p; + struct session *sessionp; + struct session *rmsession; + struct timeval tv; + time_t tm; + u_int8_t i; + + tm = time(NULL); + rmsession = NULL; + SPLAY_FOREACH(sessionp, sessiontree, &env->sc_sessions) { + + if (rmsession != NULL) + session_destroy(rmsession); + + for (i = 0; i < sizeof (rfc5321_timeouttab) / + sizeof(struct session_timeout); ++i) + if (rfc5321_timeouttab[i].state == sessionp->s_state) + break; + + if (i == sizeof (rfc5321_timeouttab) / sizeof (struct session_timeout)) { + if (tm - SMTPD_SESSION_TIMEOUT < sessionp->s_tm) + continue; + } + else if (tm - rfc5321_timeouttab[i].timeout < sessionp->s_tm) { + continue; + } + + rmsession = sessionp; + } + + if (rmsession != NULL) + session_destroy(rmsession); + + tv.tv_sec = 1; + tv.tv_usec = 0; + evtimer_add(&env->sc_ev, &tv); +} + + +SPLAY_GENERATE(sessiontree, session, s_nodes, session_cmp); diff --git a/usr.sbin/smtpd/smtpd.8 b/usr.sbin/smtpd/smtpd.8 new file mode 100644 index 00000000000..e0632198d15 --- /dev/null +++ b/usr.sbin/smtpd/smtpd.8 @@ -0,0 +1,99 @@ +.\" $OpenBSD: smtpd.8,v 1.1 2008/11/01 21:35:28 gilles Exp $ +.\" +.\" Copyright (c) 2008, Gilles Chehade <gilles@openbsd.org> +.\" Copyright (c) 2008, Pierre-Yves Ritschard <pyr@openbsd.org> +.\" +.\" 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. +.\" +.Dd $Mdocdate: November 1 2008 $ +.Dt SMTPD 8 +.Os +.Sh NAME +.Nm smtpd +.Nd "Simple Mail Transfer Protocol daemon" +.Sh SYNOPSIS +.Nm +.Op Fl dnv +.Oo Fl D Ar macro Ns = +.Ar value Oc +.Op Fl f Ar file +.Sh DESCRIPTION +.Nm +is a Simple Mail Transfer Protocol +.Pq SMTP +daemon which can be used as a machine's primary mail system. +.Nm +can listen on a network interface and handle +.Pq SMTP +transactions, it can also be fed messages through the standard +.Xr sendmail 8 +interface. +.Nm +can relay messages through remote mail transfer agents or store them +locally using either the mbox or maildir format. +This implementation supports SMTP as defined by RFC5321 as well as several +extensions. +A running +.Nm +can be controlled through +.Xr smtpctl 8 +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl D Ar macro Ns = Ns Ar value +Define +.Ar macro +to be set to +.Ar value +on the command line. +Overrides the definition of +.Ar macro +in the configuration file. +.It Fl d +Do not daemonize. +If this option is specified, +.Nm +will run in the foreground and log to +.Em stderr . +.It Fl f Ar file +Specify an altenative configuration file. +.It Fl n +Configtest mode. +Only check the configuration file for validity. +.It Fl v +Produce more verbose output. +.El +.Sh FILES +.Bl -tag -width "/var/run/smtpd.sockXX" -compact +.It /etc/mail/smtpd.conf +Default +.Nm +configuration file. +.It /var/run/smtpd.sock +Unix-domain socket used for communication with +.Xr smtpctl 8 . +.El +.Sh SEE ALSO +.Xr smtpd.conf 5 , +.Xr smtpctl 8 , +.Xr smtpdb 8 +.Rs +.%R RFC 5321 +.%T "SMTP" +.%D April 2001 +.Re +.Sh HISTORY +The +.Nm +program first appeared in +.Ox 4.4 . diff --git a/usr.sbin/smtpd/smtpd.c b/usr.sbin/smtpd/smtpd.c new file mode 100644 index 00000000000..05d2f6f550b --- /dev/null +++ b/usr.sbin/smtpd/smtpd.c @@ -0,0 +1,1005 @@ +/* + * Copyright (c) 2008 Gilles Chehade <gilles@openbsd.org> + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * + * 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 <err.h> +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <paths.h> +#include <pwd.h> +#include <signal.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sysexits.h> +#include <unistd.h> + +#include "smtpd.h" + +__dead void usage(void); +void parent_shutdown(void); +void parent_send_config(int, short, void *); +void parent_dispatch_lka(int, short, void *); +void parent_dispatch_mda(int, short, void *); +void parent_dispatch_mfa(int, short, void *); +void parent_dispatch_smtp(int, short, void *); +void parent_sig_handler(int, short, void *); +int parent_open_message_file(struct batch *); +int parent_open_mailbox(struct batch *, struct path *); +int parent_open_filename(struct batch *, struct path *); +int parent_rename_mailfile(struct batch *); +int parent_open_maildir(struct batch *, struct path *); +int parent_maildir_init(struct passwd *, char *); +int parent_external_mda(struct batch *, struct path *); +int check_child(pid_t, const char *); +int setup_spool(uid_t, gid_t); + +pid_t lka_pid = 0; +pid_t mfa_pid = 0; +pid_t queue_pid = 0; +pid_t mda_pid = 0; +pid_t mta_pid = 0; +pid_t control_pid = 0; +pid_t smtp_pid = 0; + + +int __b64_pton(char const *, unsigned char *, size_t); + +__dead void +usage(void) +{ + extern char *__progname; + + fprintf(stderr, "usage: %s [-n] [-f config]\n", __progname); + exit(1); +} + +void +parent_shutdown(void) +{ + u_int i; + pid_t pid; + pid_t pids[] = { + lka_pid, + mfa_pid, + queue_pid, + mda_pid, + mta_pid, + control_pid, + smtp_pid + }; + + for (i = 0; i < sizeof(pids) / sizeof(pid); i++) + if (pids[i]) + kill(pids[i], SIGTERM); + + do { + if ((pid = wait(NULL)) == -1 && + errno != EINTR && errno != ECHILD) + fatal("wait"); + } while (pid != -1 || (pid == -1 && errno == EINTR)); + + log_info("terminating"); + exit(0); +} + +void +parent_send_config(int fd, short event, void *p) +{ + struct smtpd *env = p; + struct buf *b; + struct listener *l; + struct ssl *s; + + log_debug("parent_send_config: configuring smtp"); + imsg_compose(env->sc_ibufs[PROC_SMTP], IMSG_CONF_START, + 0, 0, -1, NULL, 0); + + SPLAY_FOREACH(s, ssltree, &env->sc_ssl) { + b = imsg_create(env->sc_ibufs[PROC_SMTP], IMSG_CONF_SSL, 0, 0, + sizeof(*s) + s->ssl_cert_len + s->ssl_key_len); + if (b == NULL) + fatal("imsg_create"); + if (imsg_add(b, s, sizeof(*s)) == -1) + fatal("imsg_add: ssl"); + if (imsg_add(b, s->ssl_cert, s->ssl_cert_len) == -1) + fatal("imsg_add: ssl_cert"); + if (imsg_add(b, s->ssl_key, s->ssl_key_len) == -1) + fatal("imsg_add: ssl_key"); + b->fd = -1; + if (imsg_close(env->sc_ibufs[PROC_SMTP], b) == -1) + fatal("imsg_close"); + } + + TAILQ_FOREACH(l, &env->sc_listeners, entry) { + smtp_listener_setup(env, l); + imsg_compose(env->sc_ibufs[PROC_SMTP], IMSG_CONF_LISTENER, + 0, 0, l->fd, l, sizeof(*l)); + } + imsg_compose(env->sc_ibufs[PROC_SMTP], IMSG_CONF_END, + 0, 0, -1, NULL, 0); +} + +void +parent_dispatch_lka(int fd, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_LKA]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("parent_dispatch_lka: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + default: + log_debug("parent_dispatch_lka: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + +void +parent_dispatch_mfa(int fd, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_MFA]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("parent_dispatch_lka: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + default: + log_debug("parent_dispatch_lka: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + +void +parent_dispatch_mda(int fd, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_MDA]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("parent_dispatch_mda: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + case IMSG_PARENT_MAILBOX_OPEN: { + struct batch *batchp; + struct path *path; + u_int8_t i; + int desc; + struct action_handler { + enum action_type action; + int (*handler)(struct batch *, struct path *); + } action_hdl_table[] = { + { A_MBOX, parent_open_mailbox }, + { A_MAILDIR, parent_open_maildir }, + { A_EXT, parent_external_mda }, + { A_FILENAME, parent_open_filename } + }; + + batchp = imsg.data; + path = &batchp->message.recipient; + if (batchp->type & T_DAEMON_BATCH) { + path = &batchp->message.sender; + } + + for (i = 0; i < sizeof(action_hdl_table) / sizeof(struct action_handler); ++i) { + if (action_hdl_table[i].action == path->rule.r_action) { + desc = action_hdl_table[i].handler(batchp, path); + imsg_compose(ibuf, IMSG_MDA_MAILBOX_FILE, 0, 0, + desc, batchp, sizeof(struct batch)); + break; + } + } + if (i == sizeof(action_hdl_table) / sizeof(struct action_handler)) + errx(1, "%s: unknown action.", __func__); + + break; + } + case IMSG_PARENT_MESSAGE_OPEN: { + struct batch *batchp; + int desc; + + batchp = imsg.data; + desc = parent_open_message_file(batchp); + imsg_compose(ibuf, IMSG_MDA_MESSAGE_FILE, 0, 0, + desc, batchp, sizeof(struct batch)); + + break; + } + case IMSG_PARENT_MAILBOX_RENAME: { + struct batch *batchp; + + batchp = imsg.data; + parent_rename_mailfile(batchp); + + break; + } + default: + log_debug("parent_dispatch_mda: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + +void +parent_dispatch_smtp(int fd, short event, void *p) +{ + struct smtpd *env = p; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + + ibuf = env->sc_ibufs[PROC_SMTP]; + switch (event) { + case 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(&ibuf->ev); + event_loopexit(NULL); + return; + } + break; + case EV_WRITE: + if (msgbuf_write(&ibuf->w) == -1) + fatal("msgbuf_write"); + imsg_event_add(ibuf); + return; + default: + fatalx("unknown event"); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + fatal("parent_dispatch_smtp: imsg_read error"); + if (n == 0) + break; + + switch (imsg.hdr.type) { + /* XXX - NOT ADVERTISED YET */ + case IMSG_PARENT_AUTHENTICATE: { + struct session_auth_req *req; + struct session_auth_reply reply; + u_int8_t buffer[1024]; + char *pw_name; + char *pw_passwd; + struct passwd *pw; + + req = (struct session_auth_req *)imsg.data; + + reply.session_id = req->session_id; + reply.value = 0; + + if (__b64_pton(req->buffer, buffer, 1024) >= 0) { + pw_name = buffer+1; + pw_passwd = pw_name+strlen(pw_name)+1; + pw = getpwnam(pw_name); + if (pw != NULL) + if (strcmp(pw->pw_passwd, crypt(pw_passwd, + pw->pw_passwd)) == 0) + reply.value = 1; + } + imsg_compose(ibuf, IMSG_PARENT_AUTHENTICATE, 0, 0, + -1, &reply, sizeof(reply)); + + break; + } + default: + log_debug("parent_dispatch_smtp: unexpected imsg %d", + imsg.hdr.type); + break; + } + imsg_free(&imsg); + } + imsg_event_add(ibuf); +} + +void +parent_sig_handler(int sig, short event, void *p) +{ + int i; + int die = 0; + pid_t pid; + struct mdaproc *mdaproc; + struct mdaproc lookup; + struct smtpd *env = p; + struct { pid_t p; const char *s; } procs[] = { + { lka_pid, "lookup agent" }, + { mfa_pid, "mail filter agent" }, + { queue_pid, "mail queue" }, + { mda_pid, "mail delivery agent" }, + { mta_pid, "mail transfer agent" }, + { control_pid, "control process" }, + { smtp_pid, "smtp server" }, + { 0, NULL }, + }; + + switch (sig) { + case SIGTERM: + case SIGINT: + die = 1; + /* FALLTHROUGH */ + case SIGCHLD: + for (i = 0; procs[i].s != NULL; i++) + if (check_child(procs[i].p, procs[i].s)) { + procs[i].p = 0; + die = 1; + } + if (die) + parent_shutdown(); + + do { + int status; + + pid = waitpid(-1, &status, WNOHANG); + if (pid > 0) { + lookup.pid = pid; + mdaproc = SPLAY_FIND(mdaproctree, &env->mdaproc_queue, &lookup); + if (mdaproc == NULL) + errx(1, "received SIGCHLD but no known child for that pid (#%d)", pid); + + if (WIFEXITED(status) && !WIFSIGNALED(status)) { + switch (WEXITSTATUS(status)) { + case EX_OK: + log_debug("DEBUG: external mda reported success"); + break; + case EX_TEMPFAIL: + log_debug("DEBUG: external mda reported temporary failure"); + break; + default: + log_debug("DEBUG: external mda reported permanent failure"); + } + } + else { + log_debug("DEBUG: external mda process has terminated in a baaaad way"); + } + + free(mdaproc); + } + } while (pid > 0 || (pid == -1 && errno == EINTR)); + + /**/ + 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 peer peers[] = { + { PROC_LKA, parent_dispatch_lka }, + { PROC_MDA, parent_dispatch_mda }, + { PROC_MFA, parent_dispatch_mfa }, + { PROC_SMTP, parent_dispatch_smtp }, + }; + + 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 (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); + endpwent(); + + if (!setup_spool(env.sc_pw->pw_uid, 0)) + errx(1, "invalid directory permissions"); + + log_init(debug); + + if (!debug) { + if (daemon(1, 0) == -1) + err(1, "failed to daemonize"); + } + + log_info("startup%s", (debug > 1)?" [debug mode]":""); + + init_peers(&env); + + /* start subprocesses */ + lka_pid = lka(&env); + mfa_pid = mfa(&env); + queue_pid = queue(&env); + mda_pid = mda(&env); + mta_pid = mta(&env); + smtp_pid = smtp(&env); + control_pid = control(&env); + + setproctitle("parent"); + SPLAY_INIT(&env.mdaproc_queue); + + 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_peers(&env, peers, 4); + + evtimer_set(&env.sc_ev, parent_send_config, &env); + bzero(&tv, sizeof(tv)); + evtimer_add(&env.sc_ev, &tv); + + event_dispatch(); + + return (0); +} + + +int +check_child(pid_t pid, const char *pname) +{ + int status; + + if (waitpid(pid, &status, WNOHANG) > 0) { + if (WIFEXITED(status)) { + log_warnx("check_child: lost child: %s exited", pname); + return (1); + } + if (WIFSIGNALED(status)) { + log_warnx("check_child: lost child: %s terminated; " + "signal %d", pname, WTERMSIG(status)); + return (1); + } + } + + return (0); +} + +int +setup_spool(uid_t uid, gid_t gid) +{ + unsigned int n; + char *paths[] = { PATH_MESSAGES, PATH_LOCAL, PATH_RELAY, + PATH_DAEMON, PATH_ENVELOPES }; + char pathname[MAXPATHLEN]; + struct stat sb; + int ret; + + if (snprintf(pathname, MAXPATHLEN, "%s", PATH_SPOOL) >= MAXPATHLEN) + 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 < sizeof(paths)/sizeof(paths[0]); n++) { + if (snprintf(pathname, MAXPATHLEN, "%s%s", PATH_SPOOL, + paths[n]) >= MAXPATHLEN) + fatal("snprintf"); + + if (stat(pathname, &sb) == -1) { + if (errno != ENOENT) { + warn("stat: %s", pathname); + ret = 0; + continue; + } + + if (mkdir(pathname, 0700) == -1) { + ret = 0; + warn("mkdir: %s", pathname); + } + + if (chown(pathname, uid, gid) == -1) { + ret = 0; + warn("chown: %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 uid/gid */ + if (sb.st_uid != uid) { + ret = 0; + warnx("%s is not owned by uid %d", pathname, uid); + } + if (sb.st_gid != gid) { + ret = 0; + warnx("%s is not owned by gid %d", pathname, gid); + } + + /* 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)) || + (sb.st_mode & (S_IROTH|S_IWOTH|S_IXOTH))) { + ret = 0; + warnx("%s must be rwx------ (0700)", pathname); + } + } + return ret; +} + +void +imsg_event_add(struct imsgbuf *ibuf) +{ + if (ibuf->handler == NULL) { + imsg_flush(ibuf); + return; + } + + ibuf->events = EV_READ; + if (ibuf->w.queued) + ibuf->events |= EV_WRITE; + + event_del(&ibuf->ev); + event_set(&ibuf->ev, ibuf->fd, ibuf->events, ibuf->handler, ibuf->data); + event_add(&ibuf->ev, NULL); +} + +int +parent_open_message_file(struct batch *batchp) +{ + int fd; + char pathname[MAXPATHLEN]; + + if (snprintf(pathname, MAXPATHLEN, "%s%s/%s", + PATH_SPOOL, PATH_MESSAGES, batchp->message_id) + >= MAXPATHLEN) { + batchp->message.status |= S_MESSAGE_PERMFAILURE; + return -1; + } + + fd = open(pathname, O_RDONLY); + return fd; +} + +int +parent_open_mailbox(struct batch *batchp, struct path *path) +{ + int fd; + struct passwd *pw; + char pathname[MAXPATHLEN]; + + pw = getpwnam(path->pw_name); + if (pw == NULL) { + batchp->message.status |= S_MESSAGE_PERMFAILURE; + return -1; + } + + (void)snprintf(pathname, MAXPATHLEN, "%s", path->rule.r_value.path); + + fd = open(pathname, O_CREAT|O_APPEND|O_RDWR|O_EXLOCK|O_SYNC|O_NONBLOCK, 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: + case EWOULDBLOCK: + batchp->message.status |= S_MESSAGE_TEMPFAILURE; + break; + default: + batchp->message.status |= S_MESSAGE_PERMFAILURE; + } + return -1; + } + + fchown(fd, pw->pw_uid, 0); + + return fd; +} + + +int +parent_open_maildir(struct batch *batchp, struct path *path) +{ + int fd; + struct passwd *pw; + char pathname[MAXPATHLEN]; + + pw = getpwnam(path->pw_name); + if (pw == NULL) { + batchp->message.status |= S_MESSAGE_PERMFAILURE; + return -1; + } + + snprintf(pathname, MAXPATHLEN, "%s", path->rule.r_value.path); + log_debug("PATH: %s", pathname); + if (! parent_maildir_init(pw, pathname)) { + batchp->message.status |= S_MESSAGE_TEMPFAILURE; + return -1; + } + + if (snprintf(pathname, MAXPATHLEN, "%s/tmp/%s", + pathname, batchp->message.message_uid) >= MAXPATHLEN) { + batchp->message.status |= S_MESSAGE_TEMPFAILURE; + return -1; + } + + fd = open(pathname, O_CREAT|O_RDWR|O_TRUNC|O_EXLOCK|O_SYNC, 0600); + if (fd == -1) { + batchp->message.status |= S_MESSAGE_TEMPFAILURE; + return -1; + } + + fchown(fd, pw->pw_uid, pw->pw_gid); + + return fd; +} + +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 < sizeof (subdir) / sizeof (char *); ++i) { + if (snprintf(pathname, MAXPATHLEN, "%s%s", root, subdir[i]) + >= MAXPATHLEN) + return 0; + if (mkdir(pathname, 0700) == -1) + if (errno != EEXIST) + return 0; + chown(pathname, pw->pw_uid, pw->pw_gid); + } + + return 1; +} + +int +parent_rename_mailfile(struct batch *batchp) +{ + struct passwd *pw; + char srcpath[MAXPATHLEN]; + char dstpath[MAXPATHLEN]; + struct path *path; + + if (batchp->type & T_DAEMON_BATCH) { + path = &batchp->message.sender; + } + else { + path = &batchp->message.recipient; + } + + pw = getpwnam(path->pw_name); + if (pw == NULL) { + batchp->message.status |= S_MESSAGE_PERMFAILURE; + return 0; + } + + (void)snprintf(srcpath, MAXPATHLEN, "%s/tmp/%s", + path->rule.r_value.path, batchp->message.message_uid); + (void)snprintf(dstpath, MAXPATHLEN, "%s/new/%s", + path->rule.r_value.path, batchp->message.message_uid); + + if (rename(srcpath, dstpath) == -1) { + batchp->message.status |= S_MESSAGE_TEMPFAILURE; + return 0; + } + + return 1; +} + +int +parent_external_mda(struct batch *batchp, struct path *path) +{ + struct passwd *pw; + pid_t pid; + int pipefd[2]; + struct mdaproc *mdaproc; + char *pw_name; + + pw_name = path->pw_name; + if (pw_name[0] == '\0') + pw_name = SMTPD_USER; + + log_debug("executing filter as user: %s", pw_name); + pw = getpwnam(pw_name); + if (pw == NULL) { + batchp->message.status |= S_MESSAGE_PERMFAILURE; + return -1; + } + + if (socketpair(AF_UNIX, SOCK_STREAM, 0, pipefd) == -1) { + batchp->message.status |= S_MESSAGE_PERMFAILURE; + return -1; + } + + pid = fork(); + if (pid == -1) { + close(pipefd[0]); + close(pipefd[1]); + batchp->message.status |= S_MESSAGE_PERMFAILURE; + return -1; + } + + if (pid == 0) { + setproctitle("external MDA"); + + 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("mta: cannot drop privileges"); + + close(pipefd[0]); + close(STDOUT_FILENO); + close(STDERR_FILENO); + dup2(pipefd[1], 0); + + execlp(_PATH_BSHELL, "sh", "-c", path->rule.r_value.path, (void *)NULL); + exit(1); + } + + mdaproc = calloc(1, sizeof (struct mdaproc)); + if (mdaproc == NULL) + err(1, "calloc"); + mdaproc->pid = pid; + + SPLAY_INSERT(mdaproctree, &batchp->env->mdaproc_queue, mdaproc); + + close(pipefd[1]); + return pipefd[0]; +} + +int +parent_open_filename(struct batch *batchp, struct path *path) +{ + int fd; + char pathname[MAXPATHLEN]; + + (void)snprintf(pathname, MAXPATHLEN, "%s", path->u.filename); + fd = open(pathname, O_CREAT|O_APPEND|O_RDWR|O_EXLOCK|O_SYNC|O_NONBLOCK, 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: + case EWOULDBLOCK: + batchp->message.status |= S_MESSAGE_TEMPFAILURE; + break; + default: + batchp->message.status |= S_MESSAGE_PERMFAILURE; + } + return -1; + } + + return fd; +} + +int +mdaproc_cmp(struct mdaproc *s1, struct mdaproc *s2) +{ + if (s1->pid < s2->pid) + return (-1); + + if (s1->pid > s2->pid) + return (1); + + return (0); +} + +SPLAY_GENERATE(mdaproctree, mdaproc, mdaproc_nodes, mdaproc_cmp); diff --git a/usr.sbin/smtpd/smtpd.conf b/usr.sbin/smtpd/smtpd.conf new file mode 100644 index 00000000000..0797d5e93c5 --- /dev/null +++ b/usr.sbin/smtpd/smtpd.conf @@ -0,0 +1,11 @@ +listen on localhost port 25 +#ssmtp listen on localhost port 465 +hostname localhost + +local = "( 127.0.0.1, ::1 )" + +#map "aliases" { source db "/etc/mail/aliases.db" } +#map "virtual" { source db "/etc/mail/virtual.db" } + +accept for domain "localhost" deliver to mbox "/var/mail/%u" +accept from $local for all relay diff --git a/usr.sbin/smtpd/smtpd.conf.5 b/usr.sbin/smtpd/smtpd.conf.5 new file mode 100644 index 00000000000..287e306c5e7 --- /dev/null +++ b/usr.sbin/smtpd/smtpd.conf.5 @@ -0,0 +1,101 @@ +.\" $OpenBSD: smtpd.conf.5,v 1.1 2008/11/01 21:35:28 gilles Exp $ +.\" +.\" Copyright (c) 2008 Janne Johansson <jj@openbsd.org> +.\" +.\" 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. +.\" +.\" +.Dd $Mdocdate: November 1 2008 $ +.Dt SMTPD.CONF 5 +.Os +.Sh NAME +.Nm smtpd.conf +.Nd Configuration for smtpd +.Sh DESCRIPTION +.Nm +is used to configure the +.Xr smtpd 8 , +a small SMTP daemon. +.Pp +.Sh SMTPD.CONF FILE FORMAT +Lines beginning with +.Sq # +and empty lines are regarded as comments, +and ignored. +Lines may be split using the +.Sq \e +character. +.Pp +Additional configuration files can be included with the +.Ic include +keyword, for example: +.Bd -literal -offset indent +include "/etc/mail/sub.smtpd.conf" +.Ed +.Sh MACROS +Macros can be defined that will later be expanded in context. +Macro names must start with a letter, and may contain letters, digits +and underscores. +Macro names may not be reserved words (for example +.Ar listen , +.Ar accept , +.Ar port ) . +Macros are not expanded inside quotes. +.Pp +For example, +.Bd -literal -offset indent +smtp_port = 25 +listen on 127.0.0.1 port $smtp_port +.Ed +.Sh EXPANSION +Some configuration directives expect expansion of their parameter at +runtime. Such directives (for example +.Ar deliver to mbox , +.Ar deliver to maildir , +.Ar deliver to mda , +.Ar relay via ) +may use format specifiers which will be expanded before delivery or +relaying. The following formats are currently supported: +%a expands to the user part of the email address prior to the +resolution of aliases, %u expands to the user part after aliases +resolution and will typically be the system account, %d expands to +the domain part of the email address. +.Sh EXAMPLES +The following example configures a machine to accept local delivery +for both localhost and example.com, as well as the relaying of mail +destined for example.org through the mx1.example.org server and mail +destined for example.net through regular MX records lookup: +.Pp +.Bd -literal -offset listen +.Pp +listen on 0.0.0.0 port 25 +accept for domain "localhost" deliver to mbox "/var/mail/%u" +accept for domain "example.com" deliver to maildir "/home/%u/Maildir" +accept for domain "example.org" relay via "mx1.example.org" +accept for domain "example.net" relay +.Ed +.Sh FILES +.Bl -tag -width "/var/mail" -compact +.It Pa /var/spool/smtpd/ +Spool directories for mail during processing. +.It Pa /etc/mail/smtpd.conf +Default location for this file. +.El +.Sh SEE ALSO +.Xr smtpd 8 , +.Xr smtpctl 8 , +.Xr smtpdb 8 +.Sh HISTORY +.Xr smtpd 8 +first appeared in +.Ox 4.4 . diff --git a/usr.sbin/smtpd/smtpd.h b/usr.sbin/smtpd/smtpd.h new file mode 100644 index 00000000000..6fa1af2d741 --- /dev/null +++ b/usr.sbin/smtpd/smtpd.h @@ -0,0 +1,745 @@ +/* + * Copyright (c) 2008 Gilles Chehade <gilles@openbsd.org> + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * + * 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. + */ + +#define CONF_FILE "/etc/mail/smtpd.conf" +#define MAX_LISTEN 16 +#define STRLEN 1024 +#define PROC_COUNT 8 +#define READ_BUF_SIZE 65535 +#define MAX_NAME_SIZE 64 + +/* sizes include the tailing '\0' */ +#define MAX_LOCALPART_SIZE 65 +#define MAX_DOMAINPART_SIZE MAXHOSTNAMELEN +#define MAX_PATH_SIZE 256 + +/*#define SMTPD_CONNECT_TIMEOUT (60)*/ +#define SMTPD_CONNECT_TIMEOUT (10) +#define SMTPD_QUEUE_INTERVAL (15 * 60) +#define SMTPD_QUEUE_MAXINTERVAL (4 * 60 * 60) +#define SMTPD_QUEUE_EXPIRY (4 * 24 * 60 * 60) +#define SMTPD_USER "_smtpd" +#define SMTPD_SOCKET "/var/run/smtpd.sock" +#define SMTPD_BANNER "220 %s OpenSMTPD\r\n" +#define SMTPD_SESSION_TIMEOUT 300 + +#define RCPTBUFSZ 256 + +#define PATH_SPOOL "/var/spool/smtpd" + +#define PATH_MESSAGES "/messages" +#define PATH_LOCAL "/local" +#define PATH_RELAY "/relay" +#define PATH_DAEMON "/daemon" +#define PATH_ENVELOPES "/envelopes" + +/* used by newaliases */ +#define PATH_ALIASES "/etc/mail/aliases" +#define PATH_ALIASESDB "/etc/mail/aliases.db" + +/* number of MX records to lookup */ +#define MXARRAYSIZE 5 + +struct address { + char hostname[MAXHOSTNAMELEN]; + u_int16_t port; +}; + +struct netaddr { + struct sockaddr_storage ss; + int masked; +}; + +/* buffer specific headers */ +struct buf { + TAILQ_ENTRY(buf) entry; + u_char *buf; + size_t size; + size_t max; + size_t wpos; + size_t rpos; + int fd; +}; + +struct msgbuf { + TAILQ_HEAD(, buf) bufs; + u_int32_t queued; + int fd; +}; + +struct buf_read { + u_char buf[READ_BUF_SIZE]; + u_char *rptr; + size_t wpos; +}; + +struct imsg_fd { + TAILQ_ENTRY(imsg_fd) entry; + int fd; + u_int32_t id; +}; + +struct imsgbuf { + TAILQ_HEAD(, imsg_fd) fds; + struct buf_read r; + struct msgbuf w; + struct event ev; + void (*handler)(int, short, void *); + int fd; + pid_t pid; + short events; + void *data; + u_int32_t id; +}; + +struct imsg_hdr { + u_int16_t type; + u_int16_t len; + u_int32_t peerid; + pid_t pid; +}; + +struct imsg { + struct imsg_hdr hdr; + u_int32_t id; + void *data; +}; + +enum imsg_type { + IMSG_NONE, + IMSG_CTL_OK, /* answer to smtpctl requests */ + IMSG_CTL_FAIL, + IMSG_CTL_SHUTDOWN, + IMSG_CONF_START, + IMSG_CONF_SSL, + IMSG_CONF_SSL_CERT, + IMSG_CONF_SSL_KEY, + IMSG_CONF_LISTENER, + IMSG_CONF_MAP, + IMSG_CONF_RULE, + IMSG_CONF_CONDITION, + IMSG_CONF_OPTION, + IMSG_CONF_END, + IMSG_CONF_RELOAD, + IMSG_LKA_LOOKUP_MAIL, + IMSG_LKA_LOOKUP_RCPT, + IMSG_LKA_ALIAS_LOOKUP, + IMSG_LKA_VUSER_LOOKUP, + IMSG_LKA_ALIAS_RESULT, + IMSG_LKA_VUSER_RESULT, + IMSG_LKA_ALIAS_RESULT_ACK, + IMSG_LKA_ALIAS_SCHEDULE, + IMSG_LKA_ALIAS_END, + IMSG_LKA_NO_ALIAS, + IMSG_LKA_MX_LOOKUP, + IMSG_LKA_FORWARD_LOOKUP, + IMSG_LKA_HOSTNAME_LOOKUP, + IMSG_MDA_MAILBOX_FILE, + IMSG_MDA_MESSAGE_FILE, + IMSG_MDA_MAILBOX_FILE_ERROR, + IMSG_MDA_MESSAGE_FILE_ERROR, + IMSG_MFA_RPATH_SUBMIT, + IMSG_MFA_RCPT_SUBMIT, + IMSG_MFA_DATA_SUBMIT, + IMSG_MFA_LOOKUP_MAIL, + IMSG_MFA_LOOKUP_RCPT, + IMSG_QUEUE_REMOVE_SUBMISSION, + IMSG_QUEUE_CREATE_MESSAGE_FILE, + IMSG_QUEUE_DELETE_MESSAGE_FILE, + IMSG_QUEUE_MESSAGE_SUBMIT, + IMSG_QUEUE_MESSAGE_UPDATE, + IMSG_QUEUE_BATCH_COMPLETE, + IMSG_QUEUE_BATCH_CLOSE, + IMSG_QUEUE_MESSAGE_FD, + + IMSG_QUEUE_ACCEPTED_CLOSE, + IMSG_QUEUE_RETRY_CLOSE, + IMSG_QUEUE_REJECTED_CLOSE, + + IMSG_QUEUE_RECIPIENT_ACCEPTED, + IMSG_QUEUE_RECIPIENT_UPDATED, + + IMSG_CREATE_BATCH, + IMSG_BATCH_APPEND, + IMSG_BATCH_CLOSE, + + IMSG_SMTP_MESSAGE_FILE, + IMSG_SMTP_SUBMIT_ACK, + IMSG_SMTP_HOSTNAME_ANSWER, + IMSG_PARENT_MAILBOX_OPEN, + IMSG_PARENT_MESSAGE_OPEN, + IMSG_PARENT_MAILBOX_RENAME, + + IMSG_PARENT_AUTHENTICATE +}; + +#define IMSG_HEADER_SIZE sizeof(struct imsg_hdr) +#define MAX_IMSGSIZE 16384 + +enum blockmodes { + BM_NORMAL, + BM_NONBLOCK +}; + +struct ctl_conn { + TAILQ_ENTRY(ctl_conn) entry; + u_int8_t flags; +#define CTL_CONN_NOTIFY 0x01 + struct imsgbuf ibuf; +}; +TAILQ_HEAD(ctl_connlist, ctl_conn); + +typedef u_int32_t objid_t; + +struct ctl_id { + objid_t id; + char name[MAX_NAME_SIZE]; +}; + +enum smtp_proc_type { + PROC_PARENT = 0, + PROC_SMTP, + PROC_MFA, + PROC_LKA, + PROC_QUEUE, + PROC_MDA, + PROC_MTA, + PROC_CONTROL, +} smtpd_process; + +struct peer { + enum smtp_proc_type id; + void (*cb)(int, short, void *); +}; + +enum map_type { + T_SINGLE, + T_LIST, + T_HASH +}; + +enum map_src { + S_NONE, + S_DYN, + S_DNS, + S_FILE, + S_DB, + S_EXT +}; + +enum mapel_type { + ME_STRING, + ME_NET, + ME_NETMASK +}; + +struct mapel { + TAILQ_ENTRY(mapel) me_entry; + union mapel_data { + char med_string[STRLEN]; + struct netaddr med_addr; + } me_key; + union mapel_data me_val; +}; + +struct map { + TAILQ_ENTRY(map) m_entry; +#define F_USED 0x01 +#define F_DYNAMIC 0x02 + u_int8_t m_flags; + char m_name[STRLEN]; + objid_t m_id; + enum map_type m_type; + enum mapel_type m_eltype; + enum map_src m_src; + char m_config[MAXPATHLEN]; + TAILQ_HEAD(mapel_list, mapel) m_contents; +}; + +enum cond_type { + C_ALL, + C_NET, + C_DOM +}; + +struct cond { + TAILQ_ENTRY(cond) c_entry; + objid_t c_map; + enum cond_type c_type; + struct map *c_match; +}; + +enum opt_type { + O_RWUSER, /* rewrite user */ + O_RWDOMAIN, /* rewrite domain */ +}; + +struct opt { + TAILQ_ENTRY(opt) o_entry; + enum opt_type o_type; +}; + +enum action_type { + A_RELAY, + A_RELAYVIA, + A_MAILDIR, + A_MBOX, + A_FILENAME, + A_EXT +}; +#define IS_MAILBOX(x) ((x) == A_MAILDIR || (x) == A_MBOX || (x) == A_FILENAME) +#define IS_RELAY(x) ((x) == A_RELAY || (x) == A_RELAYVIA) +#define IS_EXT(x) ((x) == A_EXT) + +struct rule { + TAILQ_ENTRY(rule) r_entry; + int r_accept; + struct map *r_sources; + TAILQ_HEAD(condlist, cond) r_conditions; + enum action_type r_action; + union { + char path[MAXPATHLEN]; + struct address host; +#define MAXCOMMANDLEN 256 + char command[MAXCOMMANDLEN]; + } r_value; + TAILQ_HEAD(optlist, opt) r_options; +}; + +enum path_flags { + F_ALIAS = 0x1, + F_VIRTUAL = 0x2, + F_EXPANDED = 0x4, + F_NOFORWARD = 0x8, + F_FORWARDED = 0x10 +}; + +struct path { + TAILQ_ENTRY(path) entry; + struct rule rule; + enum path_flags flags; + u_int8_t forwardcnt; + char user[MAX_LOCALPART_SIZE]; + char domain[MAX_DOMAINPART_SIZE]; + char pw_name[MAXLOGNAME]; + union { + char filename[MAXPATHLEN]; + char filter[MAXPATHLEN]; + } u; +}; + +enum alias_type { + ALIAS_USERNAME, + ALIAS_FILENAME, + ALIAS_FILTER, + ALIAS_INCLUDE, + ALIAS_ADDRESS +}; + +struct alias { + TAILQ_ENTRY(alias) entry; + enum alias_type type; + union { + char username[MAXLOGNAME]; + char filename[MAXPATHLEN]; + char filter[MAXPATHLEN]; + struct path path; + } u; +}; +TAILQ_HEAD(aliaseslist, alias); + +struct submit_status { + u_int64_t id; + int code; + union { + struct path path; + char msgid[MAXPATHLEN]; + char errormsg[STRLEN]; + } u; + struct sockaddr_storage ss; +}; + +struct message_recipient { + u_int64_t id; + struct sockaddr_storage ss; + struct path path; +}; + +enum message_type { + T_MDA_MESSAGE = 0x1, + T_MTA_MESSAGE = 0x2, + T_DAEMON_MESSAGE = 0x4 +}; + +enum message_status { + S_MESSAGE_PERMFAILURE = 0x1, + S_MESSAGE_TEMPFAILURE = 0x2, + S_MESSAGE_REJECTED = 0x4, + S_MESSAGE_ACCEPTED = 0x8, + S_MESSAGE_RETRY = 0x10, + S_MESSAGE_EDNS = 0x20, + S_MESSAGE_ECONNECT = 0x40 +}; + +enum message_flags { + F_MESSAGE_COMPLETE = 0x1, + F_MESSAGE_RESOLVED = 0x2, + F_MESSAGE_READY = 0x4, + F_MESSAGE_EXPIRED = 0x8, + F_MESSAGE_PROCESSING = 0x10 +}; + +struct message { + SPLAY_ENTRY(message) nodes; + TAILQ_ENTRY(message) entry; + + enum message_type type; + + u_int64_t id; + u_int64_t session_id; + u_int64_t batch_id; + + char message_id[MAXPATHLEN]; + char message_uid[MAXPATHLEN]; + + char session_helo[MAXHOSTNAMELEN]; + char session_hostname[MAXHOSTNAMELEN]; + char session_errorline[STRLEN]; + struct sockaddr_storage session_ss; + + struct path sender; + struct path recipient; + TAILQ_HEAD(pathlist,path) recipients; + + u_int16_t rcptcount; + + time_t creation; + time_t lasttry; + u_int8_t retry; + enum message_flags flags; + enum message_status status; + FILE *datafp; + int mboxfd; + int messagefd; +}; + +enum batch_status { + S_BATCH_PERMFAILURE = 0x1, + S_BATCH_TEMPFAILURE = 0x2, + S_BATCH_REJECTED = 0x4, + S_BATCH_ACCEPTED = 0x8, + S_BATCH_RETRY = 0x10, + S_BATCH_EDNS = 0x20, + S_BATCH_ECONNECT = 0x40 +}; + +enum batch_type { + T_MDA_BATCH = 0x1, + T_MTA_BATCH = 0x2, + T_DAEMON_BATCH = 0x4 +}; + +enum batch_flags { + F_BATCH_COMPLETE = 0x1, + F_BATCH_RESOLVED = 0x2, + F_BATCH_SCHEDULED = 0x4, + F_BATCH_EXPIRED = 0x8, +}; + +struct mdaproc { + SPLAY_ENTRY(mdaproc) mdaproc_nodes; + + pid_t pid; +}; + +struct batch { + SPLAY_ENTRY(batch) b_nodes; + + u_int64_t id; + enum batch_type type; + enum batch_flags flags; + + struct rule rule; + + struct event ev; + struct timeval tv; + int peerfd; + struct bufferevent *bev; + u_int8_t state; + struct smtpd *env; + + char message_id[MAXPATHLEN]; + char hostname[MAXHOSTNAMELEN]; + char errorline[STRLEN]; + + u_int8_t h_errno; + struct sockaddr_storage ss[MXARRAYSIZE*2]; + u_int8_t ss_cnt; + u_int8_t ss_off; + + time_t creation; + time_t lasttry; + u_int8_t retry; + + struct message message; + struct message *messagep; + FILE *messagefp; + TAILQ_HEAD(messagelist, message) messages; + + enum batch_status status; +}; + +enum session_state { + S_INIT = 0, + S_GREETED, + S_TLS, + S_AUTH, + S_HELO, + S_MAIL, + S_RCPT, + S_DATA, + S_DATACONTENT, + S_DONE, + S_QUIT +}; + +struct ssl { + SPLAY_ENTRY(ssl) ssl_nodes; + char ssl_name[PATH_MAX]; + char *ssl_cert; + off_t ssl_cert_len; + char *ssl_key; + off_t ssl_key_len; +}; + +struct listener { +#define F_STARTTLS 0x01 +#define F_SSMTP 0x02 +#define F_SSL (F_SSMTP|F_STARTTLS) + u_int8_t flags; + int fd; + struct sockaddr_storage ss; + in_port_t port; + int backlog; + struct timeval timeout; + struct event ev; + struct smtpd *env; + char ssl_cert_name[PATH_MAX]; + struct ssl *ssl; + void *ssl_ctx; + TAILQ_ENTRY(listener) entry; +}; + +struct session_auth_req { + u_int64_t session_id; + char buffer[STRLEN]; +}; + +struct session_auth_reply { + u_int64_t session_id; + u_int8_t value; +}; + +enum session_flags { + F_QUIT = 0x1, + F_IMSG_SENT = 0x2, + F_8BITMIME = 0x4, + F_SECURE = 0x8, + F_AUTHENTICATED = 0x10 +}; + +struct session { + SPLAY_ENTRY(session) s_nodes; + u_int64_t s_id; + + enum session_flags s_flags; + enum session_state s_state; + time_t s_tm; + int s_fd; + struct sockaddr_storage s_ss; + char s_hostname[MAXHOSTNAMELEN]; + struct event s_ev; + struct bufferevent *s_bev; + struct listener *s_l; + struct smtpd *s_env; + void *s_ssl; + u_char *s_buf; + int s_buflen; + struct timeval s_tv; + struct message s_msg; +}; + +struct smtpd { +#define SMTPD_OPT_VERBOSE 0x00000001 +#define SMTPD_OPT_NOACTION 0x00000002 + u_int32_t sc_opts; +#define SMTPD_CONFIGURING 0x00000001 +#define SMTPD_EXITING 0x00000002 + u_int32_t sc_flags; + struct timeval sc_qintval; + struct event sc_ev; + int sc_pipes[PROC_COUNT] + [PROC_COUNT][2]; + struct imsgbuf *sc_ibufs[PROC_COUNT]; + struct passwd *sc_pw; + char sc_hostname[MAXHOSTNAMELEN]; + TAILQ_HEAD(listenerlist, listener) sc_listeners; + TAILQ_HEAD(maplist, map) *sc_maps; + TAILQ_HEAD(rulelist, rule) *sc_rules; + SPLAY_HEAD(sessiontree, session) sc_sessions; + SPLAY_HEAD(msgtree, message) sc_messages; + SPLAY_HEAD(ssltree, ssl) sc_ssl; + + SPLAY_HEAD(batchtree, batch) batch_queue; + SPLAY_HEAD(mdaproctree, mdaproc) mdaproc_queue; +}; + +/* aliases.c */ +int is_alias(struct path *); + +/* atomic.c */ +ssize_t atomic_read(int, void *, size_t); +ssize_t atomic_write(int, const void *, size_t); +ssize_t atomic_printfd(int, const char *, ...); + +/* log.c */ +void log_init(int); +void log_warn(const char *, ...); +void log_warnx(const char *, ...); +void log_info(const char *, ...); +void log_debug(const char *, ...); +__dead void fatal(const char *); +__dead void fatalx(const char *); + + +/* buffer.c */ +struct buf *buf_open(size_t); +struct buf *buf_dynamic(size_t, size_t); +int buf_add(struct buf *, void *, size_t); +void *buf_reserve(struct buf *, size_t); +int buf_close(struct msgbuf *, struct buf *); +void buf_free(struct buf *); +void msgbuf_init(struct msgbuf *); +void msgbuf_clear(struct msgbuf *); +int msgbuf_write(struct msgbuf *); + +/* imsg.c */ +void imsg_init(struct imsgbuf *, int, void (*)(int, short, void *)); +ssize_t imsg_read(struct imsgbuf *); +ssize_t imsg_get(struct imsgbuf *, struct imsg *); +int imsg_compose(struct imsgbuf *, enum imsg_type, u_int32_t, pid_t, + int, void *, u_int16_t); +int imsg_composev(struct imsgbuf *, enum imsg_type, u_int32_t, + pid_t, int, const struct iovec *, int); +int imsg_compose_fds(struct imsgbuf *, enum imsg_type, u_int32_t, pid_t, + void *, u_int16_t, int, ...); +struct buf *imsg_create(struct imsgbuf *, enum imsg_type, u_int32_t, pid_t, + u_int16_t); +int imsg_add(struct buf *, void *, u_int16_t); +int imsg_append(struct imsgbuf *, struct buf *); +int imsg_close(struct imsgbuf *, struct buf *); +void imsg_free(struct imsg *); +void imsg_event_add(struct imsgbuf *); /* needs to be provided externally */ +int imsg_get_fd(struct imsgbuf *, struct imsg *); +int imsg_flush(struct imsgbuf *); +void imsg_clear(struct imsgbuf *); + +/* lka.c */ +pid_t lka(struct smtpd *); + +/* mfa.c */ +pid_t mfa(struct smtpd *); +int msg_cmp(struct message *, struct message *); +SPLAY_PROTOTYPE(msgtree, message, nodes, msg_cmp); + +/* queue.c */ +pid_t queue(struct smtpd *); +u_int64_t queue_generate_id(void); +int batch_cmp(struct batch *, struct batch *); +struct batch *batch_by_id(struct smtpd *, u_int64_t); +struct message *message_by_id(struct smtpd *, struct batch *, u_int64_t); +int queue_remove_batch_message(struct smtpd *, struct batch *, struct message *); +SPLAY_PROTOTYPE(batchtree, batch, b_nodes, batch_cmp); + +/* mda.c */ +pid_t mda(struct smtpd *); +int mdaproc_cmp(struct mdaproc *, struct mdaproc *); +SPLAY_PROTOTYPE(mdaproctree, mdaproc, mdaproc_nodes, mdaproc_cmp); + +/* mta.c */ +pid_t mta(struct smtpd *); + +/* control.c */ +pid_t control(struct smtpd *); +void session_socket_blockmode(int, enum blockmodes); + +/* smtp.c */ +pid_t smtp(struct smtpd *); +void smtp_listener_setup(struct smtpd *, struct listener *); + +/* smtp_session.c */ +void session_init(struct listener *, struct session *); +void session_read(struct bufferevent *, void *); +void session_write(struct bufferevent *, void *); +void session_error(struct bufferevent *, short, void *); +int session_cmp(struct session *, struct session *); +void session_msg_submit(struct session *); +void session_pickup(struct session *, struct submit_status *); +void session_destroy(struct session *); +SPLAY_PROTOTYPE(sessiontree, session, s_nodes, session_cmp); + +/* store.c */ +int store_write_header(struct batch *, struct message *); +int store_write_message(struct batch *, struct message *); +int store_write_daemon(struct batch *, struct message *); +int store_message(struct batch *, struct message *, + int (*)(struct batch *, struct message *)); + +/* config.c */ +#define PURGE_LISTENERS 0x01 +#define PURGE_MAPS 0x02 +#define PURGE_RULES 0x04 +#define PURGE_SSL 0x08 +#define PURGE_EVERYTHING 0xff +void purge_config(struct smtpd *, u_int8_t); +void unconfigure(struct smtpd *); +void configure(struct smtpd *); +void init_peers(struct smtpd *); +void config_peers(struct smtpd *, struct peer *, u_int); + +/* parse.y */ +int parse_config(struct smtpd *, const char *, int); +int cmdline_symset(char *); + +/* ssl.c */ +void ssl_init(void); +void ssl_transaction(struct session *); + +void ssl_session_init(struct session *); +void ssl_session_destroy(struct session *); +int ssl_load_certfile(struct smtpd *, const char *); +void ssl_setup(struct smtpd *, struct listener *); +int ssl_cmp(struct ssl *, struct ssl *); +SPLAY_PROTOTYPE(ssltree, ssl, ssl_nodes, ssl_cmp); + +/* ssl_privsep.c */ +int ssl_ctx_use_private_key(void *, char *, off_t); +int ssl_ctx_use_certificate_chain(void *, char *, off_t); + +/* smtpd.c */ +struct map *map_find(struct smtpd *, objid_t); +struct map *map_findbyname(struct smtpd *, const char *); diff --git a/usr.sbin/smtpd/ssl.c b/usr.sbin/smtpd/ssl.c new file mode 100644 index 00000000000..28a5284d250 --- /dev/null +++ b/usr.sbin/smtpd/ssl.c @@ -0,0 +1,496 @@ +/* + * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org> + * Copyright (c) 2008 Reyk Floeter <reyk@openbsd.org> + * + * 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/stat.h> +#include <sys/time.h> + +#include <ctype.h> +#include <event.h> +#include <fcntl.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <openssl/ssl.h> +#include <openssl/engine.h> +#include <openssl/err.h> + +#include "smtpd.h" + +#define SSL_CIPHERS "HIGH:!ADH" + +void ssl_error(const char *); +char *ssl_load_file(const char *, off_t *); +SSL_CTX *ssl_ctx_create(void); +void ssl_session_accept(int, short, void *); +void ssl_read(int, short, void *); +void ssl_write(int, short, void *); +int ssl_bufferevent_add(struct event *, int); + +extern void bufferevent_read_pressure_cb(struct evbuffer *, size_t, + size_t, void *); + +void +ssl_read(int fd, short event, void *p) +{ + struct bufferevent *bufev = p; + struct session *s = bufev->cbarg; + int ret; + int ssl_err; + short what; + size_t len; + char rbuf[READ_BUF_SIZE]; + int howmuch = READ_BUF_SIZE; + + what = EVBUFFER_READ; + ret = ssl_err = 0; + + if (event == EV_TIMEOUT) { + what |= EVBUFFER_TIMEOUT; + goto err; + } + + if (bufev->wm_read.high != 0) + howmuch = MIN(sizeof(rbuf), bufev->wm_read.high); + + ret = SSL_read(s->s_ssl, rbuf, howmuch); + if (ret <= 0) { + ssl_err = SSL_get_error(s->s_ssl, ret); + + switch (ssl_err) { + case SSL_ERROR_WANT_READ: + goto retry; + case SSL_ERROR_WANT_WRITE: + goto retry; + default: + if (ret == 0) + what |= EVBUFFER_EOF; + else { + ssl_error("ssl_read"); + what |= EVBUFFER_ERROR; + } + goto err; + } + } + + if (evbuffer_add(bufev->input, rbuf, ret) == -1) { + what |= EVBUFFER_ERROR; + goto err; + } + + ssl_bufferevent_add(&bufev->ev_read, bufev->timeout_read); + + len = EVBUFFER_LENGTH(bufev->input); + if (bufev->wm_read.low != 0 && len < bufev->wm_read.low) + return; + if (bufev->wm_read.high != 0 && len > bufev->wm_read.high) { + struct evbuffer *buf = bufev->input; + event_del(&bufev->ev_read); + evbuffer_setcb(buf, bufferevent_read_pressure_cb, bufev); + return; + } + + if (bufev->readcb != NULL) + (*bufev->readcb)(bufev, bufev->cbarg); + return; + +retry: + ssl_bufferevent_add(&bufev->ev_read, bufev->timeout_read); + return; + +err: + (*bufev->errorcb)(bufev, what, bufev->cbarg); +} + + +void +ssl_write(int fd, short event, void *p) +{ + struct bufferevent *bufev = p; + struct session *s = bufev->cbarg; + int ret; + int ssl_err; + short what; + + ret = 0; + what = EVBUFFER_WRITE; + + if (event == EV_TIMEOUT) { + what |= EV_TIMEOUT; + goto err; + } + + if (EVBUFFER_LENGTH(bufev->output)) { + if (s->s_buf == NULL) { + s->s_buflen = EVBUFFER_LENGTH(bufev->output); + if ((s->s_buf = malloc(s->s_buflen)) == NULL) { + what |= EVBUFFER_ERROR; + goto err; + } + memcpy(s->s_buf, EVBUFFER_DATA(bufev->output), + s->s_buflen); + } + + ret = SSL_write(s->s_ssl, s->s_buf, s->s_buflen); + if (ret <= 0) { + ssl_err = SSL_get_error(s->s_ssl, ret); + + switch (ssl_err) { + case SSL_ERROR_WANT_READ: + goto retry; + case SSL_ERROR_WANT_WRITE: + goto retry; + default: + if (ret == 0) + what |= EVBUFFER_EOF; + else { + ssl_error("ssl_write"); + what |= EVBUFFER_ERROR; + } + goto err; + } + } + evbuffer_drain(bufev->output, ret); + } + if (s->s_buf != NULL) { + free(s->s_buf); + s->s_buf = NULL; + s->s_buflen = 0; + } + if (EVBUFFER_LENGTH(bufev->output) != 0) + ssl_bufferevent_add(&bufev->ev_write, bufev->timeout_write); + + if (bufev->writecb != NULL && + EVBUFFER_LENGTH(bufev->output) <= bufev->wm_write.low) + (*bufev->writecb)(bufev, bufev->cbarg); + return; + +retry: + if (s->s_buflen != 0) + ssl_bufferevent_add(&bufev->ev_write, bufev->timeout_write); + return; + +err: + if (s->s_buf != NULL) { + free(s->s_buf); + s->s_buf = NULL; + s->s_buflen = 0; + } + (*bufev->errorcb)(bufev, what, bufev->cbarg); +} + +int +ssl_bufferevent_add(struct event *ev, int timeout) +{ + struct timeval tv; + struct timeval *ptv = NULL; + + if (timeout) { + timerclear(&tv); + tv.tv_sec = timeout; + ptv = &tv; + } + + return (event_add(ev, ptv)); +} + +int +ssl_cmp(struct ssl *s1, struct ssl *s2) +{ + return (strcmp(s1->ssl_name, s2->ssl_name)); +} + +SPLAY_GENERATE(ssltree, ssl, ssl_nodes, ssl_cmp); + +char * +ssl_load_file(const char *name, off_t *len) +{ + struct stat st; + off_t size; + char *buf = NULL; + int fd; + + if ((fd = open(name, O_RDONLY)) == -1) + return (NULL); + if (fstat(fd, &st) != 0) + goto fail; + size = st.st_size; + if ((buf = calloc(1, size + 1)) == NULL) + goto fail; + if (read(fd, buf, size) != size) + goto fail; + close(fd); + + *len = size + 1; + return (buf); + +fail: + if (buf != NULL) + free(buf); + close(fd); + return (NULL); +} + +SSL_CTX * +ssl_ctx_create(void) +{ + SSL_CTX *ctx; + + ctx = SSL_CTX_new(SSLv23_method()); + if (ctx == NULL) { + ssl_error("ssl_ctx_create"); + fatal("ssl_ctx_create: could not create SSL context"); + } + + SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF); + SSL_CTX_set_timeout(ctx, SMTPD_SESSION_TIMEOUT); + SSL_CTX_set_options(ctx, SSL_OP_ALL); + SSL_CTX_set_options(ctx, + SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); + + if (!SSL_CTX_set_cipher_list(ctx, SSL_CIPHERS)) { + ssl_error("ssl_ctx_create"); + fatal("ssl_ctx_create: could not set cipher list"); + } + return (ctx); +} + +int +ssl_load_certfile(struct smtpd *env, const char *name) +{ + struct ssl *s; + struct ssl key; + char certfile[PATH_MAX]; + + if (strlcpy(key.ssl_name, name, sizeof(key.ssl_name)) + >= sizeof(key.ssl_name)) { + log_warn("ssl_load_certfile: certificate name truncated"); + return -1; + } + + s = SPLAY_FIND(ssltree, &env->sc_ssl, &key); + if (s != NULL) + return 0; + + if ((s = calloc(1, sizeof(*s))) == NULL) + fatal(NULL); + + (void)strlcpy(s->ssl_name, key.ssl_name, sizeof(s->ssl_name)); + + if (snprintf(certfile, sizeof(certfile), + "/etc/mail/certs/%s.crt", name) == -1) { + free(s); + return (-1); + } + + if ((s->ssl_cert = ssl_load_file(certfile, &s->ssl_cert_len)) == NULL) { + free(s); + return (-1); + } + + if (snprintf(certfile, sizeof(certfile), + "/etc/mail/certs/%s.key", name) == -1) { + free(s->ssl_cert); + free(s); + return -1; + } + + if ((s->ssl_key = ssl_load_file(certfile, &s->ssl_key_len)) == NULL) { + free(s->ssl_cert); + free(s); + return (-1); + } + + if (s->ssl_cert == NULL || s->ssl_key == NULL) + fatal("invalid certificates"); + + SPLAY_INSERT(ssltree, &env->sc_ssl, s); + + return (0); +} + +void +ssl_init(void) +{ + SSL_library_init(); + SSL_load_error_strings(); + + OpenSSL_add_all_algorithms(); + + /* Init hardware crypto engines. */ + ENGINE_load_builtin_engines(); + ENGINE_register_all_complete(); +} + +void +ssl_setup(struct smtpd *env, struct listener *l) +{ + struct ssl key; + + if (!(l->flags & F_SSL)) + return; + + if (strlcpy(key.ssl_name, l->ssl_cert_name, sizeof(key.ssl_name)) + >= sizeof(key.ssl_name)) + fatal("ssl_setup: certificate name truncated"); + + if ((l->ssl = SPLAY_FIND(ssltree, &env->sc_ssl, &key)) == NULL) + fatal("ssl_setup: certificate tree corrupted"); + + l->ssl_ctx = ssl_ctx_create(); + + if (!ssl_ctx_use_certificate_chain(l->ssl_ctx, + l->ssl->ssl_cert, l->ssl->ssl_cert_len)) + goto err; + if (!ssl_ctx_use_private_key(l->ssl_ctx, + l->ssl->ssl_key, l->ssl->ssl_key_len)) + goto err; + + if (!SSL_CTX_check_private_key(l->ssl_ctx)) + goto err; + if (!SSL_CTX_set_session_id_context(l->ssl_ctx, + (const unsigned char *)l->ssl_cert_name, strlen(l->ssl_cert_name) + 1)) + goto err; + + log_debug("ssl_setup: ssl setup finished for listener: %p", l); + return; + +err: + if (l->ssl_ctx != NULL) + SSL_CTX_free(l->ssl_ctx); + ssl_error("ssl_setup"); + fatal("ssl_setup: cannot set SSL up"); + return; +} + +void +ssl_error(const char *where) +{ + unsigned long code; + char errbuf[128]; + extern int debug; + + if (!debug) + return; + for (; (code = ERR_get_error()) != 0 ;) { + ERR_error_string_n(code, errbuf, sizeof(errbuf)); + log_debug("SSL library error: %s: %s", where, errbuf); + } +} + +void +ssl_session_accept(int fd, short event, void *p) +{ + struct session *s = p; + int ret; + int retry_flag; + int ssl_err; + + if (event == EV_TIMEOUT) { + log_debug("ssl_session_accept: session timed out"); + session_destroy(s); + return; + } + + retry_flag = ssl_err = 0; + + log_debug("ssl_session_accept: accepting client"); + ret = SSL_accept(s->s_ssl); + if (ret <= 0) { + ssl_err = SSL_get_error(s->s_ssl, ret); + + switch (ssl_err) { + case SSL_ERROR_WANT_READ: + retry_flag = EV_READ; + goto retry; + case SSL_ERROR_WANT_WRITE: + retry_flag = EV_WRITE; + goto retry; + case SSL_ERROR_ZERO_RETURN: + case SSL_ERROR_SYSCALL: + if (ret == 0) { + session_destroy(s); + return; + } + /* FALLTHROUGH */ + default: + ssl_error("ssl_session_accept"); + session_destroy(s); + return; + } + } + + event_set(&s->s_bev->ev_read, s->s_fd, EV_READ, ssl_read, s->s_bev); + event_set(&s->s_bev->ev_write, s->s_fd, EV_WRITE, ssl_write, s->s_bev); + + log_info("ssl_session_accept: accepted ssl client"); + s->s_flags |= F_SECURE; + session_pickup(s, NULL); + return; +retry: + event_add(&s->s_ev, &s->s_tv); +} + +void +ssl_session_init(struct session *s) +{ + struct listener *l; + SSL *ssl; + + l = s->s_l; + + if (!(l->flags & F_SSL)) + return; + + log_debug("ssl_session_init: switching to SSL"); + ssl = SSL_new(l->ssl_ctx); + if (ssl == NULL) + goto err; + + if (!SSL_set_ssl_method(ssl, SSLv23_server_method())) + goto err; + if (!SSL_set_fd(ssl, s->s_fd)) + goto err; + SSL_set_accept_state(ssl); + + s->s_ssl = ssl; + + s->s_tv.tv_sec = SMTPD_SESSION_TIMEOUT; + s->s_tv.tv_usec = 0; + event_set(&s->s_ev, s->s_fd, EV_READ|EV_TIMEOUT, ssl_session_accept, s); + event_add(&s->s_ev, &s->s_tv); + return; + + err: + if (ssl != NULL) + SSL_free(ssl); + ssl_error("ssl_session_init"); +} + +void +ssl_session_destroy(struct session *s) +{ + SSL_free(s->s_ssl); +} diff --git a/usr.sbin/smtpd/ssl_privsep.c b/usr.sbin/smtpd/ssl_privsep.c new file mode 100644 index 00000000000..e2f90b794fa --- /dev/null +++ b/usr.sbin/smtpd/ssl_privsep.c @@ -0,0 +1,170 @@ +/* $OpenBSD: ssl_privsep.c,v 1.1 2008/11/01 21:35:28 gilles Exp $ */ + +/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) + * All rights reserved. + * + * This package is an SSL implementation written + * by Eric Young (eay@cryptsoft.com). + * The implementation was written so as to conform with Netscapes SSL. + * + * This library is free for commercial and non-commercial use as long as + * the following conditions are aheared to. The following conditions + * apply to all code found in this distribution, be it the RC4, RSA, + * lhash, DES, etc., code; not just the SSL code. The SSL documentation + * included with this distribution is covered by the same copyright terms + * except that the holder is Tim Hudson (tjh@cryptsoft.com). + * + * Copyright remains Eric Young's, and as such any Copyright notices in + * the code are not to be removed. + * If this package is used in a product, Eric Young should be given attribution + * as the author of the parts of the library used. + * This can be in the form of a textual message at program startup or + * in documentation (online or textual) provided with the package. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * "This product includes cryptographic software written by + * Eric Young (eay@cryptsoft.com)" + * The word 'cryptographic' can be left out if the rouines from the library + * being used are not cryptographic related :-). + * 4. If you include any Windows specific code (or a derivative thereof) from + * the apps directory (application code) you must include an acknowledgement: + * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" + * + * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * The licence and distribution terms for any publically available version or + * derivative of this code cannot be changed. i.e. this code cannot simply be + * copied and put under another distribution licence + * [including the GNU Public Licence.] + */ + +/* + * SSL operations needed when running in a privilege separated environment. + * Adapted from openssl's ssl_rsa.c by Pierre-Yves Ritschard . + */ + +#include <unistd.h> +#include <stdio.h> + +#include <openssl/err.h> +#include <openssl/bio.h> +#include <openssl/objects.h> +#include <openssl/evp.h> +#include <openssl/x509.h> +#include <openssl/pem.h> +#include <openssl/ssl.h> + +int ssl_ctx_use_private_key(SSL_CTX *, char *, off_t); +int ssl_ctx_use_certificate_chain(SSL_CTX *, char *, off_t); + +int +ssl_ctx_use_private_key(SSL_CTX *ctx, char *buf, off_t len) +{ + int ret; + BIO *in; + EVP_PKEY *pkey; + + ret = 0; + + if ((in = BIO_new_mem_buf(buf, len)) == NULL) { + SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY_FILE, ERR_R_BUF_LIB); + return 0; + } + + pkey = PEM_read_bio_PrivateKey(in, NULL, + ctx->default_passwd_callback, + ctx->default_passwd_callback_userdata); + + if (pkey == NULL) { + SSLerr(SSL_F_SSL_CTX_USE_PRIVATEKEY_FILE, ERR_R_PEM_LIB); + goto end; + } + ret = SSL_CTX_use_PrivateKey(ctx, pkey); + EVP_PKEY_free(pkey); +end: + if (in != NULL) + BIO_free(in); + return ret; +} + + +int +ssl_ctx_use_certificate_chain(SSL_CTX *ctx, char *buf, off_t len) +{ + int ret; + BIO *in; + X509 *x; + X509 *ca; + unsigned long err; + + ret = 0; + x = ca = NULL; + + if ((in = BIO_new_mem_buf(buf, len)) == NULL) { + SSLerr(SSL_F_SSL_CTX_USE_CERTIFICATE_CHAIN_FILE, ERR_R_BUF_LIB); + goto end; + } + + if ((x = PEM_read_bio_X509(in, NULL, + ctx->default_passwd_callback, + ctx->default_passwd_callback_userdata)) == NULL) { + SSLerr(SSL_F_SSL_CTX_USE_CERTIFICATE_CHAIN_FILE, ERR_R_PEM_LIB); + goto end; + } + + if (!SSL_CTX_use_certificate(ctx, x) || ERR_peek_error() != 0) + goto end; + + /* If we could set up our certificate, now proceed to + * the CA certificates. + */ + + if (ctx->extra_certs != NULL) { + sk_X509_pop_free(ctx->extra_certs, X509_free); + ctx->extra_certs = NULL; + } + + while ((ca = PEM_read_bio_X509(in, NULL, + ctx->default_passwd_callback, + ctx->default_passwd_callback_userdata)) != NULL) { + + if (!SSL_CTX_add_extra_chain_cert(ctx, ca)) + goto end; + } + + err = ERR_peek_last_error(); + if (ERR_GET_LIB(err) == ERR_LIB_PEM && + ERR_GET_REASON(err) == PEM_R_NO_START_LINE) + ERR_clear_error(); + else + goto end; + + ret = 1; +end: + if (ca != NULL) + X509_free(ca); + if (x != NULL) + X509_free(x); + if (in != NULL) + BIO_free(in); + return (ret); +} diff --git a/usr.sbin/smtpd/store.c b/usr.sbin/smtpd/store.c new file mode 100644 index 00000000000..a67e4e4b1fd --- /dev/null +++ b/usr.sbin/smtpd/store.c @@ -0,0 +1,265 @@ +/* + * Copyright (c) 2008 Gilles Chehade <gilles@openbsd.org> + * + * 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/stat.h> +#include <sys/param.h> +#include <sys/queue.h> +#include <sys/tree.h> +#include <sys/ioctl.h> +#include <sys/socket.h> + +#include <arpa/inet.h> +#include <netinet/in.h> + +#include <dirent.h> +#include <err.h> +#include <errno.h> +#include <event.h> +#include <fcntl.h> +#include <paths.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sysexits.h> +#include <util.h> +#include <unistd.h> + +#include "smtpd.h" + +int fd_copy(int, int, size_t); +int fd_append(int, int); + +int +fd_copy(int dest, int src, size_t len) +{ + char buffer[8192]; + size_t rlen; + + for (; len;) { + + rlen = len < sizeof(buffer) ? len : sizeof(buffer); + if (atomic_read(src, buffer, rlen) == -1) + return 0; + + if (atomic_write(dest, buffer, rlen) == -1) + return 0; + + len -= rlen; + } + + return 1; +} + +int +fd_append(int dest, int src) +{ + struct stat sb; + size_t srcsz; + + if (fstat(src, &sb) == -1) + return 0; + srcsz = sb.st_size; + + if (! fd_copy(dest, src, srcsz)) + return 0; + + return 1; +} + +int +store_write_header(struct batch *batchp, struct message *messagep) +{ + time_t tm; + char timebuf[26]; /* current time */ + char ctimebuf[26]; /* creation time */ + void *p; + char addrbuf[INET6_ADDRSTRLEN]; + + tm = time(NULL); + ctime_r(&tm, timebuf); + timebuf[strcspn(timebuf, "\n")] = '\0'; + + tm = time(&messagep->creation); + ctime_r(&tm, ctimebuf); + ctimebuf[strcspn(ctimebuf, "\n")] = '\0'; + + if (messagep->session_ss.ss_family == PF_INET) { + struct sockaddr_in *ssin = (struct sockaddr_in *)&messagep->session_ss; + p = &ssin->sin_addr.s_addr; + } + if (messagep->session_ss.ss_family == PF_INET6) { + struct sockaddr_in6 *ssin6 = (struct sockaddr_in6 *)&messagep->session_ss; + p = &ssin6->sin6_addr.s6_addr; + } + + bzero(addrbuf, sizeof (addrbuf)); + inet_ntop(messagep->session_ss.ss_family, p, addrbuf, sizeof (addrbuf)); + + if (batchp->type & T_DAEMON_BATCH) { + if (atomic_printfd(messagep->mboxfd, "From %s@%s %s\n", + "MAILER-DAEMON", batchp->env->sc_hostname, timebuf) == -1) + return 0; + if (atomic_printfd(messagep->mboxfd, + "Received: from %s (%s [%s%s])\n" + "\tby %s with ESMTP id %s\n" + "\tfor <%s@%s>; %s\n\n", + messagep->session_helo, messagep->session_hostname, + messagep->session_ss.ss_family == PF_INET ? "" : "IPv6:", addrbuf, + batchp->env->sc_hostname, messagep->message_id, + messagep->sender.user, messagep->sender.domain, ctimebuf) == -1) { + return 0; + } + return 1; + } + + if (atomic_printfd(messagep->mboxfd, + "From %s@%s %s\n" + "Received: from %s (%s [%s%s])\n" + "\tby %s with ESMTP id %s\n" + "\tfor <%s@%s>; %s\n", + messagep->sender.user, messagep->sender.domain, timebuf, + messagep->session_helo, messagep->session_hostname, + messagep->session_ss.ss_family == PF_INET ? "" : "IPv6:", addrbuf, + batchp->env->sc_hostname, batchp->message_id, + messagep->recipient.user, messagep->recipient.domain, ctimebuf) == -1) { + return 0; + } + + return 1; +} + +int +store_write_daemon(struct batch *batchp, struct message *messagep) +{ + u_int32_t i; + struct message *recipient; + + if (! store_write_header(batchp, messagep)) + return 0; + + if (atomic_printfd(messagep->mboxfd, + "Hi !\n\n" + "This is the MAILER-DAEMON, please DO NOT REPLY to this e-mail it is\n" + "just a notification to let you know that an error has occured.\n\n") == -1) { + return 0; + } + + if ((batchp->status & S_BATCH_PERMFAILURE) && atomic_printfd(messagep->mboxfd, + "You ran into a PERMANENT FAILURE, which means that the e-mail can't\n" + "be delivered to the remote host no matter how much I'll try.\n\n" + "Diagnostic:\n" + "%s\n\n", batchp->errorline) == -1) { + return 0; + } + + if ((batchp->status & S_BATCH_TEMPFAILURE) && atomic_printfd(messagep->mboxfd, + "You ran into a TEMPORARY FAILURE, which means that the e-mail can't\n" + "be delivered right now, but could be deliverable at a later time. I\n" + "will attempt until it succeeds for the next four days, then let you\n" + "know if it didn't work out.\n\n" + "Diagnostic:\n" + "%s\n\n", batchp->errorline) == -1) { + return 0; + } + + if (! (batchp->status & S_BATCH_TEMPFAILURE)) { + /* First list the temporary failures */ + i = 0; + TAILQ_FOREACH(recipient, &batchp->messages, entry) { + if (recipient->status & S_MESSAGE_TEMPFAILURE) { + if (i == 0) { + if (atomic_printfd(messagep->mboxfd, + "The following recipients caused a temporary failure:\n") == -1) + return 0; + ++i; + } + if (atomic_printfd(messagep->mboxfd, + "\t<%s@%s>:\n%s\n\n", recipient->recipient.user, recipient->recipient.domain, + recipient->session_errorline) == -1) { + return 0; + } + } + } + + /* Then list the permanent failures */ + i = 0; + TAILQ_FOREACH(recipient, &batchp->messages, entry) { + if (recipient->status & S_MESSAGE_PERMFAILURE) { + if (i == 0) { + if (atomic_printfd(messagep->mboxfd, + "The following recipients caused a permanent failure:\n") == -1) + return 0; + ++i; + } + if (atomic_printfd(messagep->mboxfd, + "\t<%s@%s>:\n%s\n\n", recipient->recipient.user, recipient->recipient.domain, + recipient->session_errorline) == -1) { + return 0; + } + } + } + } + + if (atomic_printfd(messagep->mboxfd, + "Below is a copy of the original message:\n\n") == -1) + return 0; + + if (! fd_append(messagep->mboxfd, messagep->messagefd)) + return 0; + + if (atomic_printfd(messagep->mboxfd, "\n") == -1) + return 0; + + return 1; +} + +int +store_write_message(struct batch *batchp, struct message *messagep) +{ + if (! store_write_header(batchp, messagep)) + return 0; + + if (! fd_append(messagep->mboxfd, messagep->messagefd)) + return 0; + + if (atomic_printfd(messagep->mboxfd, "\n") == -1) + return 0; + + return 1; +} + +int +store_message(struct batch *batchp, struct message *messagep, + int (*writer)(struct batch *, struct message *)) +{ + struct stat sb; + + if (fstat(messagep->mboxfd, &sb) == -1) + return 0; + + if (! writer(batchp, messagep)) { + if (S_ISREG(sb.st_mode)) { + ftruncate(messagep->mboxfd, sb.st_size); + return 0; + } + return 0; + } + + return 1; +} |